diff --git a/datastore/ndb/CONTRIBUTING.md b/datastore/ndb/CONTRIBUTING.md new file mode 100644 index 00000000000..6736efd943c --- /dev/null +++ b/datastore/ndb/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# How to become a contributor and submit your own code + +## Contributor License Agreements + +We'd love to accept your sample apps and patches! Before we can take them, we +have to jump a couple of legal hurdles. + +Please fill out either the individual or corporate 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] + (https://developers.google.com/open-source/cla/individual). + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA] + (https://developers.google.com/open-source/cla/corporate). + +Follow either of the two links above to access the appropriate CLA and +instructions for how to sign and return it. Once we receive it, we'll be able to +accept your pull requests. + +## Contributing A Patch + +1. Submit an issue describing your proposed change to the repo in question. +1. The repo owner will respond to your issue promptly. +1. If your proposed change is accepted, and you haven't already done so, sign a + Contributor License Agreement (see details above). +1. Fork the desired repo, develop and test your code changes. +1. Ensure that your code adheres to the existing style in the sample to which + you are contributing. Refer to the + [Google Cloud Platform Samples Style Guide] + (https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the + recommended coding standards for this organization. +1. Ensure that your code has an appropriate set of unit tests which all pass. +1. Submit a pull request. diff --git a/datastore/ndb/LICENSE b/datastore/ndb/LICENSE new file mode 100644 index 00000000000..4f73028109b --- /dev/null +++ b/datastore/ndb/LICENSE @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + diff --git a/datastore/ndb/README.md b/datastore/ndb/README.md new file mode 100644 index 00000000000..25f67690ece --- /dev/null +++ b/datastore/ndb/README.md @@ -0,0 +1,22 @@ +appengine-ndb-snippets +====================== + +Sample code snippets for NDB. + +How to run the test +=================== + +To run the tests, please install App Engine Python SDK and tox and run +tox with the environment variable PYTHONPATH to the App Engine Python SDK. + +You can install App Engine Python SDK with [Google Cloud SDK](https://cloud.google.com/sdk/) with the following command: + + $ gcloud components update gae-python + +Here is instructions to run the tests with virtualenv, $GCLOUD is your +Google Cloud SDK installation path. + + $ virtualenv -p python2.7 --no-site-packages . + $ source bin/activate + $ pip install tox + $ env PYTHONPATH=${GCLOUD}/platform/google_appengine tox diff --git a/datastore/ndb/app.yaml b/datastore/ndb/app.yaml new file mode 100644 index 00000000000..031ed0fbca6 --- /dev/null +++ b/datastore/ndb/app.yaml @@ -0,0 +1,13 @@ +# dummy app.yaml for nosegae + +application: ndb-snippets +version: 1 +api_version: 1 +runtime: python27 +threadsafe: true + +handlers: +- url: / + static_files: README.md + upload: README.md + mime_type: text/plain diff --git a/datastore/ndb/modeling/README.md b/datastore/ndb/modeling/README.md new file mode 100644 index 00000000000..47d01fb9ed5 --- /dev/null +++ b/datastore/ndb/modeling/README.md @@ -0,0 +1,6 @@ +appengine-ndb-snippets +====================== + += modeling + +Sample code for (Modeling Entity Relationships)[https://cloud.google.com/appengine/articles/modeling]. diff --git a/datastore/ndb/modeling/contact_with_group_models.py b/datastore/ndb/modeling/contact_with_group_models.py new file mode 100644 index 00000000000..871d80c872a --- /dev/null +++ b/datastore/ndb/modeling/contact_with_group_models.py @@ -0,0 +1,84 @@ +# Copyright 2014 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. + +"""Models for representing an address book contact with grouping. + +This module provides models which implement structured properties, +strongly consistent querying, and one-to-many grouping. + +Classes: Contact, Group, PhoneNumber +""" + + +from google.appengine.ext import ndb + + +# [START contact_with_group_models] +class PhoneNumber(ndb.Model): + """A model representing a phone number.""" + phone_type = ndb.StringProperty( + choices=('home', 'work', 'fax', 'mobile', 'other')) + number = ndb.StringProperty() + + +class Group(ndb.Model): + """A model representing groups. + + Expects to have a parent key with the id of the addressbook owner. + + Example: assume my username is tmatsuo + + addrbook_key = ndb.Key('AddressBook', 'tmatsuo') + friends = models.Group(parent=addrbook_key, name="friends") + friends.put() + """ + name = ndb.StringProperty() + description = ndb.TextProperty() + + @property + def members(self): + """Returns a query object with myself as an ancestor.""" + return Contact.query(ancestor=self.key.parent()).filter( + Contact.groups == self.key) + + +class Contact(ndb.Model): + """A Contact model that uses repeated KeyProperty. + + Expects to have a parent key with the id of the addressbook owner. + + Example: assume my username is tmatsuo + + addrbook_key = ndb.Key('AddressBook', 'tmatsuo') + mary = models.Contact(parent=addrbook_key, name='mary', ...) + mary.put() + """ + # Basic info. + name = ndb.StringProperty() + birth_day = ndb.DateProperty() + + # Address info. + address = ndb.StringProperty() + + phone_numbers = ndb.StructuredProperty(PhoneNumber, repeated=True) + + # Company info. + company_title = ndb.StringProperty() + company_name = ndb.StringProperty() + company_description = ndb.TextProperty() + company_address = ndb.StringProperty() + + # Group affiliation + groups = ndb.KeyProperty(Group, repeated=True) +# [END contact_with_group_models] diff --git a/datastore/ndb/modeling/keyproperty_models.py b/datastore/ndb/modeling/keyproperty_models.py new file mode 100644 index 00000000000..ac5dada6ae1 --- /dev/null +++ b/datastore/ndb/modeling/keyproperty_models.py @@ -0,0 +1,64 @@ +# Copyright 2014 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. + + +"""Models for representing a contact with multiple phone numbers. + +This module provides models with a relationship with ndb.KeyProperty to +allow a single contact to have multiple phone numbers. + +Classes: Contact, PhoneNumber +""" + + +# In the original article, it uses ReferenceProperty on the +# PhoneNumber model. With ndb, there is no ReferenceProperty any more, +# so here we use KeyProperty first. However this pattern has a +# consistensy issue, shown in the test_fails function in +# test/test_keyproperty_models.py. + + +from google.appengine.ext import ndb + + +# [START keyproperty_models] +class Contact(ndb.Model): + """A Contact model with KeyProperty.""" + # Basic info. + name = ndb.StringProperty() + birth_day = ndb.DateProperty() + + # Address info. + address = ndb.StringProperty() + + # Company info. + company_title = ndb.StringProperty() + company_name = ndb.StringProperty() + company_description = ndb.TextProperty() + company_address = ndb.StringProperty() + + # The original phone_number property has been replaced by + # the following property. + @property + def phone_numbers(self): + return PhoneNumber.query(PhoneNumber.contact == self.key) + + +class PhoneNumber(ndb.Model): + """A model representing a phone number.""" + contact = ndb.KeyProperty(Contact) + phone_type = ndb.StringProperty( + choices=('home', 'work', 'fax', 'mobile', 'other')) + number = ndb.StringProperty() +# [END keyproperty_models] diff --git a/datastore/ndb/modeling/naive_models.py b/datastore/ndb/modeling/naive_models.py new file mode 100644 index 00000000000..326b53e8d91 --- /dev/null +++ b/datastore/ndb/modeling/naive_models.py @@ -0,0 +1,47 @@ +# Copyright 2014 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. + +"""A simple model for representing an address book contact. + +This module provides a simple model, Contact. +""" + +# This is the first naive model for starting the article. In the +# older version of this article with db module, we used +# PhoneNumberProperty and PostalAddressProperty. With ndb, there are +# no properties like those, so we just use StringProperty instead. + + +from google.appengine.ext import ndb + + +# [START naive_models] +class Contact(ndb.Model): + """A naive Contact model.""" + # Basic info. + name = ndb.StringProperty() + birth_day = ndb.DateProperty() + + # Address info. + address = ndb.StringProperty() + + # Phone info. + phone_number = ndb.StringProperty() + + # Company info. + company_title = ndb.StringProperty() + company_name = ndb.StringProperty() + company_description = ndb.TextProperty() + company_address = ndb.StringProperty() +# [END naive_models] diff --git a/datastore/ndb/modeling/parent_child_models.py b/datastore/ndb/modeling/parent_child_models.py new file mode 100644 index 00000000000..2f7fc9ef0d8 --- /dev/null +++ b/datastore/ndb/modeling/parent_child_models.py @@ -0,0 +1,63 @@ +# Copyright 2014 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. + +"""Models representing a contact with multiple phone numbers. + +This module provides models with a relationship with parent-child +relationship to allow strong consistent query. + +Classes: Contact, PhoneNumber +""" + + +# In the original article, it uses ReferenceProperty on the +# PhoneNumber model. With ndb, there is no ReferenceProperty any more, +# so here we use Parent/Child relationship instead. + + +from google.appengine.ext import ndb + + +# [START parent_child_models] +class Contact(ndb.Model): + """A Contact model with Parent/Child relationship.""" + # Basic info. + name = ndb.StringProperty() + birth_day = ndb.DateProperty() + + # Address info. + address = ndb.StringProperty() + + # Company info. + company_title = ndb.StringProperty() + company_name = ndb.StringProperty() + company_description = ndb.TextProperty() + company_address = ndb.StringProperty() + + # The original phone_number property has been replaced by + # the following property. + @property + def phone_numbers(self): + return PhoneNumber.query(ancestor=self.key) + + +class PhoneNumber(ndb.Model): + """A model representing a phone number. + + Expects to have Contact's key as the parent key. + """ + phone_type = ndb.StringProperty( + choices=('home', 'work', 'fax', 'mobile', 'other')) + number = ndb.StringProperty() +# [END parent_child_models] diff --git a/datastore/ndb/modeling/relation_model_models.py b/datastore/ndb/modeling/relation_model_models.py new file mode 100644 index 00000000000..c5dc7ab39e2 --- /dev/null +++ b/datastore/ndb/modeling/relation_model_models.py @@ -0,0 +1,112 @@ +# Copyright 2014 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. + +"""Models for representing a contact with multiple titles and groups. + +This module provides models with structured properties, strong +consistent querying, one-to-many grouping, many-to-many relationships. + +Classes: Company, ContactCompany, Contact, Group, PhoneNumber +""" + + +from google.appengine.ext import ndb + + +# [START relation_model_models] +class PhoneNumber(ndb.Model): + """A model representing a phone number.""" + phone_type = ndb.StringProperty( + choices=('home', 'work', 'fax', 'mobile', 'other')) + number = ndb.StringProperty() + + +class Group(ndb.Model): + """A model representing groups. + + Expects to have a parent key with the id of the addressbook owner. + + Example: assume my username is tmatsuo + + addrbook_key = ndb.Key('AddressBook', 'tmatsuo') + friends = models.Group(parent=addrbook_key, name="friends") + friends.put() + """ + name = ndb.StringProperty() + description = ndb.TextProperty() + + @property + def members(self): + """Returns a query object with myself as an ancestor.""" + return Contact.query(ancestor=self.key.parent()).filter( + Contact.groups == self.key) + + +class Contact(ndb.Model): + """A model representing a contact entry. + + Expects to have a parent key with the id of the addressbook owner. + + Example: assume my username is tmatsuo + + addrbook_key = ndb.Key('AddressBook', 'tmatsuo') + mary = models.Contact(parent=addrbook_key, name='Mary', ...) + mary.put() + """ + # Basic info. + name = ndb.StringProperty() + birth_day = ndb.DateProperty() + + # Address info. + address = ndb.StringProperty() + + phone_numbers = ndb.StructuredProperty(PhoneNumber, repeated=True) + + # Group affiliation + groups = ndb.KeyProperty(Group, repeated=True) + + # The original organization properties have been replaced by + # the following property. + @property + def companies(self): + rels = ContactCompany.query(ancestor=self.key.parent()).filter( + ContactCompany.contact == self.key) + keys = [rel.company for rel in rels] + return ndb.get_multi(keys) + + +class Company(ndb.Model): + """A model representing a company.""" + name = ndb.StringProperty() + description = ndb.TextProperty() + company_address = ndb.StringProperty() + + +class ContactCompany(ndb.Model): + """A model representing a relation between a contact and a company. + + Expects to have a parent key with the id of the addressbook owner. + + Example: assume my username is tmatsuo + + addrbook_key = ndb.Key('AddressBook', 'tmatsuo') + mary = models.Contact(parent=addrbook_key, name='Mary', ...) + mary.put() + models.ContactCompany(parent=addrbook_key, contact=mary.key, + company=google.key, title='engineer').put() + """ + contact = ndb.KeyProperty(Contact, required=True) + company = ndb.KeyProperty(Company, required=True) + title = ndb.StringProperty() +# [END relation_model_models] diff --git a/datastore/ndb/modeling/structured_property_models.py b/datastore/ndb/modeling/structured_property_models.py new file mode 100644 index 00000000000..ec3777fb5d1 --- /dev/null +++ b/datastore/ndb/modeling/structured_property_models.py @@ -0,0 +1,51 @@ +# Copyright 2014 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. + +"""Models for representing a contact with multiple phone numbers. + +This module provides models with NDB's StructuredProperty to represent +the one-to-many relationship. + +Classes: Contact, PhoneNumber +""" + + +from google.appengine.ext import ndb + + +# [START structured_property_models] +class PhoneNumber(ndb.Model): + """A model representing a phone number.""" + phone_type = ndb.StringProperty( + choices=('home', 'work', 'fax', 'mobile', 'other')) + number = ndb.StringProperty() + + +class Contact(ndb.Model): + """A Contact model that uses StructuredProperty for phone numbers.""" + # Basic info. + name = ndb.StringProperty() + birth_day = ndb.DateProperty() + + # Address info. + address = ndb.StringProperty() + + phone_numbers = ndb.StructuredProperty(PhoneNumber, repeated=True) + + # Company info. + company_title = ndb.StringProperty() + company_name = ndb.StringProperty() + company_description = ndb.TextProperty() + company_address = ndb.StringProperty() +# [END structured_property_models] diff --git a/datastore/ndb/modeling/tests/test_base.py b/datastore/ndb/modeling/tests/test_base.py new file mode 100644 index 00000000000..7b20270be41 --- /dev/null +++ b/datastore/ndb/modeling/tests/test_base.py @@ -0,0 +1,42 @@ +# Copyright 2014 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. + +"""Test classes for code snippet for modeling article.""" + + +import unittest + +from google.appengine.ext import testbed +from google.appengine.datastore import datastore_stub_util + + +class TestCase(unittest.TestCase): + """A base test case for common setup/teardown tasks for test.""" + def setUp(self): + """Setup the datastore and memcache stub.""" + # First, create an instance of the Testbed class. + self.testbed = testbed.Testbed() + # Then activate the testbed, which prepares the service stubs for + # use. + self.testbed.activate() + # Create a consistency policy that will simulate the High + # Replication consistency model. + self.policy = datastore_stub_util.PseudoRandomHRConsistencyPolicy( + probability=0) + # Initialize the datastore stub with this policy. + self.testbed.init_datastore_v3_stub(consistency_policy=self.policy) + self.testbed.init_memcache_stub() + + def tearDown(self): + self.testbed.deactivate() diff --git a/datastore/ndb/modeling/tests/test_contact_with_group_models.py b/datastore/ndb/modeling/tests/test_contact_with_group_models.py new file mode 100644 index 00000000000..2993d3228d7 --- /dev/null +++ b/datastore/ndb/modeling/tests/test_contact_with_group_models.py @@ -0,0 +1,62 @@ +# Copyright 2014 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. + +"""Test classes for code snippet for modeling article.""" + + +import unittest + +from google.appengine.ext import ndb + +import contact_with_group_models as models +import test_base + + +class ContactTestCase(test_base.TestCase): + """A test case for the Contact model with groups.""" + def setUp(self): + """Creates 3 contacts and 1 group. + + Assuming the group and contacts are private and belong to tmatsuo's + addressbook. + """ + super(ContactTestCase, self).setUp() + self.myaddressbook_key = ndb.Key('AddressBook', 'tmatsuo') + + friends = models.Group(parent=self.myaddressbook_key, name='friends') + friends.put() + self.friends_key = friends.key + mary = models.Contact(parent=self.myaddressbook_key, name='Mary') + mary.put() + self.mary_key = mary.key + + def test_groups(self): + # Add Mary to your 'friends' group + mary = self.mary_key.get() + friends = self.friends_key.get() + if friends.key not in mary.groups: + mary.groups.append(friends.key) + mary.put() + + # Now Mary is your friend + mary = self.mary_key.get() + self.assertTrue(friends.key in mary.groups) + + # How about 'members' property? + friend_list = friends.members.fetch() + self.assertEqual(len(friend_list), 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/datastore/ndb/modeling/tests/test_keyproperty_models.py b/datastore/ndb/modeling/tests/test_keyproperty_models.py new file mode 100644 index 00000000000..07c9dabfd3e --- /dev/null +++ b/datastore/ndb/modeling/tests/test_keyproperty_models.py @@ -0,0 +1,55 @@ +# Copyright 2014 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. + +"""Test classes for code snippet for modeling article.""" + + +import unittest + +import keyproperty_models as models +import test_base + + +class ContactTestCase(test_base.TestCase): + """A test case for the Contact model class with KeyProperty.""" + NAME = 'Takashi Matsuo' + + def setUp(self): + super(ContactTestCase, self).setUp() + contact = models.Contact(name=self.NAME) + contact.put() + self.contact_key = contact.key + + def test_basic(self): + """Test for getting a Contact entity.""" + contact = self.contact_key.get() + self.assertEqual(contact.name, self.NAME) + + # This test fails because of the eventual consistency nature of + # HRD. We configure HRD consistency for the test datastore stub to + # match the production behavior. + @unittest.skip("Intentionally showing a failing test case.") + # [START failing_test] + def test_fails(self): + contact = self.contact_key.get() + models.PhoneNumber(contact=self.contact_key, + phone_type='home', + number='(650) 555 - 2200').put() + numbers = contact.phone_numbers.fetch() + self.assertEqual(1, len(numbers)) + # [END failing_test] + + +if __name__ == '__main__': + unittest.main() diff --git a/datastore/ndb/modeling/tests/test_naive_models.py b/datastore/ndb/modeling/tests/test_naive_models.py new file mode 100644 index 00000000000..e2294f7648c --- /dev/null +++ b/datastore/ndb/modeling/tests/test_naive_models.py @@ -0,0 +1,41 @@ +# Copyright 2014 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. + +"""Test classes for code snippet for modeling article.""" + + +import unittest + +import naive_models as models +import test_base + + +class ContactTestCase(test_base.TestCase): + """A test case for the naive Contact model classe.""" + NAME = 'Takashi Matsuo' + + def setUp(self): + super(ContactTestCase, self).setUp() + contact = models.Contact(name=self.NAME) + contact.put() + self.contact_key = contact.key + + def test_basic(self): + """Test for getting a NaiveContact entity.""" + contact = self.contact_key.get() + self.assertEqual(contact.name, self.NAME) + + +if __name__ == '__main__': + unittest.main() diff --git a/datastore/ndb/modeling/tests/test_parent_child_models.py b/datastore/ndb/modeling/tests/test_parent_child_models.py new file mode 100644 index 00000000000..152776031ab --- /dev/null +++ b/datastore/ndb/modeling/tests/test_parent_child_models.py @@ -0,0 +1,88 @@ +# Copyright 2014 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. + +"""Test classes for code snippet for modeling article.""" + + +import unittest + +import parent_child_models as models +import test_base + +from google.appengine.ext import ndb + + +class ContactTestCase(test_base.TestCase): + """A test case for the Contact model class with KeyProperty.""" + NAME = 'Takashi Matsuo' + + def setUp(self): + super(ContactTestCase, self).setUp() + contact = models.Contact(name=self.NAME) + contact.put() + self.contact_key = contact.key + + def test_basic(self): + """Test for getting a Contact entity.""" + contact = self.contact_key.get() + self.assertEqual(contact.name, self.NAME) + + # [START succeeding_test] + def test_success(self): + contact = self.contact_key.get() + models.PhoneNumber(parent=self.contact_key, + phone_type='home', + number='(650) 555 - 2200').put() + numbers = contact.phone_numbers.fetch() + self.assertEqual(1, len(numbers)) + # [START succeeding_test] + + def test_phone_numbers(self): + """A test for 'phone_numbers' property.""" + models.PhoneNumber(parent=self.contact_key, + phone_type='home', + number='(650) 555 - 2200').put() + models.PhoneNumber(parent=self.contact_key, + phone_type='mobile', + number='(650) 555 - 2201').put() + contact = self.contact_key.get() + for phone in contact.phone_numbers: + # it doesn't ensure any order + if phone.phone_type == 'home': + self.assertEqual('(650) 555 - 2200', phone.number) + elif phone.phone_type == 'mobile': + self.assertEqual(phone.number, '(650) 555 - 2201') + + # filer the phone numbers by type. Note that this is an + # ancestor query. + query = contact.phone_numbers.filter( + models.PhoneNumber.phone_type == 'home') + entities = query.fetch() + self.assertEqual(1, len(entities)) + self.assertEqual(entities[0].number, '(650) 555 - 2200') + + # delete the mobile phones + query = contact.phone_numbers.filter( + models.PhoneNumber.phone_type == 'mobile') + ndb.delete_multi([e.key for e in query]) + + # make sure there's no mobile phones any more + query = contact.phone_numbers.filter( + models.PhoneNumber.phone_type == 'mobile') + entities = query.fetch() + self.assertEqual(0, len(entities)) + + +if __name__ == '__main__': + unittest.main() diff --git a/datastore/ndb/modeling/tests/test_relation_model_models.py b/datastore/ndb/modeling/tests/test_relation_model_models.py new file mode 100644 index 00000000000..c0efc5b8ee3 --- /dev/null +++ b/datastore/ndb/modeling/tests/test_relation_model_models.py @@ -0,0 +1,65 @@ +# Copyright 2014 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. + +"""Test classes for code snippet for modeling article.""" + + +import unittest + +import relation_model_models as models +import test_base + +from google.appengine.ext import ndb + + +class ContactTestCase(test_base.TestCase): + """A test case for the Contact model with relationship model.""" + def setUp(self): + """Creates 1 contact and 1 company. + + Assuming the contact belongs to tmatsuo's addressbook. + """ + super(ContactTestCase, self).setUp() + self.myaddressbook_key = ndb.Key('AddressBook', 'tmatsuo') + mary = models.Contact(parent=self.myaddressbook_key, name='Mary') + mary.put() + self.mary_key = mary.key + google = models.Company(name='Google') + google.put() + self.google_key = google.key + candit = models.Company(name='Candit') + candit.put() + self.candit_key = candit.key + + def test_relationship(self): + """Two companies hire Mary.""" + mary = self.mary_key.get() + google = self.google_key.get() + candit = self.candit_key.get() + # first google hires Mary + models.ContactCompany(parent=self.myaddressbook_key, + contact=mary.key, + company=google.key, + title='engineer').put() + # then another company named 'candit' hires Mary too + models.ContactCompany(parent=self.myaddressbook_key, + contact=mary.key, + company=candit.key, + title='president').put() + # get the list of companies that Mary belongs to + self.assertEqual(len(mary.companies), 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/datastore/ndb/modeling/tests/test_structured_property_models.py b/datastore/ndb/modeling/tests/test_structured_property_models.py new file mode 100644 index 00000000000..9bdac47ce2a --- /dev/null +++ b/datastore/ndb/modeling/tests/test_structured_property_models.py @@ -0,0 +1,72 @@ +# Copyright 2014 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. + +"""Test classes for code snippet for modeling article.""" + + +import unittest + +import structured_property_models as models +import test_base + + +class ContactTestCase(test_base.TestCase): + """A test case for the Contact model with StructuredProperty.""" + def setUp(self): + """Creates one Contact entity with 2 phone numbers.""" + super(ContactTestCase, self).setUp() + scott = models.Contact(name='scott') + scott.phone_numbers.append( + models.PhoneNumber(phone_type='home', + number='(650) 555 - 2200')) + scott.phone_numbers.append( + models.PhoneNumber(phone_type='mobile', + number='(650) 555 - 2201')) + scott.put() + self.scott_key = scott.key + + def test_phone_numbers(self): + """A test for 'phone_numbers' property.""" + scott = self.scott_key.get() + # make sure there are 2 numbers, you can expect the order is preserved. + self.assertEqual(len(scott.phone_numbers), 2) + self.assertEqual(scott.phone_numbers[0].phone_type, 'home') + self.assertEqual(scott.phone_numbers[0].number, '(650) 555 - 2200') + self.assertEqual(scott.phone_numbers[1].phone_type, 'mobile') + self.assertEqual(scott.phone_numbers[1].number, '(650) 555 - 2201') + + # filer scott's phone numbers by type + home_numbers = [phone_number for phone_number in scott.phone_numbers + if phone_number.phone_type == 'home'] + self.assertEqual(len(home_numbers), 1) + self.assertEqual(home_numbers[0].number, '(650) 555 - 2200') + + # delete scott's mobile phone + mobile_numbers = [phone_number for phone_number in scott.phone_numbers + if phone_number.phone_type == 'mobile'] + self.assertEqual(len(mobile_numbers), 1) + lost_phone = mobile_numbers[0] + scott.phone_numbers.remove(lost_phone) + # Updates the entity (resending all its properties over the wire). + scott.put() + + # make sure there's no mobile phone of scott + scott = self.scott_key.get() + self.assertEqual(len(scott.phone_numbers), 1) + self.assertEqual(scott.phone_numbers[0].phone_type, 'home') + self.assertEqual(scott.phone_numbers[0].number, '(650) 555 - 2200') + + +if __name__ == '__main__': + unittest.main() diff --git a/datastore/ndb/overview/README.md b/datastore/ndb/overview/README.md new file mode 100644 index 00000000000..098498d50b8 --- /dev/null +++ b/datastore/ndb/overview/README.md @@ -0,0 +1,57 @@ +## NDB Overview Sample + +This is a sample app for Google App Engine that exercises the [NDB Python API](https://cloud.google.com/appengine/docs/python/ndb/). + +See our other [Google Cloud Platform github +repos](https://github.com/GoogleCloudPlatform) for sample applications and +scaffolding for other python frameworks and use cases. + +## 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. + + ``` + 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. + + ``` + git clone https://github.com/GoogleCloudPlatform/datastore-pthon-samples.git + cd datastore-python-samples/ndb-overview + ``` +1. Run this project locally from the command line. + + ``` + gcloud preview app run ./ + ``` + +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 ./ + ``` + +1. Congratulations! Your application is now live at your-app-id.appspot.com + +## Contributing changes + +* See [CONTRIBUTING.md](../../CONTRIBUTING.md) + +## Licensing + +* See [LICENSE](../../LICENSE) diff --git a/datastore/ndb/overview/app.yaml b/datastore/ndb/overview/app.yaml new file mode 100644 index 00000000000..5227472ff0c --- /dev/null +++ b/datastore/ndb/overview/app.yaml @@ -0,0 +1,22 @@ +# 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: + +# 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/datastore/ndb/overview/favicon.ico b/datastore/ndb/overview/favicon.ico new file mode 100644 index 00000000000..23c553a2966 Binary files /dev/null and b/datastore/ndb/overview/favicon.ico differ diff --git a/datastore/ndb/overview/index.yaml b/datastore/ndb/overview/index.yaml new file mode 100644 index 00000000000..f933a1c8e6d --- /dev/null +++ b/datastore/ndb/overview/index.yaml @@ -0,0 +1,17 @@ +indexes: + +# AUTOGENERATED + +# This index.yaml is automatically updated whenever the dev_appserver +# detects that a new type of query is run. If you want to manage the +# index.yaml file manually, remove the above marker line (the line +# saying "# AUTOGENERATED"). If you want to manage some indexes +# manually, move them above the marker line. The index.yaml file is +# automatically uploaded to the admin console when you next deploy +# your application using appcfg.py. + +- kind: Greeting + ancestor: yes + properties: + - name: date + direction: desc diff --git a/datastore/ndb/overview/main.py b/datastore/ndb/overview/main.py new file mode 100644 index 00000000000..91befce85f0 --- /dev/null +++ b/datastore/ndb/overview/main.py @@ -0,0 +1,79 @@ +# 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. + +# [START all] +import cgi +import urllib + +import webapp2 + +from google.appengine.ext import ndb + + +# [START greeting] +class Greeting(ndb.Model): + """Models an individual Guestbook entry with content and date.""" + content = ndb.StringProperty() + date = ndb.DateTimeProperty(auto_now_add=True) +# [END greeting] + +# [START query] + @classmethod + def query_book(cls, ancestor_key): + return cls.query(ancestor=ancestor_key).order(-cls.date) + + +class MainPage(webapp2.RequestHandler): + def get(self): + self.response.out.write('') + guestbook_name = self.request.get('guestbook_name') + ancestor_key = ndb.Key("Book", guestbook_name or "*notitle*") + greetings = Greeting.query_book(ancestor_key).fetch(20) + + for greeting in greetings: + self.response.out.write('
%s
' % + cgi.escape(greeting.content)) +# [END query] + + self.response.out.write(""" +
+
+
+
+
+
Guestbook name: +
+ + """ % (urllib.urlencode({'guestbook_name': guestbook_name}), + cgi.escape(guestbook_name))) + + +# [START submit] +class SubmitForm(webapp2.RequestHandler): + def post(self): + # We set the parent key on each 'Greeting' to ensure each guestbook's + # greetings are in the same entity group. + guestbook_name = self.request.get('guestbook_name') + greeting = Greeting(parent=ndb.Key("Book", guestbook_name or "*notitle*"), + content=self.request.get('content')) + greeting.put() +# [END submit] + self.redirect('/?' + urllib.urlencode({'guestbook_name': guestbook_name})) + + +app = webapp2.WSGIApplication([ + ('/', MainPage), + ('/sign', SubmitForm) +]) +# [END all] diff --git a/datastore/ndb/tox.ini b/datastore/ndb/tox.ini new file mode 100644 index 00000000000..9a17a256173 --- /dev/null +++ b/datastore/ndb/tox.ini @@ -0,0 +1,14 @@ +[tox] +skipsdist = True +envlist = flake8-py2.7,py2.7 + +[testenv:flake8-py2.7] +deps = flake8 +commands = flake8 modeling + +[testenv:py2.7] +deps = + nose + nosegae +commands = + nosetests --with-gae modeling diff --git a/datastore/ndb/transactions/README.md b/datastore/ndb/transactions/README.md new file mode 100644 index 00000000000..39c983ae0ac --- /dev/null +++ b/datastore/ndb/transactions/README.md @@ -0,0 +1,61 @@ +## NDB Transactions Sample + +This is a sample app for Google App Engine that exercises the [NDB Transactions Python API](https://cloud-dot-devsite.googleplex.com/appengine/docs/python/ndb/transactions) + +This app presents a list of notes. After you submit a note with a particular title, you may not change that note or submit a new note with the same title. There are multiple note pages available. + +See our other [Google Cloud Platform github +repos](https://github.com/GoogleCloudPlatform) for sample applications and +scaffolding for other python frameworks and use cases. + +## 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. + + ``` + 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. + + ``` + git clone https://github.com/GoogleCloudPlatform/datastore-samples.git + cd datastore-samples/python/ndb/transactions + ``` +1. Run this project locally from the command line. + + ``` + pip install -r requirements.txt -t lib/ + gcloud preview app run ./app.yaml + ``` + +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. + + ``` + pip install -r requirements.txt -t lib/ + gcloud preview app deploy ./app.yaml + ``` + +1. Congratulations! Your application is now live at your-app-id.appspot.com + +## Contributing changes + +* See [CONTRIBUTING.md](../../../CONTRIBUTING.md) + +## Licensing + +* See [LICENSE](../../../LICENSE) diff --git a/datastore/ndb/transactions/app.yaml b/datastore/ndb/transactions/app.yaml new file mode 100644 index 00000000000..a71a3a86c34 --- /dev/null +++ b/datastore/ndb/transactions/app.yaml @@ -0,0 +1,18 @@ +# 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: + +# 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 diff --git a/datastore/ndb/transactions/appengine_config.py b/datastore/ndb/transactions/appengine_config.py new file mode 100644 index 00000000000..a51535059b2 --- /dev/null +++ b/datastore/ndb/transactions/appengine_config.py @@ -0,0 +1,11 @@ +""" +`appengine_config.py` is automatically loaded when Google App Engine +starts a new instance of your application. This runs before any +WSGI applications specified in app.yaml are loaded. +""" + +from google.appengine.ext import vendor + +# Third-party libraries are stored in "lib", vendoring will make +# sure that they are importable by the application. +vendor.add('lib') diff --git a/datastore/ndb/transactions/favicon.ico b/datastore/ndb/transactions/favicon.ico new file mode 100644 index 00000000000..23c553a2966 Binary files /dev/null and b/datastore/ndb/transactions/favicon.ico differ diff --git a/datastore/ndb/transactions/main.py b/datastore/ndb/transactions/main.py new file mode 100644 index 00000000000..6f06bc067ff --- /dev/null +++ b/datastore/ndb/transactions/main.py @@ -0,0 +1,188 @@ +# 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 cgi +import flask +import random +import urllib +# [START taskq-imp] +from google.appengine.ext import ndb +from google.appengine.api import taskqueue +# [END taskq-imp] + + +class Note(ndb.Model): + """Models an individual Note entry with content.""" + content = ndb.StringProperty() + + +def parent_key(page_name): + return ndb.Key("Parent", page_name) + + +app = flask.Flask(__name__) + + +@app.route('/') +def main_page(): + page_name = flask.request.args.get('page_name', 'default') + response = """ + +

