diff --git a/.travis.yml b/.travis.yml index 932fee9e9fcc..318d883dff97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ cache: - $HOME/.cache env: global: - secure: V8kTaIK8NYMEUVzaekLoVgJzz5/9yA/KKL8CVgOmiPEjt1o1wAXy+ojyXCgjGmB16OOcTYKtXKvOBndDg99MxUzMc9uLrHF4ub2fJZy3ZoCfPaxHNOpIOTTAhB8J/nog/JpW5NnJOBGE8fAQ/TUy8nSvOwe27n4qKO5eWqTy5kA= + secure: RPClDmwFZnpBIKtMvlzjzZVam4flvJtSvxFD8mHCQVQ//KqyBbQxl970kqStOK7p0RXkOB3XuFDvVixqyuptoQ8wTdgSBEPAub4DwRpcmCc1exzErHIt9zep3cQhSUuzl8N/tNl3o6GG04NsZTeErORqxfDvk3WbqFa9593G358= addons: apt: sources: @@ -24,7 +24,6 @@ addons: install: - pip install --upgrade pip wheel virtualenv - pip install --upgrade nox-automation coverage -# Temporarily install this from source. - pip install --upgrade git+https://github.com/dhermes/ci-diff-helper.git script: - ./scripts/travis.sh diff --git a/conftest.py b/conftest.py index 1a39069a5e13..a4f3c1008f8a 100644 --- a/conftest.py +++ b/conftest.py @@ -32,6 +32,7 @@ def cloud_config(): storage_bucket=os.environ.get('CLOUD_STORAGE_BUCKET'), client_secrets=os.environ.get('GOOGLE_CLIENT_SECRETS'), bigtable_instance=os.environ.get('BIGTABLE_CLUSTER'), + spanner_instance=os.environ.get('SPANNER_INSTANCE'), api_key=os.environ.get('API_KEY')) diff --git a/nox.py b/nox.py index 70647b037703..15209e68673d 100644 --- a/nox.py +++ b/nox.py @@ -138,7 +138,7 @@ def _setup_appengine_sdk(session): '--cov-report', 'term'] FLAKE8_COMMON_ARGS = [ - '--show-source', '--builtin', 'gettext', '--max-complexity', '15', + '--show-source', '--builtin', 'gettext', '--max-complexity', '20', '--import-order-style', 'google', '--exclude', '.nox,.cache,env,lib,generated_pb2', ] diff --git a/spanner/cloud-client/README.rst b/spanner/cloud-client/README.rst new file mode 100644 index 000000000000..5e5bd52f3027 --- /dev/null +++ b/spanner/cloud-client/README.rst @@ -0,0 +1,151 @@ +.. This file is automatically generated. Do not edit this file directly. + +Google Cloud Spanner Python Samples +=============================================================================== + +This directory contains samples for Google Cloud Spanner. `Google Cloud Spanner`_ is a highly scalable, transactional, managed, NewSQL database service. Cloud Spanner solves the need for a horizontally-scaling database with consistent global transactions and SQL semantics. + + + + +.. _Google Cloud Spanner: https://cloud.google.com/spanner/docs + +Setup +------------------------------------------------------------------------------- + + +Authentication +++++++++++++++ + +Authentication is typically done through `Application Default Credentials`_, +which means you do not have to change the code to authenticate as long as +your environment has credentials. You have a few options for setting up +authentication: + +#. When running locally, use the `Google Cloud SDK`_ + + .. code-block:: bash + + gcloud beta auth application-default login + + +#. When running on App Engine or Compute Engine, credentials are already + set-up. However, you may need to configure your Compute Engine instance + with `additional scopes`_. + +#. You can create a `Service Account key file`_. This file can be used to + authenticate to Google Cloud Platform services from any environment. To use + the file, set the ``GOOGLE_APPLICATION_CREDENTIALS`` environment variable to + the path to the key file, for example: + + .. code-block:: bash + + export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account.json + +.. _Application Default Credentials: https://cloud.google.com/docs/authentication#getting_credentials_for_server-centric_flow +.. _additional scopes: https://cloud.google.com/compute/docs/authentication#using +.. _Service Account key file: https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount + +Install Dependencies +++++++++++++++++++++ + +#. Install `pip`_ and `virtualenv`_ if you do not already have them. + +#. Create a virtualenv. Samples are compatible with Python 2.7 and 3.4+. + + .. code-block:: bash + + $ virtualenv env + $ source env/bin/activate + +#. Install the dependencies needed to run the samples. + + .. code-block:: bash + + $ pip install -r requirements.txt + +.. _pip: https://pip.pypa.io/ +.. _virtualenv: https://virtualenv.pypa.io/ + +Samples +------------------------------------------------------------------------------- + +Snippets ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + + +To run this sample: + +.. code-block:: bash + + $ python snippets.py + + usage: snippets.py [-h] [--database-name DATABASE_NAME] + instance_name + {insert_data,query_data,read_data,update_data,read_write_transaction,query_data_with_index,read_data_with_index,read_data_with_storing_index} + ... + + This application demonstrates how to do basic operations using Cloud + Spanner. + + For more information, see the README.rst under /spanner. + + positional arguments: + instance_name Your Cloud Spanner instance name. + {insert_data,query_data,read_data,update_data,read_write_transaction,query_data_with_index,read_data_with_index,read_data_with_storing_index} + insert_data Inserts sample data into the given database. The + database and table must already exist and can be + created using `create_database`. + query_data Queries sample data from the database using SQL. + read_data Reads sample data from the database. + update_data Updates sample data in the database. This updates the + `MarketingBudget` column which must be created before + running this sample. Run the following query on your + database to create the column: ALTER TABLE Albums ADD + COLUMN MarketingBudget INT64 + read_write_transaction + Performs a read-write transaction to update two sample + records in the database. This will transfer 200,000 + from the `MarketingBudget` field for the first Album + to the second Album. If the `MarketingBudget` is too + low, it will raise an exception. Before running this + sample, you will need to run the `update_data` sample + to populate the fields. + query_data_with_index + Inserts sample data into the given database. The + database and table must already exist and can be + created using `create_database`. + read_data_with_index + Inserts sample data into the given database. The + database and table must already exist and can be + created using `create_database`. + read_data_with_storing_index + Inserts sample data into the given database. The + database and table must already exist and can be + created using `create_database`. + + optional arguments: + -h, --help show this help message and exit + --database-name DATABASE_NAME + Your Cloud Spanner database name. + + + + +The client library +------------------------------------------------------------------------------- + +This sample uses the `Google Cloud Client Library for Python`_. +You can read the documentation for more details on API usage and use GitHub +to `browse the source`_ and `report issues`_. + +.. Google Cloud Client Library for Python: + https://googlecloudplatform.github.io/google-cloud-python/ +.. browse the source: + https://github.com/GoogleCloudPlatform/google-cloud-python +.. report issues: + https://github.com/GoogleCloudPlatform/google-cloud-python/issues + + +.. _Google Cloud SDK: https://cloud.google.com/sdk/ \ No newline at end of file diff --git a/spanner/cloud-client/README.rst.in b/spanner/cloud-client/README.rst.in new file mode 100644 index 000000000000..40ff29695be8 --- /dev/null +++ b/spanner/cloud-client/README.rst.in @@ -0,0 +1,22 @@ +# This file is used to generate README.rst + +product: + name: Google Cloud Spanner + short_name: Cloud Spanner + url: https://cloud.google.com/spanner/docs + description: > + `Google Cloud Spanner`_ is a highly scalable, transactional, managed, + NewSQL database service. Cloud Spanner solves the need for a + horizontally-scaling database with consistent global transactions and + SQL semantics. + +setup: +- auth +- install_deps + +samples: +- name: Snippets + file: snippets.py + show_help: true + +cloud_client_library: true diff --git a/spanner/cloud-client/quickstart.py b/spanner/cloud-client/quickstart.py new file mode 100644 index 000000000000..43cbf4d7ab8a --- /dev/null +++ b/spanner/cloud-client/quickstart.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python + +# Copyright 2016 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. + + +def run_quickstart(): + # [START spanner_quickstart] + # Imports the Google Cloud Client Library. + from google.cloud import spanner + + # Instantiate a client. + spanner_client = spanner.Client() + + # Your Cloud Spanner instance ID. + instance_id = 'my-instance-id' + + # Get a Cloud Spanner instance by ID. + instance = spanner_client.instance(instance_id) + + # Your Cloud Spanner database ID. + database_id = 'my-database-id' + + # Get a Cloud Spanner database by ID. + database = instance.database(database_id) + + # Execute a simple SQL statement. + results = database.execute_sql('SELECT 1') + + for row in results: + print(row) + # [END spanner_quickstart] + + +if __name__ == '__main__': + run_quickstart() diff --git a/spanner/cloud-client/quickstart_test.py b/spanner/cloud-client/quickstart_test.py new file mode 100644 index 000000000000..dafac78b952c --- /dev/null +++ b/spanner/cloud-client/quickstart_test.py @@ -0,0 +1,55 @@ +# Copyright 2016 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. + +from google.cloud import spanner +import google.cloud.exceptions +import google.cloud.spanner.client +import mock +import pytest + +import quickstart + + +@pytest.fixture +def patch_instance(cloud_config): + original_instance = google.cloud.spanner.client.Client.instance + + def new_instance(self, unused_instance_name): + return original_instance(self, cloud_config.spanner_instance) + + instance_patch = mock.patch( + 'google.cloud.spanner.client.Client.instance', + side_effect=new_instance, + autospec=True) + + with instance_patch: + yield + + +@pytest.fixture +def example_database(cloud_config): + spanner_client = spanner.Client() + instance = spanner_client.instance(cloud_config.spanner_instance) + database = instance.database('my-database-id') + + if not database.exists(): + database.create() + + yield + + +def test_quickstart(capsys, patch_instance, example_database): + quickstart.run_quickstart() + out, _ = capsys.readouterr() + assert '[1]' in out diff --git a/spanner/cloud-client/requirements.txt b/spanner/cloud-client/requirements.txt new file mode 100644 index 000000000000..8afaefa2c6f1 --- /dev/null +++ b/spanner/cloud-client/requirements.txt @@ -0,0 +1 @@ +google-cloud-spanner==0.23.0 diff --git a/spanner/cloud-client/snippets.py b/spanner/cloud-client/snippets.py new file mode 100644 index 000000000000..b80cf8dc798a --- /dev/null +++ b/spanner/cloud-client/snippets.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python + +# Copyright 2016 Google, Inc. +# +# 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. + +"""This application demonstrates how to do basic operations using Cloud +Spanner. + +For more information, see the README.rst under /spanner. +""" + +import argparse + +from google.cloud import spanner + + +def create_database(instance_id, database_id): + """Creates a database and tables for sample data.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + database = instance.database(database_id, ddl_statements=[ + """CREATE TABLE Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + SingerInfo BYTES(MAX) + ) PRIMARY KEY (SingerId)""", + """CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(MAX) + ) PRIMARY KEY (SingerId, AlbumId), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE""" + ]) + + operation = database.create() + + print('Waiting for operation to complete...') + operation.result() + + print('Created database {} on instance {}'.format( + database_id, instance_id)) + + +def insert_data(instance_id, database_id): + """Inserts sample data into the given database. + + The database and table must already exist and can be created using + `create_database`. + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + with database.batch() as batch: + batch.insert( + table='Singers', + columns=('SingerId', 'FirstName', 'LastName',), + values=[ + (1, u'Marc', u'Richards'), + (2, u'Catalina', u'Smith'), + (3, u'Alice', u'Trentor'), + (4, u'Lea', u'Martin'), + (5, u'David', u'Lomond')]) + + batch.insert( + table='Albums', + columns=('SingerId', 'AlbumId', 'AlbumTitle',), + values=[ + (1, 1, u'Go, Go, Go'), + (1, 2, u'Total Junk'), + (2, 1, u'Green'), + (2, 2, u'Forever Hold Your Peace'), + (2, 3, u'Terrified')]) + + print('Inserted data.') + + +def query_data(instance_id, database_id): + """Queries sample data from the database using SQL.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + results = database.execute_sql( + 'SELECT SingerId, AlbumId, AlbumTitle FROM Albums') + + for row in results: + print(u'SingerId: {}, AlbumId: {}, AlbumTitle: {}'.format(*row)) + + +def read_data(instance_id, database_id): + """Reads sample data from the database.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + keyset = spanner.KeySet(all_=True) + results = database.read( + table='Albums', + columns=('SingerId', 'AlbumId', 'AlbumTitle',), + keyset=keyset,) + + for row in results: + print(u'SingerId: {}, AlbumId: {}, AlbumTitle: {}'.format(*row)) + + +def query_data_with_new_column(instance_id, database_id): + """Queries sample data from the database using SQL. + + This sample uses the `MarketingBudget` column. You can add the column + by running the `add_column` sample or by running this DDL statement against + your database: + + ALTER TABLE Albums ADD COLUMN MarketingBudget INT64 + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + results = database.execute_sql( + 'SELECT SingerId, AlbumId, MarketingBudget FROM Albums') + + for row in results: + print(u'SingerId: {}, AlbumId: {}, MarketingBudget: {}'.format(*row)) + + +def add_index(instance_id, database_id): + """Adds a simple index to the example database.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + operation = database.update_ddl([ + 'CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)']) + + print('Waiting for operation to complete...') + operation.result() + + print('Added the AlbumsByAlbumTitle index.') + + +def query_data_with_index(instance_id, database_id): + """Queries sample data from the database using SQL and an index. + + The index must exist before running this sample. You can add the index + by running the `add_index` sample or by running this DDL statement against + your database: + + CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle) + + This sample also uses the `MarketingBudget` column. You can add the column + by running the `add_column` sample or by running this DDL statement against + your database: + + ALTER TABLE Albums ADD COLUMN MarketingBudget INT64 + + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + results = database.execute_sql( + "SELECT AlbumId, AlbumTitle, MarketingBudget " + "FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle} " + "WHERE AlbumTitle >= 'Ardvark' AND AlbumTitle < 'Goo'") + + for row in results: + print( + u'AlbumId: {}, AlbumTitle: {}, ' + 'MarketingBudget: {}'.format(*row)) + + +def read_data_with_index(instance_id, database_id): + """Reads sample data from the database using an index. + + The index must exist before running this sample. You can add the index + by running the `add_index` sample or by running this DDL statement against + your database: + + CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle) + + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + keyset = spanner.KeySet(all_=True) + results = database.read( + table='Albums', + columns=('AlbumId', 'AlbumTitle'), + keyset=keyset, + index='AlbumsByAlbumTitle') + + for row in results: + print('AlbumId: {}, AlbumTitle: {}'.format(*row)) + + +def add_storing_index(instance_id, database_id): + """Adds an storing index to the example database.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + operation = database.update_ddl([ + 'CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle)' + 'STORING (MarketingBudget)']) + + print('Waiting for operation to complete...') + operation.result() + + print('Added the AlbumsByAlbumTitle2 index.') + + +def read_data_with_storing_index(instance_id, database_id): + """Reads sample data from the database using an index with a storing + clause. + + The index must exist before running this sample. You can add the index + by running the `add_soring_index` sample or by running this DDL statement + against your database: + + CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) + STORING (MarketingBudget) + + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + keyset = spanner.KeySet(all_=True) + results = database.read( + table='Albums', + columns=('AlbumId', 'AlbumTitle', 'MarketingBudget'), + keyset=keyset, + index='AlbumsByAlbumTitle2') + + for row in results: + print( + u'AlbumId: {}, AlbumTitle: {}, ' + 'MarketingBudget: {}'.format(*row)) + + +def add_column(instance_id, database_id): + """Adds a new column to the Albums table in the example database.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + operation = database.update_ddl([ + 'ALTER TABLE Albums ADD COLUMN MarketingBudget INT64']) + + print('Waiting for operation to complete...') + operation.result() + + print('Added the MarketingBudget column.') + + +def update_data(instance_id, database_id): + """Updates sample data in the database. + + This updates the `MarketingBudget` column which must be created before + running this sample. You can add the column by running the `add_column` + sample or by running this DDL statement against your database: + + ALTER TABLE Albums ADD COLUMN MarketingBudget INT64 + + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + with database.batch() as batch: + batch.update( + table='Albums', + columns=( + 'SingerId', 'AlbumId', 'MarketingBudget'), + values=[ + (1, 1, 100000), + (2, 2, 500000)]) + + print('Updated data.') + + +def read_write_transaction(instance_id, database_id): + """Performs a read-write transaction to update two sample records in the + database. + + This will transfer 200,000 from the `MarketingBudget` field for the second + Album to the first Album. If the `MarketingBudget` is too low, it will + raise an exception. + + Before running this sample, you will need to run the `update_data` sample + to populate the fields. + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + def update_albums(transaction): + # Read the second album budget. + second_album_keyset = spanner.KeySet(keys=[(2, 2)]) + second_album_result = transaction.read( + table='Albums', columns=('MarketingBudget',), + keyset=second_album_keyset, limit=1) + second_album_row = list(second_album_result)[0] + second_album_budget = second_album_row[0] + + transfer_amount = 200000 + + if second_album_budget < transfer_amount: + # Raising an exception will automatically roll back the + # transaction. + raise ValueError( + 'The second album doesn\'t have enough funds to transfer') + + # Read the first album's budget. + first_album_keyset = spanner.KeySet(keys=[(1, 1)]) + first_album_result = transaction.read( + table='Albums', columns=('MarketingBudget',), + keyset=first_album_keyset, limit=1) + first_album_row = list(first_album_result)[0] + first_album_budget = first_album_row[0] + + # Update the budgets. + second_album_budget -= transfer_amount + first_album_budget += transfer_amount + print( + 'Setting first album\'s budget to {} and the second album\'s ' + 'budget to {}.'.format( + first_album_budget, second_album_budget)) + + # Update the rows. + transaction.update( + table='Albums', + columns=( + 'SingerId', 'AlbumId', 'MarketingBudget'), + values=[ + (1, 1, first_album_budget), + (2, 2, second_album_budget)]) + + database.run_in_transaction(update_albums) + + print('Transaction complete.') + + +def read_only_transaction(instance_id, database_id): + """Reads data inside of a read-only transaction. + + Within the read-only transaction, or "snapshot", the application sees + consistent view of the database at a particular timestamp. + """ + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + + with database.snapshot() as snapshot: + # Read using SQL. + results = snapshot.execute_sql( + 'SELECT SingerId, AlbumId, AlbumTitle FROM Albums') + + print('Results from first read:') + for row in results: + print(u'SingerId: {}, AlbumId: {}, AlbumTitle: {}'.format(*row)) + + # Perform another read using the `read` method. Even if the data + # is updated in-between the reads, the snapshot ensures that both + # return the same data. + keyset = spanner.KeySet(all_=True) + results = database.read( + table='Albums', + columns=('SingerId', 'AlbumId', 'AlbumTitle',), + keyset=keyset,) + + print('Results from second read:') + for row in results: + print(u'SingerId: {}, AlbumId: {}, AlbumTitle: {}'.format(*row)) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument( + 'instance_id', help='Your Cloud Spanner instance ID.') + parser.add_argument( + '--database-id', help='Your Cloud Spanner database ID.', + default='example_db') + + subparsers = parser.add_subparsers(dest='command') + subparsers.add_parser('create_database', help=create_database.__doc__) + subparsers.add_parser('insert_data', help=insert_data.__doc__) + subparsers.add_parser('query_data', help=query_data.__doc__) + subparsers.add_parser('read_data', help=read_data.__doc__) + subparsers.add_parser('add_column', help=add_column.__doc__) + subparsers.add_parser('update_data', help=update_data.__doc__) + subparsers.add_parser( + 'query_data_with_new_column', help=query_data_with_new_column.__doc__) + subparsers.add_parser( + 'read_write_transaction', help=read_write_transaction.__doc__) + subparsers.add_parser( + 'read_only_transaction', help=read_only_transaction.__doc__) + subparsers.add_parser('add_index', help=add_index.__doc__) + subparsers.add_parser('query_data_with_index', help=insert_data.__doc__) + subparsers.add_parser('read_data_with_index', help=insert_data.__doc__) + subparsers.add_parser('add_storing_index', help=add_storing_index.__doc__) + subparsers.add_parser( + 'read_data_with_storing_index', help=insert_data.__doc__) + + args = parser.parse_args() + + if args.command == 'create_database': + create_database(args.instance_id, args.database_id) + elif args.command == 'insert_data': + insert_data(args.instance_id, args.database_id) + elif args.command == 'query_data': + query_data(args.instance_id, args.database_id) + elif args.command == 'read_data': + read_data(args.instance_id, args.database_id) + elif args.command == 'add_column': + add_column(args.instance_id, args.database_id) + elif args.command == 'update_data': + update_data(args.instance_id, args.database_id) + elif args.command == 'query_data_with_new_column': + query_data_with_new_column(args.instance_id, args.database_id) + elif args.command == 'read_write_transaction': + read_write_transaction(args.instance_id, args.database_id) + elif args.command == 'read_only_transaction': + read_only_transaction(args.instance_id, args.database_id) + elif args.command == 'add_index': + add_index(args.instance_id, args.database_id) + elif args.command == 'query_data_with_index': + query_data_with_index(args.instance_id, args.database_id) + elif args.command == 'read_data_with_index': + read_data_with_index(args.instance_id, args.database_id) + elif args.command == 'add_storing_index': + add_storing_index(args.instance_id, args.database_id) + elif args.command == 'read_data_with_storing_index': + read_data_with_storing_index(args.instance_id, args.database_id) diff --git a/spanner/cloud-client/snippets_test.py b/spanner/cloud-client/snippets_test.py new file mode 100644 index 000000000000..36915332bc02 --- /dev/null +++ b/spanner/cloud-client/snippets_test.py @@ -0,0 +1,180 @@ +# Copyright 2016 Google, Inc. +# +# 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 random +import string + +from gcp.testing import eventually_consistent +from google.cloud import spanner +import pytest + +import snippets + + +@pytest.fixture(scope='module') +def spanner_instance(cloud_config): + spanner_client = spanner.Client() + return spanner_client.instance(cloud_config.spanner_instance) + + +def unique_database_id(): + return 'test-db-{}'.format(''.join(random.choice( + string.ascii_lowercase + string.digits) for _ in range(5))) + + +def test_create_database(cloud_config, spanner_instance): + database_id = unique_database_id() + print(cloud_config.spanner_instance, database_id) + snippets.create_database( + cloud_config.spanner_instance, database_id) + + database = spanner_instance.database(database_id) + database.reload() # Will only succeed if the database exists. + database.drop() + + +@pytest.fixture(scope='module') +def temporary_database(cloud_config, spanner_instance): + database_id = unique_database_id() + snippets.create_database(cloud_config.spanner_instance, database_id) + snippets.insert_data( + cloud_config.spanner_instance, database_id) + database = spanner_instance.database(database_id) + database.reload() + yield database + database.drop() + + +def test_query_data(cloud_config, temporary_database, capsys): + snippets.query_data( + cloud_config.spanner_instance, temporary_database.database_id) + + out, _ = capsys.readouterr() + + assert 'Total Junk' in out + + +def test_read_data(cloud_config, temporary_database, capsys): + snippets.read_data( + cloud_config.spanner_instance, temporary_database.database_id) + + out, _ = capsys.readouterr() + + assert 'Total Junk' in out + + +@pytest.fixture(scope='module') +def temporary_database_with_column(cloud_config, temporary_database): + snippets.add_column( + cloud_config.spanner_instance, temporary_database.database_id) + yield temporary_database + + +def test_update_data(cloud_config, temporary_database_with_column): + snippets.update_data( + cloud_config.spanner_instance, + temporary_database_with_column.database_id) + + +def test_query_data_with_new_column( + cloud_config, temporary_database_with_column, capsys): + snippets.query_data_with_new_column( + cloud_config.spanner_instance, + temporary_database_with_column.database_id) + + out, _ = capsys.readouterr() + assert 'MarketingBudget' in out + + +@pytest.fixture(scope='module') +def temporary_database_with_indexes( + cloud_config, temporary_database_with_column): + snippets.add_index( + cloud_config.spanner_instance, + temporary_database_with_column.database_id) + snippets.add_storing_index( + cloud_config.spanner_instance, + temporary_database_with_column.database_id) + + yield temporary_database_with_column + + +@pytest.mark.slow +def test_query_data_with_index( + cloud_config, temporary_database_with_indexes, capsys): + @eventually_consistent.call + def _(): + snippets.query_data_with_index( + cloud_config.spanner_instance, + temporary_database_with_indexes.database_id) + + out, _ = capsys.readouterr() + assert 'Go, Go, Go' in out + + +@pytest.mark.slow +def test_read_data_with_index( + cloud_config, temporary_database_with_indexes, capsys): + @eventually_consistent.call + def _(): + snippets.read_data_with_index( + cloud_config.spanner_instance, + temporary_database_with_indexes.database_id) + + out, _ = capsys.readouterr() + assert 'Go, Go, Go' in out + + +@pytest.mark.slow +def test_read_data_with_storing_index( + cloud_config, temporary_database_with_indexes, capsys): + @eventually_consistent.call + def _(): + snippets.read_data_with_storing_index( + cloud_config.spanner_instance, + temporary_database_with_indexes.database_id) + + out, _ = capsys.readouterr() + assert 'Go, Go, Go' in out + + +@pytest.mark.slow +def test_read_write_transaction( + cloud_config, temporary_database_with_column, capsys): + @eventually_consistent.call + def _(): + snippets.update_data( + cloud_config.spanner_instance, + temporary_database_with_column.database_id) + snippets.read_write_transaction( + cloud_config.spanner_instance, + temporary_database_with_column.database_id) + + out, _ = capsys.readouterr() + + assert '300000' in out + + +@pytest.mark.slow +def test_read_only_transaction( + cloud_config, temporary_database, capsys): + @eventually_consistent.call + def _(): + snippets.read_only_transaction( + cloud_config.spanner_instance, + temporary_database.database_id) + + out, _ = capsys.readouterr() + + assert 'Forever Hold Your Peace' in out diff --git a/testing/secrets.tar.enc b/testing/secrets.tar.enc index 6ef05dd0ee46..fce758f2906d 100644 Binary files a/testing/secrets.tar.enc and b/testing/secrets.tar.enc differ diff --git a/testing/test-env.tmpl.sh b/testing/test-env.tmpl.sh index 0734f19fa27c..43dcb2e3f2bc 100644 --- a/testing/test-env.tmpl.sh +++ b/testing/test-env.tmpl.sh @@ -4,6 +4,7 @@ export CLOUD_STORAGE_BUCKET=$GCLOUD_PROJECT export API_KEY= export BIGTABLE_CLUSTER=bigtable-test export BIGTABLE_ZONE=us-central1-c +export SPANNER_INSTANCE= # Environment variables for App Engine Flexible system tests. export GA_TRACKING_ID=