Permenant note page: %s

""" % cgi.escape(page_name) + + parent = parent_key(page_name) + notes = Note.query(ancestor=parent).fetch(20) + for note in notes: + response += '

%s

' % cgi.escape(note.key.id()) + response += '
%s
' % cgi.escape(note.content) + + response += """ +
+
+ Submit Note:
+ +
""" % urllib.urlencode({'page_name': page_name}) + response += """ +
+
Switch page: +
+ + """ % cgi.escape(page_name, quote=True) + + return response + + +# [START standard] +@ndb.transactional +def insert_if_absent(note_key, note): + fetch = note_key.get() + if fetch is None: + note.put() + return True + return False +# [END standard] + + +# [START two-tries] +@ndb.transactional(retries=1) +def insert_if_absent_2_retries(note_key, note): + # do insert + # [END two-tries] + fetch = note_key.get() + if fetch is None: + note.put() + return True + return False + + +# [START cross-group] +@ndb.transactional(xg=True) +def insert_if_absent_xg(note_key, note): + # do insert + # [END cross-group] + fetch = note_key.get() + if fetch is None: + note.put() + return True + return False + + +# [START sometimes] +def insert_if_absent_sometimes(note_key, note): + # do insert + # [END sometimes] + fetch = note_key.get() + if fetch is None: + note.put() + return True + return False + + +# [START indep] +@ndb.transactional(propagation=ndb.TransactionOptions.INDEPENDENT) +def insert_if_absent_indep(note_key, note): + # do insert + # [END indep] + fetch = note_key.get() + if fetch is None: + note.put() + return True + return False + +# [START taskq] +@ndb.transactional +def insert_if_absent_taskq(note_key, note): + taskqueue.add(url=flask.url_for('taskq_worker'), transactional=True) + # do insert + # [END taskq] + fetch = note_key.get() + if fetch is None: + note.put() + return True + return False + + +@app.route('/worker') +def taskq_worker(): + pass + +def pick_random_insert(note_key, note): + choice = random.randint(0, 5) + if choice == 0: + # [START calling2] + inserted = insert_if_absent(note_key, note) + # [END calling2] + elif choice == 1: + inserted = insert_if_absent_2_retries(note_key, note) + elif choice == 2: + inserted = insert_if_absent_xg(note_key, note) + elif choice == 3: + # [START sometimes-call] + inserted = ndb.transaction(lambda: insert_if_absent_sometimes(note_key, note)) + # [END sometimes-call] + elif choice == 4: + inserted = insert_if_absent_indep(note_key, note) + elif choice == 5: + inserted = insert_if_absent_taskq(note_key, note) + return inserted + + +@app.route('/add', methods=['POST']) +def add_note(): + page_name = flask.request.args.get('page_name', 'default') + note_title = flask.request.form['note_title'] + note_text = flask.request.form['note_text'] + + parent = parent_key(page_name) + + choice = random.randint(0, 1) + if choice == 0: + # Use transactional function + # [START calling] + note_key = ndb.Key(Note, note_title, parent=parent) + note = Note(key=note_key, content=note_text) + # [END calling] + if pick_random_insert(note_key, note) is False: + return 'Already there
Return' % flask.url_for('main_page', page_name=page_name) + return flask.redirect(flask.url_for('main_page', page_name=page_name)) + elif choice == 1: + # Use get_or_insert, which is transactional + note = Note.get_or_insert(note_title, parent=parent, content=note_text) + if note.content != note_text: + return 'Already there
Return' % flask.url_for('main_page', page_name=page_name) + return flask.redirect(flask.url_for('main_page', page_name=page_name)) + + +if __name__ == '__main__': + app.run() diff --git a/datastore/ndb/transactions/requirements.txt b/datastore/ndb/transactions/requirements.txt new file mode 100644 index 00000000000..a257e293a8b --- /dev/null +++ b/datastore/ndb/transactions/requirements.txt @@ -0,0 +1,7 @@ +# This requirements file lists all third-party dependencies for this project. +# +# Run 'pip install -r requirements.txt -t lib/' to install these dependencies +# in `lib/` subdirectory. +# +# Note: The `lib` directory is added to `sys.path` by `appengine_config.py`. +Flask==0.10