From f9d9c7c2b6231b7a69236d9057abd8a1b413e0a3 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 24 Apr 2026 11:42:13 -0700 Subject: [PATCH 1/5] simplified id token type annotations --- packages/google-auth/google/oauth2/id_token.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/google-auth/google/oauth2/id_token.py b/packages/google-auth/google/oauth2/id_token.py index d21be1a06c84..3587a76a28b9 100644 --- a/packages/google-auth/google/oauth2/id_token.py +++ b/packages/google-auth/google/oauth2/id_token.py @@ -59,7 +59,7 @@ import http.client as http_client import json import os -from typing import Any, Mapping, Union +from typing import Union from google.auth import environment_vars from google.auth import exceptions @@ -113,7 +113,7 @@ def verify_token( audience: Union[str, list[str], None] = None, certs_url: str = _GOOGLE_OAUTH2_CERTS_URL, clock_skew_in_seconds: int = 0, -) -> Mapping[str, Any]: +) -> dict: # Note: type annotation simplified to prevent issues in google3 """Verifies an ID token and returns the decoded token. Args: @@ -130,7 +130,7 @@ def verify_token( validation. Returns: - Mapping[str, Any]: The decoded token. + dict[str, Any]: The decoded token. """ certs = _fetch_certs(request, certs_url) @@ -172,7 +172,7 @@ def verify_oauth2_token(id_token, request, audience=None, clock_skew_in_seconds= validation. Returns: - Mapping[str, Any]: The decoded token. + dict[str, Any]: The decoded token. Raises: exceptions.GoogleAuthError: If the issuer is invalid. @@ -210,7 +210,7 @@ def verify_firebase_token(id_token, request, audience=None, clock_skew_in_second validation. Returns: - Mapping[str, Any]: The decoded token. + dict[str, Any]: The decoded token. """ return verify_token( id_token, From 74331dd7d1d22914b064d10586028560e18e2ff6 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 22 May 2026 13:18:21 -0700 Subject: [PATCH 2/5] adding back samples --- .../samples/AUTHORING_GUIDE.md | 1 + .../samples/CONTRIBUTING.md | 1 + .../google-cloud-bigtable/samples/README.md | 24 + .../google-cloud-bigtable/samples/__init__.py | 0 .../samples/beam/__init__.py | 0 .../samples/beam/hello_world_write.py | 66 + .../samples/beam/hello_world_write_test.py | 48 + .../samples/beam/noxfile.py | 290 ++ .../samples/beam/noxfile_config.py | 45 + .../samples/beam/requirements-test.txt | 1 + .../samples/beam/requirements.txt | 5 + .../samples/hello/README.md | 52 + .../samples/hello/__init__.py | 0 .../samples/hello/async_main.py | 146 + .../samples/hello/async_main_test.py | 36 + .../samples/hello/main.py | 149 + .../samples/hello/main_test.py | 35 + .../samples/hello/noxfile.py | 292 ++ .../samples/hello/requirements-test.txt | 1 + .../samples/hello/requirements.txt | 2 + .../samples/hello_happybase/README.md | 52 + .../samples/hello_happybase/__init__.py | 0 .../samples/hello_happybase/main.py | 117 + .../samples/hello_happybase/main_test.py | 44 + .../samples/hello_happybase/noxfile.py | 292 ++ .../hello_happybase/requirements-test.txt | 1 + .../samples/hello_happybase/requirements.txt | 2 + .../samples/instanceadmin/README.md | 52 + .../samples/instanceadmin/instanceadmin.py | 232 ++ .../samples/instanceadmin/noxfile.py | 292 ++ .../instanceadmin/requirements-test.txt | 1 + .../samples/instanceadmin/requirements.txt | 2 + .../instanceadmin/test_instanceadmin.py | 180 + .../samples/metricscaler/Dockerfile | 24 + .../samples/metricscaler/README.md | 52 + .../samples/metricscaler/metricscaler.py | 235 ++ .../samples/metricscaler/metricscaler_test.py | 231 ++ .../samples/metricscaler/noxfile.py | 292 ++ .../samples/metricscaler/noxfile_config.py | 39 + .../metricscaler/requirements-test.txt | 3 + .../samples/metricscaler/requirements.txt | 2 + .../samples/quickstart/README.md | 52 + .../samples/quickstart/__init__.py | 0 .../samples/quickstart/diff | 3534 +++++++++++++++++ .../samples/quickstart/main.py | 57 + .../samples/quickstart/main_async.py | 61 + .../samples/quickstart/main_async_test.py | 49 + .../samples/quickstart/main_test.py | 46 + .../samples/quickstart/noxfile.py | 292 ++ .../samples/quickstart/requirements-test.txt | 2 + .../samples/quickstart/requirements.txt | 1 + .../samples/quickstart_happybase/README.md | 52 + .../samples/quickstart_happybase/__init__.py | 0 .../samples/quickstart_happybase/main.py | 61 + .../samples/quickstart_happybase/main_test.py | 44 + .../samples/quickstart_happybase/noxfile.py | 292 ++ .../requirements-test.txt | 1 + .../quickstart_happybase/requirements.txt | 2 + .../samples/snippets/README.md | 33 + .../samples/snippets/__init__.py | 0 .../samples/snippets/data_client/__init__.py | 0 .../data_client/data_client_snippets_async.py | 318 ++ .../data_client_snippets_async_test.py | 117 + .../samples/snippets/data_client/noxfile.py | 292 ++ .../data_client/requirements-test.txt | 2 + .../snippets/data_client/requirements.txt | 1 + .../samples/snippets/deletes/__init__.py | 0 .../snippets/deletes/deletes_async_test.py | 274 ++ .../snippets/deletes/deletes_snippets.py | 131 + .../deletes/deletes_snippets_async.py | 117 + .../samples/snippets/deletes/deletes_test.py | 133 + .../samples/snippets/deletes/noxfile.py | 292 ++ .../snippets/deletes/requirements-test.txt | 2 + .../samples/snippets/deletes/requirements.txt | 1 + .../samples/snippets/filters/__init__.py | 0 .../snippets/filters/filter_snippets.py | 356 ++ .../snippets/filters/filter_snippets_async.py | 389 ++ .../filters/filter_snippets_async_test.py | 447 +++ .../samples/snippets/filters/filters_test.py | 236 ++ .../samples/snippets/filters/noxfile.py | 292 ++ .../snippets/filters/requirements-test.txt | 2 + .../samples/snippets/filters/requirements.txt | 1 + .../snippets/filters/snapshots/__init__.py | 0 .../filters/snapshots/snap_filters_test.py | 480 +++ .../samples/snippets/reads/__init__.py | 0 .../samples/snippets/reads/noxfile.py | 292 ++ .../samples/snippets/reads/read_snippets.py | 168 + .../samples/snippets/reads/reads_test.py | 117 + .../snippets/reads/requirements-test.txt | 1 + .../samples/snippets/reads/requirements.txt | 1 + .../snippets/reads/snapshots/__init__.py | 0 .../reads/snapshots/snap_reads_test.py | 141 + .../samples/snippets/writes/__init__.py | 0 .../samples/snippets/writes/noxfile.py | 292 ++ .../snippets/writes/requirements-test.txt | 2 + .../samples/snippets/writes/requirements.txt | 1 + .../samples/snippets/writes/write_batch.py | 46 + .../snippets/writes/write_conditionally.py | 46 + .../snippets/writes/write_increment.py | 36 + .../samples/snippets/writes/write_simple.py | 42 + .../samples/snippets/writes/writes_test.py | 73 + .../samples/tableadmin/README.md | 52 + .../samples/tableadmin/__init__.py | 0 .../samples/tableadmin/noxfile.py | 292 ++ .../samples/tableadmin/requirements-test.txt | 2 + .../samples/tableadmin/requirements.txt | 1 + .../samples/tableadmin/tableadmin.py | 265 ++ .../samples/tableadmin/tableadmin_test.py | 61 + .../samples/testdata/README.md | 5 + .../samples/testdata/descriptors.pb | Bin 0 -> 182 bytes .../samples/testdata/singer.proto | 15 + .../samples/testdata/singer_pb2.py | 27 + .../google-cloud-bigtable/samples/utils.py | 99 + 113 files changed, 13878 insertions(+) create mode 100644 packages/google-cloud-bigtable/samples/AUTHORING_GUIDE.md create mode 100644 packages/google-cloud-bigtable/samples/CONTRIBUTING.md create mode 100644 packages/google-cloud-bigtable/samples/README.md create mode 100644 packages/google-cloud-bigtable/samples/__init__.py create mode 100644 packages/google-cloud-bigtable/samples/beam/__init__.py create mode 100644 packages/google-cloud-bigtable/samples/beam/hello_world_write.py create mode 100644 packages/google-cloud-bigtable/samples/beam/hello_world_write_test.py create mode 100644 packages/google-cloud-bigtable/samples/beam/noxfile.py create mode 100644 packages/google-cloud-bigtable/samples/beam/noxfile_config.py create mode 100644 packages/google-cloud-bigtable/samples/beam/requirements-test.txt create mode 100644 packages/google-cloud-bigtable/samples/beam/requirements.txt create mode 100644 packages/google-cloud-bigtable/samples/hello/README.md create mode 100644 packages/google-cloud-bigtable/samples/hello/__init__.py create mode 100644 packages/google-cloud-bigtable/samples/hello/async_main.py create mode 100644 packages/google-cloud-bigtable/samples/hello/async_main_test.py create mode 100644 packages/google-cloud-bigtable/samples/hello/main.py create mode 100644 packages/google-cloud-bigtable/samples/hello/main_test.py create mode 100644 packages/google-cloud-bigtable/samples/hello/noxfile.py create mode 100644 packages/google-cloud-bigtable/samples/hello/requirements-test.txt create mode 100644 packages/google-cloud-bigtable/samples/hello/requirements.txt create mode 100644 packages/google-cloud-bigtable/samples/hello_happybase/README.md create mode 100644 packages/google-cloud-bigtable/samples/hello_happybase/__init__.py create mode 100644 packages/google-cloud-bigtable/samples/hello_happybase/main.py create mode 100644 packages/google-cloud-bigtable/samples/hello_happybase/main_test.py create mode 100644 packages/google-cloud-bigtable/samples/hello_happybase/noxfile.py create mode 100644 packages/google-cloud-bigtable/samples/hello_happybase/requirements-test.txt create mode 100644 packages/google-cloud-bigtable/samples/hello_happybase/requirements.txt create mode 100644 packages/google-cloud-bigtable/samples/instanceadmin/README.md create mode 100644 packages/google-cloud-bigtable/samples/instanceadmin/instanceadmin.py create mode 100644 packages/google-cloud-bigtable/samples/instanceadmin/noxfile.py create mode 100644 packages/google-cloud-bigtable/samples/instanceadmin/requirements-test.txt create mode 100644 packages/google-cloud-bigtable/samples/instanceadmin/requirements.txt create mode 100644 packages/google-cloud-bigtable/samples/instanceadmin/test_instanceadmin.py create mode 100644 packages/google-cloud-bigtable/samples/metricscaler/Dockerfile create mode 100644 packages/google-cloud-bigtable/samples/metricscaler/README.md create mode 100644 packages/google-cloud-bigtable/samples/metricscaler/metricscaler.py create mode 100644 packages/google-cloud-bigtable/samples/metricscaler/metricscaler_test.py create mode 100644 packages/google-cloud-bigtable/samples/metricscaler/noxfile.py create mode 100644 packages/google-cloud-bigtable/samples/metricscaler/noxfile_config.py create mode 100644 packages/google-cloud-bigtable/samples/metricscaler/requirements-test.txt create mode 100644 packages/google-cloud-bigtable/samples/metricscaler/requirements.txt create mode 100644 packages/google-cloud-bigtable/samples/quickstart/README.md create mode 100644 packages/google-cloud-bigtable/samples/quickstart/__init__.py create mode 100644 packages/google-cloud-bigtable/samples/quickstart/diff create mode 100644 packages/google-cloud-bigtable/samples/quickstart/main.py create mode 100644 packages/google-cloud-bigtable/samples/quickstart/main_async.py create mode 100644 packages/google-cloud-bigtable/samples/quickstart/main_async_test.py create mode 100644 packages/google-cloud-bigtable/samples/quickstart/main_test.py create mode 100644 packages/google-cloud-bigtable/samples/quickstart/noxfile.py create mode 100644 packages/google-cloud-bigtable/samples/quickstart/requirements-test.txt create mode 100644 packages/google-cloud-bigtable/samples/quickstart/requirements.txt create mode 100644 packages/google-cloud-bigtable/samples/quickstart_happybase/README.md create mode 100644 packages/google-cloud-bigtable/samples/quickstart_happybase/__init__.py create mode 100644 packages/google-cloud-bigtable/samples/quickstart_happybase/main.py create mode 100644 packages/google-cloud-bigtable/samples/quickstart_happybase/main_test.py create mode 100644 packages/google-cloud-bigtable/samples/quickstart_happybase/noxfile.py create mode 100644 packages/google-cloud-bigtable/samples/quickstart_happybase/requirements-test.txt create mode 100644 packages/google-cloud-bigtable/samples/quickstart_happybase/requirements.txt create mode 100644 packages/google-cloud-bigtable/samples/snippets/README.md create mode 100644 packages/google-cloud-bigtable/samples/snippets/__init__.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/data_client/__init__.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async_test.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/data_client/noxfile.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/data_client/requirements-test.txt create mode 100644 packages/google-cloud-bigtable/samples/snippets/data_client/requirements.txt create mode 100644 packages/google-cloud-bigtable/samples/snippets/deletes/__init__.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/deletes/deletes_async_test.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets_async.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/deletes/deletes_test.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/deletes/noxfile.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/deletes/requirements-test.txt create mode 100644 packages/google-cloud-bigtable/samples/snippets/deletes/requirements.txt create mode 100644 packages/google-cloud-bigtable/samples/snippets/filters/__init__.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets_async.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets_async_test.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/filters/filters_test.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/filters/noxfile.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/filters/requirements-test.txt create mode 100644 packages/google-cloud-bigtable/samples/snippets/filters/requirements.txt create mode 100644 packages/google-cloud-bigtable/samples/snippets/filters/snapshots/__init__.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/filters/snapshots/snap_filters_test.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/reads/__init__.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/reads/noxfile.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/reads/read_snippets.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/reads/reads_test.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/reads/requirements-test.txt create mode 100644 packages/google-cloud-bigtable/samples/snippets/reads/requirements.txt create mode 100644 packages/google-cloud-bigtable/samples/snippets/reads/snapshots/__init__.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/reads/snapshots/snap_reads_test.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/writes/__init__.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/writes/noxfile.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/writes/requirements-test.txt create mode 100644 packages/google-cloud-bigtable/samples/snippets/writes/requirements.txt create mode 100644 packages/google-cloud-bigtable/samples/snippets/writes/write_batch.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/writes/write_conditionally.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/writes/write_increment.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/writes/write_simple.py create mode 100644 packages/google-cloud-bigtable/samples/snippets/writes/writes_test.py create mode 100644 packages/google-cloud-bigtable/samples/tableadmin/README.md create mode 100644 packages/google-cloud-bigtable/samples/tableadmin/__init__.py create mode 100644 packages/google-cloud-bigtable/samples/tableadmin/noxfile.py create mode 100644 packages/google-cloud-bigtable/samples/tableadmin/requirements-test.txt create mode 100644 packages/google-cloud-bigtable/samples/tableadmin/requirements.txt create mode 100644 packages/google-cloud-bigtable/samples/tableadmin/tableadmin.py create mode 100755 packages/google-cloud-bigtable/samples/tableadmin/tableadmin_test.py create mode 100644 packages/google-cloud-bigtable/samples/testdata/README.md create mode 100644 packages/google-cloud-bigtable/samples/testdata/descriptors.pb create mode 100644 packages/google-cloud-bigtable/samples/testdata/singer.proto create mode 100644 packages/google-cloud-bigtable/samples/testdata/singer_pb2.py create mode 100644 packages/google-cloud-bigtable/samples/utils.py diff --git a/packages/google-cloud-bigtable/samples/AUTHORING_GUIDE.md b/packages/google-cloud-bigtable/samples/AUTHORING_GUIDE.md new file mode 100644 index 000000000000..8249522ffc2d --- /dev/null +++ b/packages/google-cloud-bigtable/samples/AUTHORING_GUIDE.md @@ -0,0 +1 @@ +See https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/AUTHORING_GUIDE.md \ No newline at end of file diff --git a/packages/google-cloud-bigtable/samples/CONTRIBUTING.md b/packages/google-cloud-bigtable/samples/CONTRIBUTING.md new file mode 100644 index 000000000000..f5fe2e6baf13 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/CONTRIBUTING.md @@ -0,0 +1 @@ +See https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/CONTRIBUTING.md \ No newline at end of file diff --git a/packages/google-cloud-bigtable/samples/README.md b/packages/google-cloud-bigtable/samples/README.md new file mode 100644 index 000000000000..1301c6fb1f60 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/README.md @@ -0,0 +1,24 @@ +[//]: # "This README.md file is auto-generated, all changes to this file will be lost." +[//]: # "To regenerate it, use `python -m synthtool`." + +## Python Samples for Cloud Bigtable + +This directory contains samples for Cloud Bigtable, which may be used as a refererence for how to use this product. + +## Additional Information + +You can read the documentation for more details on API usage and use GitHub +to browse the source and [report issues][issues]. + +### Contributing +View the [contributing guidelines][contrib_guide], the [Python style guide][py_style] for more information. + +[authentication]: https://cloud.google.com/docs/authentication/getting-started +[enable_billing]:https://cloud.google.com/apis/docs/getting-started#enabling_billing +[client_library_python]: https://googlecloudplatform.github.io/google-cloud-python/ +[issues]: https://github.com/GoogleCloudPlatform/google-cloud-python/issues +[contrib_guide]: https://github.com/googleapis/google-cloud-python/blob/main/CONTRIBUTING.rst +[py_style]: http://google.github.io/styleguide/pyguide.html +[cloud_sdk]: https://cloud.google.com/sdk/docs +[gcloud_shell]: https://cloud.google.com/shell/docs +[gcloud_shell]: https://cloud.google.com/shell/docs diff --git a/packages/google-cloud-bigtable/samples/__init__.py b/packages/google-cloud-bigtable/samples/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/samples/beam/__init__.py b/packages/google-cloud-bigtable/samples/beam/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/samples/beam/hello_world_write.py b/packages/google-cloud-bigtable/samples/beam/hello_world_write.py new file mode 100644 index 000000000000..89f541d0d190 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/beam/hello_world_write.py @@ -0,0 +1,66 @@ +# Copyright 2020 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 datetime + +import apache_beam as beam +from apache_beam.io.gcp.bigtableio import WriteToBigTable +from apache_beam.options.pipeline_options import PipelineOptions +from google.cloud.bigtable import row + + +class BigtableOptions(PipelineOptions): + @classmethod + def _add_argparse_args(cls, parser): + parser.add_argument( + "--bigtable-project", + help="The Bigtable project ID, this can be different than your " + "Dataflow project", + default="bigtable-project", + ) + parser.add_argument( + "--bigtable-instance", + help="The Bigtable instance ID", + default="bigtable-instance", + ) + parser.add_argument( + "--bigtable-table", + help="The Bigtable table ID in the instance.", + default="bigtable-table", + ) + + +class CreateRowFn(beam.DoFn): + def process(self, key): + direct_row = row.DirectRow(row_key=key) + direct_row.set_cell( + "stats_summary", b"os_build", b"android", datetime.datetime.now() + ) + return [direct_row] + + +def run(argv=None): + """Build and run the pipeline.""" + options = BigtableOptions(argv) + with beam.Pipeline(options=options) as p: + p | beam.Create( + ["phone#4c410523#20190501", "phone#4c410523#20190502"] + ) | beam.ParDo(CreateRowFn()) | WriteToBigTable( + project_id=options.bigtable_project, + instance_id=options.bigtable_instance, + table_id=options.bigtable_table, + ) + + +if __name__ == "__main__": + run() diff --git a/packages/google-cloud-bigtable/samples/beam/hello_world_write_test.py b/packages/google-cloud-bigtable/samples/beam/hello_world_write_test.py new file mode 100644 index 000000000000..ba0e980964bb --- /dev/null +++ b/packages/google-cloud-bigtable/samples/beam/hello_world_write_test.py @@ -0,0 +1,48 @@ +# Copyright 2020 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 os +import uuid + +import pytest + +from . import hello_world_write +from ..utils import create_table_cm + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] +TABLE_ID = f"mobile-time-series-beam-{str(uuid.uuid4())[:16]}" + + +@pytest.fixture(scope="module", autouse=True) +def table(): + with create_table_cm( + PROJECT, BIGTABLE_INSTANCE, TABLE_ID, {"stats_summary": None} + ) as table: + yield table + + +def test_hello_world_write(table): + hello_world_write.run( + [ + "--bigtable-project=%s" % PROJECT, + "--bigtable-instance=%s" % BIGTABLE_INSTANCE, + "--bigtable-table=%s" % TABLE_ID, + ] + ) + + rows = table.read_rows() + count = 0 + for _ in rows: + count += 1 + assert count == 2 diff --git a/packages/google-cloud-bigtable/samples/beam/noxfile.py b/packages/google-cloud-bigtable/samples/beam/noxfile.py new file mode 100644 index 000000000000..d0b343a9167c --- /dev/null +++ b/packages/google-cloud-bigtable/samples/beam/noxfile.py @@ -0,0 +1,290 @@ +# Copyright 2019 Google LLC +# +# 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 __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, Optional + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +# todo(kolea2): temporary workaround to install pinned dep version +INSTALL_LIBRARY_FROM_SOURCE = False + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8") + else: + session.install("flake8", "flake8-annotations") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# format = isort + black +# + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list.extend(glob.glob("**/tests", recursive=True)) + + if len(test_list) == 0: + print("No tests found, skipping directory.") + return + + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + concurrent_args = [] + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + with open("requirements.txt") as rfile: + packages = rfile.read() + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install( + "-r", "requirements-test.txt", "-c", "constraints-test.txt" + ) + else: + session.install("-r", "requirements-test.txt") + with open("requirements-test.txt") as rtfile: + packages += rtfile.read() + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + if "pytest-parallel" in packages: + concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + elif "pytest-xdist" in packages: + concurrent_args.extend(['-n', 'auto']) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs + concurrent_args), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/packages/google-cloud-bigtable/samples/beam/noxfile_config.py b/packages/google-cloud-bigtable/samples/beam/noxfile_config.py new file mode 100644 index 000000000000..66d7bc5aca17 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/beam/noxfile_config.py @@ -0,0 +1,45 @@ +# Copyright 2021 Google LLC +# +# 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. + +# Default TEST_CONFIG_OVERRIDE for python repos. + +# You can copy this file into your directory, then it will be imported from +# the noxfile.py. + +# The source of truth: +# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py + +TEST_CONFIG_OVERRIDE = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [ + "3.7", # Beam no longer supports Python 3.7 for new releases + "3.12", # Beam not yet supported for Python 3.12 + ], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} diff --git a/packages/google-cloud-bigtable/samples/beam/requirements-test.txt b/packages/google-cloud-bigtable/samples/beam/requirements-test.txt new file mode 100644 index 000000000000..e079f8a6038d --- /dev/null +++ b/packages/google-cloud-bigtable/samples/beam/requirements-test.txt @@ -0,0 +1 @@ +pytest diff --git a/packages/google-cloud-bigtable/samples/beam/requirements.txt b/packages/google-cloud-bigtable/samples/beam/requirements.txt new file mode 100644 index 000000000000..e709a03cb849 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/beam/requirements.txt @@ -0,0 +1,5 @@ +apache-beam===2.60.0; python_version == '3.8' +apache-beam===2.69.0; python_version == '3.9' +apache-beam==2.71.0; python_version >= '3.10' +google-cloud-bigtable==2.35.0 +google-cloud-core==2.5.0 diff --git a/packages/google-cloud-bigtable/samples/hello/README.md b/packages/google-cloud-bigtable/samples/hello/README.md new file mode 100644 index 000000000000..b3779fb43b27 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/hello/README.md @@ -0,0 +1,52 @@ +[//]: # "This README.md file is auto-generated, all changes to this file will be lost." +[//]: # "To regenerate it, use `python -m synthtool`." + +## Python Samples for Cloud Bigtable + +This directory contains samples for Cloud Bigtable, which may be used as a refererence for how to use this product. +Samples, quickstarts, and other documentation are available at cloud.google.com. + + +### Hello World in Cloud Bigtable + +Demonstrates how to connect to Cloud Bigtable and run some basic operations. More information available at: https://cloud.google.com/bigtable/docs/samples-python-hello + + +Open in Cloud Shell + + +To run this sample: + +1. If this is your first time working with GCP products, you will need to set up [the Cloud SDK][cloud_sdk] or utilize [Google Cloud Shell][gcloud_shell]. This sample may [require authentication][authentication] and you will need to [enable billing][enable_billing]. + +1. Make a fork of this repo and clone the branch locally, then navigate to the sample directory you want to use. + +1. Install the dependencies needed to run the samples. + + pip install -r requirements.txt + +1. Run the sample using + + python main.py + + + +
usage: main.py [-h] [--table TABLE] project_id instance_id
Demonstrates how to connect to Cloud Bigtable and run some basic operations.
Prerequisites: - Create a Cloud Bigtable cluster.
https://cloud.google.com/bigtable/docs/creating-cluster - Set your Google
Application Default Credentials.
https://developers.google.com/identity/protocols/application-default-
credentials


positional arguments:
  project_id     Your Cloud Platform project ID.
  instance_id    ID of the Cloud Bigtable instance to connect to.


optional arguments:
  -h, --help     show this help message and exit
  --table TABLE  Table to create and destroy. (default: Hello-Bigtable)
+ +## Additional Information + +You can read the documentation for more details on API usage and use GitHub +to browse the source and [report issues][issues]. + +### Contributing +View the [contributing guidelines][contrib_guide], the [Python style guide][py_style] for more information. + +[authentication]: https://cloud.google.com/docs/authentication/getting-started +[enable_billing]:https://cloud.google.com/apis/docs/getting-started#enabling_billing +[client_library_python]: https://googlecloudplatform.github.io/google-cloud-python/ +[issues]: https://github.com/GoogleCloudPlatform/google-cloud-python/issues +[contrib_guide]: https://github.com/googleapis/google-cloud-python/blob/main/CONTRIBUTING.rst +[py_style]: http://google.github.io/styleguide/pyguide.html +[cloud_sdk]: https://cloud.google.com/sdk/docs +[gcloud_shell]: https://cloud.google.com/shell/docs +[gcloud_shell]: https://cloud.google.com/shell/docs diff --git a/packages/google-cloud-bigtable/samples/hello/__init__.py b/packages/google-cloud-bigtable/samples/hello/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/samples/hello/async_main.py b/packages/google-cloud-bigtable/samples/hello/async_main.py new file mode 100644 index 000000000000..e134e28d0cc3 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/hello/async_main.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python + +# Copyright 2024 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. + +"""Demonstrates how to connect to Cloud Bigtable and run some basic operations with the async APIs + +Prerequisites: + +- Create a Cloud Bigtable instance. + https://cloud.google.com/bigtable/docs/creating-instance +- Set your Google Application Default Credentials. + https://developers.google.com/identity/protocols/application-default-credentials +""" + +import argparse +import asyncio +from ..utils import wait_for_table + +# [START bigtable_async_hw_imports] +from google.cloud import bigtable +from google.cloud.bigtable.data import row_filters +# [END bigtable_async_hw_imports] + +# use to ignore warnings +row_filters + + +async def main(project_id, instance_id, table_id): + # [START bigtable_async_hw_connect] + client = bigtable.data.BigtableDataClientAsync(project=project_id) + table = client.get_table(instance_id, table_id) + # [END bigtable_async_hw_connect] + + # [START bigtable_async_hw_create_table] + from google.cloud.bigtable import column_family + + # the async client only supports the data API. Table creation as an admin operation + # use admin client to create the table + print("Creating the {} table.".format(table_id)) + admin_client = bigtable.Client(project=project_id, admin=True) + admin_instance = admin_client.instance(instance_id) + admin_table = admin_instance.table(table_id) + + print("Creating column family cf1 with Max Version GC rule...") + # Create a column family with GC policy : most recent N versions + # Define the GC policy to retain only the most recent 2 versions + max_versions_rule = column_family.MaxVersionsGCRule(2) + column_family_id = b"cf1" + column_families = {column_family_id: max_versions_rule} + if not admin_table.exists(): + admin_table.create(column_families=column_families) + else: + print("Table {} already exists.".format(table_id)) + # [END bigtable_async_hw_create_table] + + try: + # let table creation complete + wait_for_table(admin_table) + # [START bigtable_async_hw_write_rows] + print("Writing some greetings to the table.") + greetings = [b"Hello World!", b"Hello Cloud Bigtable!", b"Hello Python!"] + mutations = [] + column = b"greeting" + for i, value in enumerate(greetings): + # Note: This example uses sequential numeric IDs for simplicity, + # but this can result in poor performance in a production + # application. Since rows are stored in sorted order by key, + # sequential keys can result in poor distribution of operations + # across nodes. + # + # We recommend that you use bytestrings directly for row keys + # where possible, rather than encoding strings. + # + # For more information about how to design a Bigtable schema for + # the best performance, see the documentation: + # + # https://cloud.google.com/bigtable/docs/schema-design + row_key = f"greeting{i}".encode() + row_mutation = bigtable.data.RowMutationEntry( + row_key, bigtable.data.SetCell(column_family_id, column, value) + ) + mutations.append(row_mutation) + await table.bulk_mutate_rows(mutations) + # [END bigtable_async_hw_write_rows] + + # [START bigtable_async_hw_create_filter] + # Create a filter to only retrieve the most recent version of the cell + # for each column across entire row. + row_filter = bigtable.data.row_filters.CellsColumnLimitFilter(1) + # [END bigtable_async_hw_create_filter] + + # [START bigtable_async_hw_get_with_filter] + # [START bigtable_async_hw_get_by_key] + print("Getting a single greeting by row key.") + key = "greeting0".encode() + + row = await table.read_row(key, row_filter=row_filter) + cell = row.cells[0] + print(cell.value.decode("utf-8")) + # [END bigtable_async_hw_get_by_key] + # [END bigtable_async_hw_get_with_filter] + + # [START bigtable_async_hw_scan_with_filter] + # [START bigtable_async_hw_scan_all] + print("Scanning for all greetings:") + query = bigtable.data.ReadRowsQuery(row_filter=row_filter) + async for row in await table.read_rows_stream(query): + cell = row.cells[0] + print(cell.value.decode("utf-8")) + # [END bigtable_async_hw_scan_all] + # [END bigtable_async_hw_scan_with_filter] + finally: + # [START bigtable_async_hw_delete_table] + # the async client only supports the data API. Table deletion as an admin operation + # use admin client to create the table + print("Deleting the {} table.".format(table_id)) + admin_table.delete() + # [END bigtable_async_hw_delete_table] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("project_id", help="Your Cloud Platform project ID.") + parser.add_argument( + "instance_id", help="ID of the Cloud Bigtable instance to connect to." + ) + parser.add_argument( + "--table", help="Table to create and destroy.", default="Hello-Bigtable" + ) + + args = parser.parse_args() + asyncio.run(main(args.project_id, args.instance_id, args.table)) diff --git a/packages/google-cloud-bigtable/samples/hello/async_main_test.py b/packages/google-cloud-bigtable/samples/hello/async_main_test.py new file mode 100644 index 000000000000..aa65a86523f4 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/hello/async_main_test.py @@ -0,0 +1,36 @@ +# Copyright 2024 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 os +import asyncio +import uuid + +from .async_main import main + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] +TABLE_ID = f"hello-world-test-async-{str(uuid.uuid4())[:16]}" + + +def test_async_main(capsys): + asyncio.run(main(PROJECT, BIGTABLE_INSTANCE, TABLE_ID)) + + out, _ = capsys.readouterr() + assert "Creating the {} table.".format(TABLE_ID) in out + assert "Writing some greetings to the table." in out + assert "Getting a single greeting by row key." in out + assert "Hello World!" in out + assert "Scanning for all greetings" in out + assert "Hello Cloud Bigtable!" in out + assert "Deleting the {} table.".format(TABLE_ID) in out diff --git a/packages/google-cloud-bigtable/samples/hello/main.py b/packages/google-cloud-bigtable/samples/hello/main.py new file mode 100644 index 000000000000..2c0d83f98fb8 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/hello/main.py @@ -0,0 +1,149 @@ +#!/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. + +"""Demonstrates how to connect to Cloud Bigtable and run some basic operations. + +Prerequisites: + +- Create a Cloud Bigtable instance. + https://cloud.google.com/bigtable/docs/creating-instance +- Set your Google Application Default Credentials. + https://developers.google.com/identity/protocols/application-default-credentials +""" + +import argparse +from ..utils import wait_for_table + +# [START bigtable_hw_imports] +from datetime import datetime, timezone + +from google.cloud import bigtable +from google.cloud.bigtable import column_family +from google.cloud.bigtable import row_filters + +# [END bigtable_hw_imports] + +# use to avoid warnings +row_filters +column_family + + +def main(project_id, instance_id, table_id): + # [START bigtable_hw_connect] + # The client must be created with admin=True because it will create a + # table. + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + # [END bigtable_hw_connect] + + # [START bigtable_hw_create_table] + print("Creating the {} table.".format(table_id)) + table = instance.table(table_id) + + print("Creating column family cf1 with Max Version GC rule...") + # Create a column family with GC policy : most recent N versions + # Define the GC policy to retain only the most recent 2 versions + max_versions_rule = bigtable.column_family.MaxVersionsGCRule(2) + column_family_id = b"cf1" + column_families = {column_family_id: max_versions_rule} + if not table.exists(): + table.create(column_families=column_families) + else: + print("Table {} already exists.".format(table_id)) + # [END bigtable_hw_create_table] + + try: + # let table creation complete + wait_for_table(table) + + # [START bigtable_hw_write_rows] + print("Writing some greetings to the table.") + greetings = [b"Hello World!", b"Hello Cloud Bigtable!", b"Hello Python!"] + rows = [] + column = b"greeting" + for i, value in enumerate(greetings): + # Note: This example uses sequential numeric IDs for simplicity, + # but this can result in poor performance in a production + # application. Since rows are stored in sorted order by key, + # sequential keys can result in poor distribution of operations + # across nodes. + # + # We recommend that you use bytestrings directly for row keys + # where possible, rather than encoding strings. + # + # For more information about how to design a Bigtable schema for + # the best performance, see the documentation: + # + # https://cloud.google.com/bigtable/docs/schema-design + row_key = f"greeting{i}".encode() + row = table.direct_row(row_key) + row.set_cell( + column_family_id, column, value, timestamp=datetime.now(timezone.utc), + ) + rows.append(row) + table.mutate_rows(rows) + # [END bigtable_hw_write_rows] + + # [START bigtable_hw_create_filter] + # Create a filter to only retrieve the most recent version of the cell + # for each column across entire row. + row_filter = bigtable.row_filters.CellsColumnLimitFilter(1) + # [END bigtable_hw_create_filter] + + # [START bigtable_hw_get_with_filter] + # [START bigtable_hw_get_by_key] + print("Getting a single greeting by row key.") + key = b"greeting0" + + row = table.read_row(key, row_filter) + cell = row.cells[column_family_id.decode("utf-8")][column][0] + print(cell.value.decode("utf-8")) + # [END bigtable_hw_get_by_key] + # [END bigtable_hw_get_with_filter] + + # [START bigtable_hw_scan_with_filter] + # [START bigtable_hw_scan_all] + print("Scanning for all greetings:") + partial_rows = table.read_rows(filter_=row_filter) + + for row in partial_rows: + column_family_id_str = column_family_id.decode("utf-8") + cell = row.cells[column_family_id_str][column][0] + print(cell.value.decode("utf-8")) + # [END bigtable_hw_scan_all] + # [END bigtable_hw_scan_with_filter] + + finally: + # [START bigtable_hw_delete_table] + print("Deleting the {} table.".format(table_id)) + table.delete() + # [END bigtable_hw_delete_table] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("project_id", help="Your Cloud Platform project ID.") + parser.add_argument( + "instance_id", help="ID of the Cloud Bigtable instance to connect to." + ) + parser.add_argument( + "--table", help="Table to create and destroy.", default="Hello-Bigtable" + ) + + args = parser.parse_args() + main(args.project_id, args.instance_id, args.table) diff --git a/packages/google-cloud-bigtable/samples/hello/main_test.py b/packages/google-cloud-bigtable/samples/hello/main_test.py new file mode 100644 index 000000000000..28814d909d2c --- /dev/null +++ b/packages/google-cloud-bigtable/samples/hello/main_test.py @@ -0,0 +1,35 @@ +# 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 os +import uuid + +from .main import main + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] +TABLE_ID = f"hello-world-test-{str(uuid.uuid4())[:16]}" + + +def test_main(capsys): + main(PROJECT, BIGTABLE_INSTANCE, TABLE_ID) + + out, _ = capsys.readouterr() + assert "Creating the {} table.".format(TABLE_ID) in out + assert "Writing some greetings to the table." in out + assert "Getting a single greeting by row key." in out + assert "Hello World!" in out + assert "Scanning for all greetings" in out + assert "Hello Cloud Bigtable!" in out + assert "Deleting the {} table.".format(TABLE_ID) in out diff --git a/packages/google-cloud-bigtable/samples/hello/noxfile.py b/packages/google-cloud-bigtable/samples/hello/noxfile.py new file mode 100644 index 000000000000..a169b5b5b464 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/hello/noxfile.py @@ -0,0 +1,292 @@ +# Copyright 2019 Google LLC +# +# 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 __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, Optional + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in ( + "True", + "true", +) + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8") + else: + session.install("flake8", "flake8-annotations") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# format = isort + black +# + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list.extend(glob.glob("**/tests", recursive=True)) + + if len(test_list) == 0: + print("No tests found, skipping directory.") + return + + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + concurrent_args = [] + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + with open("requirements.txt") as rfile: + packages = rfile.read() + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install( + "-r", "requirements-test.txt", "-c", "constraints-test.txt" + ) + else: + session.install("-r", "requirements-test.txt") + with open("requirements-test.txt") as rtfile: + packages += rtfile.read() + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + if "pytest-parallel" in packages: + concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + elif "pytest-xdist" in packages: + concurrent_args.extend(['-n', 'auto']) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs + concurrent_args), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/packages/google-cloud-bigtable/samples/hello/requirements-test.txt b/packages/google-cloud-bigtable/samples/hello/requirements-test.txt new file mode 100644 index 000000000000..e079f8a6038d --- /dev/null +++ b/packages/google-cloud-bigtable/samples/hello/requirements-test.txt @@ -0,0 +1 @@ +pytest diff --git a/packages/google-cloud-bigtable/samples/hello/requirements.txt b/packages/google-cloud-bigtable/samples/hello/requirements.txt new file mode 100644 index 000000000000..5113ca7f17bb --- /dev/null +++ b/packages/google-cloud-bigtable/samples/hello/requirements.txt @@ -0,0 +1,2 @@ +google-cloud-bigtable==2.35.0 +google-cloud-core==2.5.0 diff --git a/packages/google-cloud-bigtable/samples/hello_happybase/README.md b/packages/google-cloud-bigtable/samples/hello_happybase/README.md new file mode 100644 index 000000000000..fdbea4e63739 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/hello_happybase/README.md @@ -0,0 +1,52 @@ +[//]: # "This README.md file is auto-generated, all changes to this file will be lost." +[//]: # "To regenerate it, use `python -m synthtool`." + +## Python Samples for Cloud Bigtable + +This directory contains samples for Cloud Bigtable, which may be used as a refererence for how to use this product. +Samples, quickstarts, and other documentation are available at cloud.google.com. + + +### Hello World using HappyBase + +This sample demonstrates using the Google Cloud Client Library HappyBase package, an implementation of the HappyBase API to connect to and interact with Cloud Bigtable. More information available at: https://cloud.google.com/bigtable/docs/samples-python-hello-happybase + + +Open in Cloud Shell + + +To run this sample: + +1. If this is your first time working with GCP products, you will need to set up [the Cloud SDK][cloud_sdk] or utilize [Google Cloud Shell][gcloud_shell]. This sample may [require authetication][authentication] and you will need to [enable billing][enable_billing]. + +1. Make a fork of this repo and clone the branch locally, then navigate to the sample directory you want to use. + +1. Install the dependencies needed to run the samples. + + pip install -r requirements.txt + +1. Run the sample using + + python main.py + + + +
usage: main.py [-h] [--table TABLE] project_id instance_id
Demonstrates how to connect to Cloud Bigtable and run some basic operations.
Prerequisites: - Create a Cloud Bigtable cluster.
https://cloud.google.com/bigtable/docs/creating-cluster - Set your Google
Application Default Credentials.
https://developers.google.com/identity/protocols/application-default-
credentials


positional arguments:
  project_id     Your Cloud Platform project ID.
  instance_id    ID of the Cloud Bigtable instance to connect to.


optional arguments:
  -h, --help     show this help message and exit
  --table TABLE  Table to create and destroy. (default: Hello-Bigtable)
+ +## Additional Information + +You can read the documentation for more details on API usage and use GitHub +to browse the source and [report issues][issues]. + +### Contributing +View the [contributing guidelines][contrib_guide], the [Python style guide][py_style] for more information. + +[authentication]: https://cloud.google.com/docs/authentication/getting-started +[enable_billing]:https://cloud.google.com/apis/docs/getting-started#enabling_billing +[client_library_python]: https://googlecloudplatform.github.io/google-cloud-python/ +[issues]: https://github.com/GoogleCloudPlatform/google-cloud-python/issues +[contrib_guide]: https://github.com/googleapis/google-cloud-python/blob/main/CONTRIBUTING.rst +[py_style]: http://google.github.io/styleguide/pyguide.html +[cloud_sdk]: https://cloud.google.com/sdk/docs +[gcloud_shell]: https://cloud.google.com/shell/docs +[gcloud_shell]: https://cloud.google.com/shell/docs diff --git a/packages/google-cloud-bigtable/samples/hello_happybase/__init__.py b/packages/google-cloud-bigtable/samples/hello_happybase/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/samples/hello_happybase/main.py b/packages/google-cloud-bigtable/samples/hello_happybase/main.py new file mode 100644 index 000000000000..50820febde8b --- /dev/null +++ b/packages/google-cloud-bigtable/samples/hello_happybase/main.py @@ -0,0 +1,117 @@ +#!/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. + +"""Demonstrates how to connect to Cloud Bigtable and run some basic operations. + +Prerequisites: + +- Create a Cloud Bigtable cluster. + https://cloud.google.com/bigtable/docs/creating-cluster +- Set your Google Application Default Credentials. + https://developers.google.com/identity/protocols/application-default-credentials +""" + +import argparse +from ..utils import wait_for_table + +# [START bigtable_hw_imports_happybase] +from google.cloud import bigtable +from google.cloud import happybase + +# [END bigtable_hw_imports_happybase] + + +def main(project_id, instance_id, table_name): + # [START bigtable_hw_connect_happybase] + # The client must be created with admin=True because it will create a + # table. + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + connection = happybase.Connection(instance=instance) + # [END bigtable_hw_connect_happybase] + + try: + # [START bigtable_hw_create_table_happybase] + print("Creating the {} table.".format(table_name)) + column_family_name = "cf1" + connection.create_table( + table_name, {column_family_name: dict()} # Use default options. + ) + # [END bigtable_hw_create_table_happybase] + + wait_for_table(instance.table(table_name)) + + # [START bigtable_hw_write_rows_happybase] + print("Writing some greetings to the table.") + table = connection.table(table_name) + column_name = "{fam}:greeting".format(fam=column_family_name) + greetings = [ + "Hello World!", + "Hello Cloud Bigtable!", + "Hello HappyBase!", + ] + + for i, value in enumerate(greetings): + # Note: This example uses sequential numeric IDs for simplicity, + # but this can result in poor performance in a production + # application. Since rows are stored in sorted order by key, + # sequential keys can result in poor distribution of operations + # across nodes. + # + # For more information about how to design a Bigtable schema for + # the best performance, see the documentation: + # + # https://cloud.google.com/bigtable/docs/schema-design + row_key = "greeting{}".format(i) + table.put(row_key, {column_name.encode("utf-8"): value.encode("utf-8")}) + # [END bigtable_hw_write_rows_happybase] + + # [START bigtable_hw_get_by_key_happybase] + print("Getting a single greeting by row key.") + key = "greeting0".encode("utf-8") + row = table.row(key) + print("\t{}: {}".format(key, row[column_name.encode("utf-8")])) + # [END bigtable_hw_get_by_key_happybase] + + # [START bigtable_hw_scan_all_happybase] + print("Scanning for all greetings:") + + for key, row in table.scan(): + print("\t{}: {}".format(key, row[column_name.encode("utf-8")])) + # [END bigtable_hw_scan_all_happybase] + + finally: + # [START bigtable_hw_delete_table_happybase] + print("Deleting the {} table.".format(table_name)) + connection.delete_table(table_name) + # [END bigtable_hw_delete_table_happybase] + connection.close() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("project_id", help="Your Cloud Platform project ID.") + parser.add_argument( + "instance_id", help="ID of the Cloud Bigtable instance to connect to." + ) + parser.add_argument( + "--table", help="Table to create and destroy.", default="Hello-Bigtable" + ) + + args = parser.parse_args() + main(args.project_id, args.instance_id, args.table) diff --git a/packages/google-cloud-bigtable/samples/hello_happybase/main_test.py b/packages/google-cloud-bigtable/samples/hello_happybase/main_test.py new file mode 100644 index 000000000000..252f4ccaf9e7 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/hello_happybase/main_test.py @@ -0,0 +1,44 @@ +# 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 os +import uuid + +from .main import main +from google.cloud import bigtable + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] +TABLE_ID = f"hello-world-hb-test-{str(uuid.uuid4())[:16]}" + + +def test_main(capsys): + try: + main(PROJECT, BIGTABLE_INSTANCE, TABLE_ID) + + out, _ = capsys.readouterr() + assert "Creating the {} table.".format(TABLE_ID) in out + assert "Writing some greetings to the table." in out + assert "Getting a single greeting by row key." in out + assert "Hello World!" in out + assert "Scanning for all greetings" in out + assert "Hello Cloud Bigtable!" in out + assert "Deleting the {} table.".format(TABLE_ID) in out + finally: + # delete table + client = bigtable.Client(PROJECT, admin=True) + instance = client.instance(BIGTABLE_INSTANCE) + table = instance.table(TABLE_ID) + if table.exists(): + table.delete() diff --git a/packages/google-cloud-bigtable/samples/hello_happybase/noxfile.py b/packages/google-cloud-bigtable/samples/hello_happybase/noxfile.py new file mode 100644 index 000000000000..a169b5b5b464 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/hello_happybase/noxfile.py @@ -0,0 +1,292 @@ +# Copyright 2019 Google LLC +# +# 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 __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, Optional + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in ( + "True", + "true", +) + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8") + else: + session.install("flake8", "flake8-annotations") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# format = isort + black +# + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list.extend(glob.glob("**/tests", recursive=True)) + + if len(test_list) == 0: + print("No tests found, skipping directory.") + return + + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + concurrent_args = [] + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + with open("requirements.txt") as rfile: + packages = rfile.read() + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install( + "-r", "requirements-test.txt", "-c", "constraints-test.txt" + ) + else: + session.install("-r", "requirements-test.txt") + with open("requirements-test.txt") as rtfile: + packages += rtfile.read() + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + if "pytest-parallel" in packages: + concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + elif "pytest-xdist" in packages: + concurrent_args.extend(['-n', 'auto']) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs + concurrent_args), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/packages/google-cloud-bigtable/samples/hello_happybase/requirements-test.txt b/packages/google-cloud-bigtable/samples/hello_happybase/requirements-test.txt new file mode 100644 index 000000000000..e079f8a6038d --- /dev/null +++ b/packages/google-cloud-bigtable/samples/hello_happybase/requirements-test.txt @@ -0,0 +1 @@ +pytest diff --git a/packages/google-cloud-bigtable/samples/hello_happybase/requirements.txt b/packages/google-cloud-bigtable/samples/hello_happybase/requirements.txt new file mode 100644 index 000000000000..dc1a04f30378 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/hello_happybase/requirements.txt @@ -0,0 +1,2 @@ +google-cloud-happybase==0.33.0 +six==1.17.0 # See https://github.com/googleapis/google-cloud-python-happybase/issues/128 diff --git a/packages/google-cloud-bigtable/samples/instanceadmin/README.md b/packages/google-cloud-bigtable/samples/instanceadmin/README.md new file mode 100644 index 000000000000..675add700e93 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/instanceadmin/README.md @@ -0,0 +1,52 @@ +[//]: # "This README.md file is auto-generated, all changes to this file will be lost." +[//]: # "To regenerate it, use `python -m synthtool`." + +## Python Samples for Cloud Bigtable + +This directory contains samples for Cloud Bigtable, which may be used as a refererence for how to use this product. +Samples, quickstarts, and other documentation are available at cloud.google.com. + + +### cbt Command Demonstration + +This page explains how to use the cbt command to connect to a Cloud Bigtable instance, perform basic administrative tasks, and read and write data in a table. More information about this quickstart is available at https://cloud.google.com/bigtable/docs/quickstart-cbt + + +Open in Cloud Shell + + +To run this sample: + +1. If this is your first time working with GCP products, you will need to set up [the Cloud SDK][cloud_sdk] or utilize [Google Cloud Shell][gcloud_shell]. This sample may [require authetication][authentication] and you will need to [enable billing][enable_billing]. + +1. Make a fork of this repo and clone the branch locally, then navigate to the sample directory you want to use. + +1. Install the dependencies needed to run the samples. + + pip install -r requirements.txt + +1. Run the sample using + + python instanceadmin.py + + + +
usage: instanceadmin.py [-h] [run] [dev-instance] [del-instance] [add-cluster] [del-cluster] project_id instance_id cluster_id
Demonstrates how to connect to Cloud Bigtable and run some basic operations.
Prerequisites: - Create a Cloud Bigtable cluster.
https://cloud.google.com/bigtable/docs/creating-cluster - Set your Google
Application Default Credentials.
https://developers.google.com/identity/protocols/application-default-
credentials


positional arguments:
  project_id     Your Cloud Platform project ID.
  instance_id    ID of the Cloud Bigtable instance to connect to.


optional arguments:
  -h, --help     show this help message and exit
  --table TABLE  Table to create and destroy. (default: Hello-Bigtable)
+ +## Additional Information + +You can read the documentation for more details on API usage and use GitHub +to browse the source and [report issues][issues]. + +### Contributing +View the [contributing guidelines][contrib_guide], the [Python style guide][py_style] for more information. + +[authentication]: https://cloud.google.com/docs/authentication/getting-started +[enable_billing]:https://cloud.google.com/apis/docs/getting-started#enabling_billing +[client_library_python]: https://googlecloudplatform.github.io/google-cloud-python/ +[issues]: https://github.com/GoogleCloudPlatform/google-cloud-python/issues +[contrib_guide]: https://github.com/googleapis/google-cloud-python/blob/main/CONTRIBUTING.rst +[py_style]: http://google.github.io/styleguide/pyguide.html +[cloud_sdk]: https://cloud.google.com/sdk/docs +[gcloud_shell]: https://cloud.google.com/shell/docs +[gcloud_shell]: https://cloud.google.com/shell/docs diff --git a/packages/google-cloud-bigtable/samples/instanceadmin/instanceadmin.py b/packages/google-cloud-bigtable/samples/instanceadmin/instanceadmin.py new file mode 100644 index 000000000000..7341bfc46f19 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/instanceadmin/instanceadmin.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python + +# Copyright 2018, Google LLC +# 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. + +"""Demonstrates how to connect to Cloud Bigtable and run some basic operations. +# http://www.apache.org/licenses/LICENSE-2.0 +Prerequisites: +- Create a Cloud Bigtable project. + https://cloud.google.com/bigtable/docs/ +- Set your Google Application Default Credentials. + https://developers.google.com/identity/protocols/application-default-credentials + +Operations performed: +- Create a Cloud Bigtable Instance. +- List Instance for a Cloud Bigtable. +- Delete a Cloud Bigtable Instance. +- Create a Cloud Bigtable Cluster. +- List Cloud Bigtable Clusters. +- Delete a Cloud Bigtable Cluster. +""" + +import argparse + +from google.cloud import bigtable +from google.cloud.bigtable import enums + + +def run_instance_operations(project_id, instance_id, cluster_id): + """Check Instance exists. + Creates a Production instance with default Cluster. + List instances in a project. + List clusters in an instance. + + :type project_id: str + :param project_id: Project id of the client. + + :type instance_id: str + :param instance_id: Instance of the client. + """ + client = bigtable.Client(project=project_id, admin=True) + location_id = "us-central1-f" + serve_nodes = 1 + storage_type = enums.StorageType.SSD + labels = {"prod-label": "prod-label"} + instance = client.instance(instance_id, labels=labels) + + # [START bigtable_check_instance_exists] + if not instance.exists(): + print("Instance {} does not exist.".format(instance_id)) + else: + print("Instance {} already exists.".format(instance_id)) + # [END bigtable_check_instance_exists] + + # [START bigtable_create_prod_instance] + cluster = instance.cluster( + cluster_id, + location_id=location_id, + serve_nodes=serve_nodes, + default_storage_type=storage_type, + ) + if not instance.exists(): + print("\nCreating an instance") + # Create instance with given options + operation = instance.create(clusters=[cluster]) + # Ensure the operation completes. + operation.result(timeout=480) + print("\nCreated instance: {}".format(instance_id)) + # [END bigtable_create_prod_instance] + + # [START bigtable_list_instances] + print("\nListing instances:") + for instance_local in client.list_instances()[0]: + print(instance_local.instance_id) + # [END bigtable_list_instances] + + # [START bigtable_get_instance] + print( + "\nName of instance: {}\nLabels: {}".format( + instance.display_name, instance.labels + ) + ) + # [END bigtable_get_instance] + + # [START bigtable_get_clusters] + print("\nListing clusters...") + for cluster in instance.list_clusters()[0]: + print(cluster.cluster_id) + # [END bigtable_get_clusters] + + +def delete_instance(project_id, instance_id): + """Delete the Instance + + :type project_id: str + :param project_id: Project id of the client. + + :type instance_id: str + :param instance_id: Instance of the client. + """ + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + # [START bigtable_delete_instance] + print("\nDeleting instance") + if not instance.exists(): + print("Instance {} does not exist.".format(instance_id)) + else: + instance.delete() + print("Deleted instance: {}".format(instance_id)) + # [END bigtable_delete_instance] + + +def add_cluster(project_id, instance_id, cluster_id): + """Add Cluster + + :type project_id: str + :param project_id: Project id of the client. + + :type instance_id: str + :param instance_id: Instance of the client. + + :type cluster_id: str + :param cluster_id: Cluster id. + """ + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + + location_id = "us-central1-a" + serve_nodes = 1 + storage_type = enums.StorageType.SSD + + if not instance.exists(): + print("Instance {} does not exist.".format(instance_id)) + else: + print("\nAdding cluster to instance {}".format(instance_id)) + # [START bigtable_create_cluster] + print("\nListing clusters...") + for cluster in instance.list_clusters()[0]: + print(cluster.cluster_id) + cluster = instance.cluster( + cluster_id, + location_id=location_id, + serve_nodes=serve_nodes, + default_storage_type=storage_type, + ) + if cluster.exists(): + print("\nCluster not created, as {} already exists.".format(cluster_id)) + else: + operation = cluster.create() + # Ensure the operation completes. + operation.result(timeout=480) + print("\nCluster created: {}".format(cluster_id)) + # [END bigtable_create_cluster] + + +def delete_cluster(project_id, instance_id, cluster_id): + """Delete the cluster + + :type project_id: str + :param project_id: Project id of the client. + + :type instance_id: str + :param instance_id: Instance of the client. + + :type cluster_id: str + :param cluster_id: Cluster id. + """ + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + cluster = instance.cluster(cluster_id) + + # [START bigtable_delete_cluster] + print("\nDeleting cluster") + if cluster.exists(): + cluster.delete() + print("Cluster deleted: {}".format(cluster_id)) + else: + print("\nCluster {} does not exist.".format(cluster_id)) + + # [END bigtable_delete_cluster] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "command", + help="run, del-instance, \ + add-cluster or del-cluster. \ + Operation to perform on Instance.", + ) + parser.add_argument("project_id", help="Your Cloud Platform project ID.") + parser.add_argument( + "instance_id", + help="ID of the Cloud Bigtable instance to \ + connect to.", + ) + parser.add_argument( + "cluster_id", + help="ID of the Cloud Bigtable cluster to \ + connect to.", + ) + + args = parser.parse_args() + + if args.command.lower() == "run": + run_instance_operations(args.project_id, args.instance_id, args.cluster_id) + elif args.command.lower() == "del-instance": + delete_instance(args.project_id, args.instance_id) + elif args.command.lower() == "add-cluster": + add_cluster(args.project_id, args.instance_id, args.cluster_id) + elif args.command.lower() == "del-cluster": + delete_cluster(args.project_id, args.instance_id, args.cluster_id) + else: + print( + "Command should be either run \n Use argument -h, \ + --help to show help and exit." + ) diff --git a/packages/google-cloud-bigtable/samples/instanceadmin/noxfile.py b/packages/google-cloud-bigtable/samples/instanceadmin/noxfile.py new file mode 100644 index 000000000000..a169b5b5b464 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/instanceadmin/noxfile.py @@ -0,0 +1,292 @@ +# Copyright 2019 Google LLC +# +# 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 __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, Optional + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in ( + "True", + "true", +) + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8") + else: + session.install("flake8", "flake8-annotations") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# format = isort + black +# + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list.extend(glob.glob("**/tests", recursive=True)) + + if len(test_list) == 0: + print("No tests found, skipping directory.") + return + + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + concurrent_args = [] + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + with open("requirements.txt") as rfile: + packages = rfile.read() + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install( + "-r", "requirements-test.txt", "-c", "constraints-test.txt" + ) + else: + session.install("-r", "requirements-test.txt") + with open("requirements-test.txt") as rtfile: + packages += rtfile.read() + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + if "pytest-parallel" in packages: + concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + elif "pytest-xdist" in packages: + concurrent_args.extend(['-n', 'auto']) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs + concurrent_args), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/packages/google-cloud-bigtable/samples/instanceadmin/requirements-test.txt b/packages/google-cloud-bigtable/samples/instanceadmin/requirements-test.txt new file mode 100644 index 000000000000..e079f8a6038d --- /dev/null +++ b/packages/google-cloud-bigtable/samples/instanceadmin/requirements-test.txt @@ -0,0 +1 @@ +pytest diff --git a/packages/google-cloud-bigtable/samples/instanceadmin/requirements.txt b/packages/google-cloud-bigtable/samples/instanceadmin/requirements.txt new file mode 100644 index 000000000000..67a1ea5b8d23 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/instanceadmin/requirements.txt @@ -0,0 +1,2 @@ +google-cloud-bigtable==2.35.0 +backoff==2.2.1 diff --git a/packages/google-cloud-bigtable/samples/instanceadmin/test_instanceadmin.py b/packages/google-cloud-bigtable/samples/instanceadmin/test_instanceadmin.py new file mode 100644 index 000000000000..b0041294bf1c --- /dev/null +++ b/packages/google-cloud-bigtable/samples/instanceadmin/test_instanceadmin.py @@ -0,0 +1,180 @@ +# Copyright 2018 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 os +import random +import time +import warnings + +import backoff +from google.api_core import exceptions +from google.cloud import bigtable +import pytest + +import instanceadmin + + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +INSTANCE_ID_FORMAT = "instanceadmin-{:03}-{}" +CLUSTER_ID_FORMAT = "instanceadmin-{:03}" +ID_RANGE = 1000 + +INSTANCE = INSTANCE_ID_FORMAT.format(random.randrange(ID_RANGE), int(time.time())) +CLUSTER1 = CLUSTER_ID_FORMAT.format(random.randrange(ID_RANGE)) +CLUSTER2 = CLUSTER_ID_FORMAT.format(random.randrange(ID_RANGE)) + + +@pytest.fixture(scope="module", autouse=True) +def preclean(): + """In case any test instances weren't cleared out in a previous run. + + Deletes any test instances that were created over an hour ago. Newer instances may + be being used by a concurrent test run. + """ + client = bigtable.Client(project=PROJECT, admin=True) + for instance in client.list_instances()[0]: + if instance.instance_id.startswith("instanceadmin-"): + timestamp = instance.instance_id.split("-")[-1] + timestamp = int(timestamp) + if time.time() - timestamp > 3600: + warnings.warn( + f"Deleting leftover test instance: {instance.instance_id}" + ) + instance.delete() + + +@pytest.fixture +def dispose_of(): + instances = [] + + def disposal(instance): + instances.append(instance) + + yield disposal + + client = bigtable.Client(project=PROJECT, admin=True) + for instance_id in instances: + instance = client.instance(instance_id) + if instance.exists(): + instance.delete() + + +def test_run_instance_operations(capsys, dispose_of): + dispose_of(INSTANCE) + + instanceadmin.run_instance_operations(PROJECT, INSTANCE, CLUSTER1) + out = capsys.readouterr().out + assert f"Instance {INSTANCE} does not exist." in out + assert "Creating an instance" in out + assert f"Created instance: {INSTANCE}" in out + assert "Listing instances" in out + assert f"\n{INSTANCE}\n" in out + assert f"Name of instance: {INSTANCE}" in out + assert "Labels: {'prod-label': 'prod-label'}" in out + assert "Listing clusters..." in out + assert f"\n{CLUSTER1}\n" in out + + instanceadmin.run_instance_operations(PROJECT, INSTANCE, CLUSTER1) + out = capsys.readouterr().out + assert f"Instance {INSTANCE} already exists." in out + assert "Listing instances" in out + assert f"\n{INSTANCE}\n" in out + assert f"Name of instance: {INSTANCE}" in out + assert "Labels: {'prod-label': 'prod-label'}" in out + assert "Listing clusters..." in out + assert f"\n{CLUSTER1}\n" in out + + +def test_delete_instance(capsys, dispose_of): + from concurrent.futures import TimeoutError + + @backoff.on_exception(backoff.expo, TimeoutError) + def _set_up_instance(): + dispose_of(INSTANCE) + + # Can't delete it, it doesn't exist + instanceadmin.delete_instance(PROJECT, INSTANCE) + out = capsys.readouterr().out + assert "Deleting instance" in out + assert f"Instance {INSTANCE} does not exist" in out + + # Ok, create it then + instanceadmin.run_instance_operations(PROJECT, INSTANCE, CLUSTER1) + capsys.readouterr() # throw away output + + _set_up_instance() + + # Now delete it + instanceadmin.delete_instance(PROJECT, INSTANCE) + out = capsys.readouterr().out + assert "Deleting instance" in out + assert f"Deleted instance: {INSTANCE}" in out + + +def test_add_and_delete_cluster(capsys, dispose_of): + from concurrent.futures import TimeoutError + + @backoff.on_exception(backoff.expo, TimeoutError) + def _set_up_instance(): + dispose_of(INSTANCE) + + # This won't work, because the instance isn't created yet + instanceadmin.add_cluster(PROJECT, INSTANCE, CLUSTER2) + out = capsys.readouterr().out + assert f"Instance {INSTANCE} does not exist" in out + + # Get the instance created + instanceadmin.run_instance_operations(PROJECT, INSTANCE, CLUSTER1) + capsys.readouterr() # throw away output + + _set_up_instance() + + # Add a cluster to that instance + # Avoid failing for "instance is currently being changed" by + # applying an exponential backoff + backoff_503 = backoff.on_exception(backoff.expo, exceptions.ServiceUnavailable) + + backoff_503(instanceadmin.add_cluster)(PROJECT, INSTANCE, CLUSTER2) + out = capsys.readouterr().out + assert f"Adding cluster to instance {INSTANCE}" in out + assert "Listing clusters..." in out + assert f"\n{CLUSTER1}\n" in out + assert f"Cluster created: {CLUSTER2}" in out + + # Try to add the same cluster again, won't work + instanceadmin.add_cluster(PROJECT, INSTANCE, CLUSTER2) + out = capsys.readouterr().out + assert "Listing clusters..." in out + assert f"\n{CLUSTER1}\n" in out + assert f"\n{CLUSTER2}\n" in out + assert f"Cluster not created, as {CLUSTER2} already exists." + + # Now delete it + instanceadmin.delete_cluster(PROJECT, INSTANCE, CLUSTER2) + out = capsys.readouterr().out + assert "Deleting cluster" in out + assert f"Cluster deleted: {CLUSTER2}" in out + + # Verify deletion + instanceadmin.run_instance_operations(PROJECT, INSTANCE, CLUSTER1) + out = capsys.readouterr().out + assert "Listing clusters..." in out + assert f"\n{CLUSTER1}\n" in out + assert f"\n{CLUSTER2}\n" not in out + + # Try deleting it again, for fun (and coverage) + instanceadmin.delete_cluster(PROJECT, INSTANCE, CLUSTER2) + out = capsys.readouterr().out + assert "Deleting cluster" in out + assert f"Cluster {CLUSTER2} does not exist" in out diff --git a/packages/google-cloud-bigtable/samples/metricscaler/Dockerfile b/packages/google-cloud-bigtable/samples/metricscaler/Dockerfile new file mode 100644 index 000000000000..d8a5ec0c1a9b --- /dev/null +++ b/packages/google-cloud-bigtable/samples/metricscaler/Dockerfile @@ -0,0 +1,24 @@ +# Copyright 2020 Google LLC +# +# 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 python:3 + +WORKDIR /usr/src/app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +ENTRYPOINT [ "python", "./metricscaler.py"] +CMD ["--help"] diff --git a/packages/google-cloud-bigtable/samples/metricscaler/README.md b/packages/google-cloud-bigtable/samples/metricscaler/README.md new file mode 100644 index 000000000000..e1624bb1872e --- /dev/null +++ b/packages/google-cloud-bigtable/samples/metricscaler/README.md @@ -0,0 +1,52 @@ +[//]: # "This README.md file is auto-generated, all changes to this file will be lost." +[//]: # "To regenerate it, use `python -m synthtool`." + +## Python Samples for Cloud Bigtable + +This directory contains samples for Cloud Bigtable, which may be used as a refererence for how to use this product. +Samples, quickstarts, and other documentation are available at cloud.google.com. + + +### Metric Scaler + +This sample demonstrates how to use Stackdriver Monitoring to scale Cloud Bigtable based on CPU usage. + + +Open in Cloud Shell + + +To run this sample: + +1. If this is your first time working with GCP products, you will need to set up [the Cloud SDK][cloud_sdk] or utilize [Google Cloud Shell][gcloud_shell]. This sample may [require authetication][authentication] and you will need to [enable billing][enable_billing]. + +1. Make a fork of this repo and clone the branch locally, then navigate to the sample directory you want to use. + +1. Install the dependencies needed to run the samples. + + pip install -r requirements.txt + +1. Run the sample using + + python metricscaler.py + + + +
usage: metricscaler.py [-h] [--high_cpu_threshold HIGH_CPU_THRESHOLD] [--low_cpu_threshold LOW_CPU_THRESHOLD] [--short_sleep SHORT_SLEEP] [--long_sleep LONG_SLEEP] bigtable_instance bigtable_cluster
usage: metricscaler.py [-h] [--high_cpu_threshold HIGH_CPU_THRESHOLD]
                       [--low_cpu_threshold LOW_CPU_THRESHOLD]
                       [--short_sleep SHORT_SLEEP] [--long_sleep LONG_SLEEP]
                       bigtable_instance bigtable_cluster


Scales Cloud Bigtable clusters based on CPU usage.


positional arguments:
  bigtable_instance     ID of the Cloud Bigtable instance to connect to.
  bigtable_cluster      ID of the Cloud Bigtable cluster to connect to.


optional arguments:
  -h, --help            show this help message and exit
  --high_cpu_threshold HIGH_CPU_THRESHOLD
                        If Cloud Bigtable CPU usage is above this threshold,
                        scale up
  --low_cpu_threshold LOW_CPU_THRESHOLD
                        If Cloud Bigtable CPU usage is below this threshold,
                        scale down
  --short_sleep SHORT_SLEEP
                        How long to sleep in seconds between checking metrics
                        after no scale operation
  --long_sleep LONG_SLEEP
                        How long to sleep in seconds between checking metrics
                        after a scaling operation
+ +## Additional Information + +You can read the documentation for more details on API usage and use GitHub +to browse the source and [report issues][issues]. + +### Contributing +View the [contributing guidelines][contrib_guide], the [Python style guide][py_style] for more information. + +[authentication]: https://cloud.google.com/docs/authentication/getting-started +[enable_billing]:https://cloud.google.com/apis/docs/getting-started#enabling_billing +[client_library_python]: https://googlecloudplatform.github.io/google-cloud-python/ +[issues]: https://github.com/GoogleCloudPlatform/google-cloud-python/issues +[contrib_guide]: https://github.com/googleapis/google-cloud-python/blob/main/CONTRIBUTING.rst +[py_style]: http://google.github.io/styleguide/pyguide.html +[cloud_sdk]: https://cloud.google.com/sdk/docs +[gcloud_shell]: https://cloud.google.com/shell/docs +[gcloud_shell]: https://cloud.google.com/shell/docs diff --git a/packages/google-cloud-bigtable/samples/metricscaler/metricscaler.py b/packages/google-cloud-bigtable/samples/metricscaler/metricscaler.py new file mode 100644 index 000000000000..f1fe80523dd8 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/metricscaler/metricscaler.py @@ -0,0 +1,235 @@ +# Copyright 2017 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. + +"""Sample that demonstrates how to use Stackdriver Monitoring metrics to +programmatically scale a Google Cloud Bigtable cluster.""" + +import argparse +import logging +import os +import time + +from google.cloud import bigtable +from google.cloud import monitoring_v3 +from google.cloud.bigtable import enums +from google.cloud.monitoring_v3 import query + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] + +logger = logging.getLogger("bigtable.metricscaler") +logger.addHandler(logging.StreamHandler()) +logger.setLevel(logging.INFO) + + +def get_cpu_load(bigtable_instance, bigtable_cluster): + """Returns the most recent Cloud Bigtable CPU load measurement. + + Returns: + float: The most recent Cloud Bigtable CPU usage metric + """ + # [START bigtable_cpu] + client = monitoring_v3.MetricServiceClient() + cpu_query = query.Query( + client, + project=PROJECT, + metric_type="bigtable.googleapis.com/" "cluster/cpu_load", + minutes=5, + ) + cpu_query = cpu_query.select_resources( + instance=bigtable_instance, cluster=bigtable_cluster + ) + cpu = next(cpu_query.iter()) + return cpu.points[0].value.double_value + # [END bigtable_cpu] + + +def get_storage_utilization(bigtable_instance, bigtable_cluster): + """Returns the most recent Cloud Bigtable storage utilization measurement. + + Returns: + float: The most recent Cloud Bigtable storage utilization metric + """ + # [START bigtable_metric_scaler_storage_utilization] + client = monitoring_v3.MetricServiceClient() + utilization_query = query.Query( + client, + project=PROJECT, + metric_type="bigtable.googleapis.com/" "cluster/storage_utilization", + minutes=5, + ) + utilization_query = utilization_query.select_resources( + instance=bigtable_instance, cluster=bigtable_cluster + ) + utilization = next(utilization_query.iter()) + return utilization.points[0].value.double_value + # [END bigtable_metric_scaler_storage_utilization] + + +def scale_bigtable(bigtable_instance, bigtable_cluster, scale_up): + """Scales the number of Cloud Bigtable nodes up or down. + + Edits the number of nodes in the Cloud Bigtable cluster to be increased + or decreased, depending on the `scale_up` boolean argument. Currently + the `incremental` strategy from `strategies.py` is used. + + + Args: + bigtable_instance (str): Cloud Bigtable instance ID to scale + bigtable_cluster (str): Cloud Bigtable cluster ID to scale + scale_up (bool): If true, scale up, otherwise scale down + """ + + # The minimum number of nodes to use. The default minimum is 3. If you have + # a lot of data, the rule of thumb is to not go below 2.5 TB per node for + # SSD lusters, and 8 TB for HDD. The + # "bigtable.googleapis.com/disk/bytes_used" metric is useful in figuring + # out the minimum number of nodes. + min_node_count = 1 + + # The maximum number of nodes to use. The default maximum is 30 nodes per + # zone. If you need more quota, you can request more by following the + # instructions at https://cloud.google.com/bigtable/quota. + max_node_count = 30 + + # The number of nodes to change the cluster by. + size_change_step = 3 + + # [START bigtable_scale] + bigtable_client = bigtable.Client(admin=True) + instance = bigtable_client.instance(bigtable_instance) + instance.reload() + + if instance.type_ == enums.Instance.Type.DEVELOPMENT: + raise ValueError("Development instances cannot be scaled.") + + cluster = instance.cluster(bigtable_cluster) + cluster.reload() + + current_node_count = cluster.serve_nodes + + if scale_up: + if current_node_count < max_node_count: + new_node_count = min(current_node_count + size_change_step, max_node_count) + cluster.serve_nodes = new_node_count + operation = cluster.update() + response = operation.result(480) + logger.info( + "Scaled up from {} to {} nodes for {}.".format( + current_node_count, new_node_count, response.name + ) + ) + else: + if current_node_count > min_node_count: + new_node_count = max(current_node_count - size_change_step, min_node_count) + cluster.serve_nodes = new_node_count + operation = cluster.update() + response = operation.result(480) + logger.info( + "Scaled down from {} to {} nodes for {}.".format( + current_node_count, new_node_count, response.name + ) + ) + # [END bigtable_scale] + + +def main( + bigtable_instance, + bigtable_cluster, + high_cpu_threshold, + low_cpu_threshold, + high_storage_threshold, + short_sleep, + long_sleep, +): + """Main loop runner that autoscales Cloud Bigtable. + + Args: + bigtable_instance (str): Cloud Bigtable instance ID to autoscale + high_cpu_threshold (float): If CPU is higher than this, scale up. + low_cpu_threshold (float): If CPU is lower than this, scale down. + high_storage_threshold (float): If storage is higher than this, + scale up. + short_sleep (int): How long to sleep after no operation + long_sleep (int): How long to sleep after the number of nodes is + changed + """ + cluster_cpu = get_cpu_load(bigtable_instance, bigtable_cluster) + cluster_storage = get_storage_utilization(bigtable_instance, bigtable_cluster) + logger.info("Detected cpu of {}".format(cluster_cpu)) + logger.info("Detected storage utilization of {}".format(cluster_storage)) + try: + if cluster_cpu > high_cpu_threshold or cluster_storage > high_storage_threshold: + scale_bigtable(bigtable_instance, bigtable_cluster, True) + time.sleep(long_sleep) + elif cluster_cpu < low_cpu_threshold: + if cluster_storage < high_storage_threshold: + scale_bigtable(bigtable_instance, bigtable_cluster, False) + time.sleep(long_sleep) + else: + logger.info("CPU within threshold, sleeping.") + time.sleep(short_sleep) + except Exception as e: + logger.error("Error during scaling: %s", e) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Scales Cloud Bigtable clusters based on CPU usage." + ) + parser.add_argument( + "bigtable_instance", help="ID of the Cloud Bigtable instance to connect to." + ) + parser.add_argument( + "bigtable_cluster", help="ID of the Cloud Bigtable cluster to connect to." + ) + parser.add_argument( + "--high_cpu_threshold", + help="If Cloud Bigtable CPU usage is above this threshold, scale up", + default=0.6, + ) + parser.add_argument( + "--low_cpu_threshold", + help="If Cloud Bigtable CPU usage is below this threshold, scale down", + default=0.2, + ) + parser.add_argument( + "--high_storage_threshold", + help="If Cloud Bigtable storage utilization is above this threshold, " + "scale up", + default=0.6, + ) + parser.add_argument( + "--short_sleep", + help="How long to sleep in seconds between checking metrics after no " + "scale operation", + default=60, + ) + parser.add_argument( + "--long_sleep", + help="How long to sleep in seconds between checking metrics after a " + "scaling operation", + default=60 * 10, + ) + args = parser.parse_args() + + while True: + main( + args.bigtable_instance, + args.bigtable_cluster, + float(args.high_cpu_threshold), + float(args.low_cpu_threshold), + float(args.high_storage_threshold), + int(args.short_sleep), + int(args.long_sleep), + ) diff --git a/packages/google-cloud-bigtable/samples/metricscaler/metricscaler_test.py b/packages/google-cloud-bigtable/samples/metricscaler/metricscaler_test.py new file mode 100644 index 000000000000..47be38187f30 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/metricscaler/metricscaler_test.py @@ -0,0 +1,231 @@ +# Copyright 2017 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. + +"""Unit and system tests for metricscaler.py""" + +import os +import uuid + +from google.cloud import bigtable +from google.cloud.bigtable import enums +from mock import Mock, patch + +import pytest +from test_utils.retry import RetryInstanceState +from test_utils.retry import RetryResult + +from metricscaler import get_cpu_load +from metricscaler import get_storage_utilization +from metricscaler import main +from metricscaler import scale_bigtable + + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_ZONE = os.environ["BIGTABLE_ZONE"] +SIZE_CHANGE_STEP = 3 +INSTANCE_ID_FORMAT = "metric-scale-test-{}" +BIGTABLE_INSTANCE = INSTANCE_ID_FORMAT.format(str(uuid.uuid4())[:10]) +BIGTABLE_DEV_INSTANCE = INSTANCE_ID_FORMAT.format(str(uuid.uuid4())[:10]) + + +# System tests to verify API calls succeed + + +@patch("metricscaler.query") +def test_get_cpu_load(monitoring_v3_query): + iter_mock = monitoring_v3_query.Query().select_resources().iter + iter_mock.return_value = iter([Mock(points=[Mock(value=Mock(double_value=1.0))])]) + assert float(get_cpu_load(BIGTABLE_INSTANCE, BIGTABLE_INSTANCE)) > 0.0 + + +@patch("metricscaler.query") +def test_get_storage_utilization(monitoring_v3_query): + iter_mock = monitoring_v3_query.Query().select_resources().iter + iter_mock.return_value = iter([Mock(points=[Mock(value=Mock(double_value=1.0))])]) + assert float(get_storage_utilization(BIGTABLE_INSTANCE, BIGTABLE_INSTANCE)) > 0.0 + + +@pytest.fixture() +def instance(): + cluster_id = BIGTABLE_INSTANCE + + client = bigtable.Client(project=PROJECT, admin=True) + + serve_nodes = 1 + storage_type = enums.StorageType.SSD + production = enums.Instance.Type.PRODUCTION + labels = {"prod-label": "prod-label"} + instance = client.instance( + BIGTABLE_INSTANCE, instance_type=production, labels=labels + ) + + if not instance.exists(): + cluster = instance.cluster( + cluster_id, + location_id=BIGTABLE_ZONE, + serve_nodes=serve_nodes, + default_storage_type=storage_type, + ) + operation = instance.create(clusters=[cluster]) + response = operation.result(480) + print(f"Successfully created {response.name}") + + # Eventual consistency check + retry_found = RetryResult(bool) + retry_found(instance.exists)() + + yield + + instance.delete() + + +@pytest.fixture() +def dev_instance(): + cluster_id = BIGTABLE_DEV_INSTANCE + + client = bigtable.Client(project=PROJECT, admin=True) + + storage_type = enums.StorageType.SSD + development = enums.Instance.Type.DEVELOPMENT + labels = {"dev-label": "dev-label"} + instance = client.instance( + BIGTABLE_DEV_INSTANCE, instance_type=development, labels=labels + ) + + if not instance.exists(): + cluster = instance.cluster( + cluster_id, location_id=BIGTABLE_ZONE, default_storage_type=storage_type + ) + operation = instance.create(clusters=[cluster]) + response = operation.result(480) + print(f"Successfully created {response.name}") + + # Eventual consistency check + retry_found = RetryResult(bool) + retry_found(instance.exists)() + + yield + + instance.delete() + + +class ClusterNodeCountPredicate: + def __init__(self, expected_node_count): + self.expected_node_count = expected_node_count + + def __call__(self, cluster): + expected = self.expected_node_count + print(f"Expected node count: {expected}; found: {cluster.serve_nodes}") + return cluster.serve_nodes == expected + + +def test_scale_bigtable(instance): + bigtable_client = bigtable.Client(admin=True) + + instance = bigtable_client.instance(BIGTABLE_INSTANCE) + instance.reload() + + cluster = instance.cluster(BIGTABLE_INSTANCE) + + _nonzero_node_count = RetryInstanceState( + instance_predicate=lambda c: c.serve_nodes > 0, + max_tries=10, + ) + _nonzero_node_count(cluster.reload)() + + original_node_count = cluster.serve_nodes + + scale_bigtable(BIGTABLE_INSTANCE, BIGTABLE_INSTANCE, True) + + scaled_node_count_predicate = ClusterNodeCountPredicate( + original_node_count + SIZE_CHANGE_STEP + ) + scaled_node_count_predicate.__name__ = "scaled_node_count_predicate" + _scaled_node_count = RetryInstanceState( + instance_predicate=scaled_node_count_predicate, + max_tries=10, + ) + _scaled_node_count(cluster.reload)() + + scale_bigtable(BIGTABLE_INSTANCE, BIGTABLE_INSTANCE, False) + + restored_node_count_predicate = ClusterNodeCountPredicate(original_node_count) + restored_node_count_predicate.__name__ = "restored_node_count_predicate" + _restored_node_count = RetryInstanceState( + instance_predicate=restored_node_count_predicate, + max_tries=10, + ) + _restored_node_count(cluster.reload)() + + +def test_handle_dev_instance(capsys, dev_instance): + with pytest.raises(ValueError): + scale_bigtable(BIGTABLE_DEV_INSTANCE, BIGTABLE_DEV_INSTANCE, True) + + +@patch("time.sleep") +@patch("metricscaler.get_storage_utilization") +@patch("metricscaler.get_cpu_load") +@patch("metricscaler.scale_bigtable") +def test_main(scale_bigtable, get_cpu_load, get_storage_utilization, sleep): + SHORT_SLEEP = 5 + LONG_SLEEP = 10 + + # Test okay CPU, okay storage utilization + get_cpu_load.return_value = 0.5 + get_storage_utilization.return_value = 0.5 + + main(BIGTABLE_INSTANCE, BIGTABLE_INSTANCE, 0.6, 0.3, 0.6, SHORT_SLEEP, LONG_SLEEP) + scale_bigtable.assert_not_called() + scale_bigtable.reset_mock() + + # Test high CPU, okay storage utilization + get_cpu_load.return_value = 0.7 + get_storage_utilization.return_value = 0.5 + main(BIGTABLE_INSTANCE, BIGTABLE_INSTANCE, 0.6, 0.3, 0.6, SHORT_SLEEP, LONG_SLEEP) + scale_bigtable.assert_called_once_with(BIGTABLE_INSTANCE, BIGTABLE_INSTANCE, True) + scale_bigtable.reset_mock() + + # Test low CPU, okay storage utilization + get_storage_utilization.return_value = 0.5 + get_cpu_load.return_value = 0.2 + main(BIGTABLE_INSTANCE, BIGTABLE_INSTANCE, 0.6, 0.3, 0.6, SHORT_SLEEP, LONG_SLEEP) + scale_bigtable.assert_called_once_with(BIGTABLE_INSTANCE, BIGTABLE_INSTANCE, False) + scale_bigtable.reset_mock() + + # Test okay CPU, high storage utilization + get_cpu_load.return_value = 0.5 + get_storage_utilization.return_value = 0.7 + + main(BIGTABLE_INSTANCE, BIGTABLE_INSTANCE, 0.6, 0.3, 0.6, SHORT_SLEEP, LONG_SLEEP) + scale_bigtable.assert_called_once_with(BIGTABLE_INSTANCE, BIGTABLE_INSTANCE, True) + scale_bigtable.reset_mock() + + # Test high CPU, high storage utilization + get_cpu_load.return_value = 0.7 + get_storage_utilization.return_value = 0.7 + main(BIGTABLE_INSTANCE, BIGTABLE_INSTANCE, 0.6, 0.3, 0.6, SHORT_SLEEP, LONG_SLEEP) + scale_bigtable.assert_called_once_with(BIGTABLE_INSTANCE, BIGTABLE_INSTANCE, True) + scale_bigtable.reset_mock() + + # Test low CPU, high storage utilization + get_cpu_load.return_value = 0.2 + get_storage_utilization.return_value = 0.7 + main(BIGTABLE_INSTANCE, BIGTABLE_INSTANCE, 0.6, 0.3, 0.6, SHORT_SLEEP, LONG_SLEEP) + scale_bigtable.assert_called_once_with(BIGTABLE_INSTANCE, BIGTABLE_INSTANCE, True) + scale_bigtable.reset_mock() + + +if __name__ == "__main__": + test_get_cpu_load() diff --git a/packages/google-cloud-bigtable/samples/metricscaler/noxfile.py b/packages/google-cloud-bigtable/samples/metricscaler/noxfile.py new file mode 100644 index 000000000000..a169b5b5b464 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/metricscaler/noxfile.py @@ -0,0 +1,292 @@ +# Copyright 2019 Google LLC +# +# 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 __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, Optional + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in ( + "True", + "true", +) + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8") + else: + session.install("flake8", "flake8-annotations") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# format = isort + black +# + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list.extend(glob.glob("**/tests", recursive=True)) + + if len(test_list) == 0: + print("No tests found, skipping directory.") + return + + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + concurrent_args = [] + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + with open("requirements.txt") as rfile: + packages = rfile.read() + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install( + "-r", "requirements-test.txt", "-c", "constraints-test.txt" + ) + else: + session.install("-r", "requirements-test.txt") + with open("requirements-test.txt") as rtfile: + packages += rtfile.read() + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + if "pytest-parallel" in packages: + concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + elif "pytest-xdist" in packages: + concurrent_args.extend(['-n', 'auto']) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs + concurrent_args), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/packages/google-cloud-bigtable/samples/metricscaler/noxfile_config.py b/packages/google-cloud-bigtable/samples/metricscaler/noxfile_config.py new file mode 100644 index 000000000000..8a2d55bea291 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/metricscaler/noxfile_config.py @@ -0,0 +1,39 @@ +# Copyright 2021 Google LLC +# +# 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. + +# Default TEST_CONFIG_OVERRIDE for python repos. + +# You can copy this file into your directory, then it will be imported from +# the noxfile.py. + +# The source of truth: +# https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/noxfile_config.py + +TEST_CONFIG_OVERRIDE = { + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "BUILD_SPECIFIC_GCLOUD_PROJECT", + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} diff --git a/packages/google-cloud-bigtable/samples/metricscaler/requirements-test.txt b/packages/google-cloud-bigtable/samples/metricscaler/requirements-test.txt new file mode 100644 index 000000000000..d11108b81f7c --- /dev/null +++ b/packages/google-cloud-bigtable/samples/metricscaler/requirements-test.txt @@ -0,0 +1,3 @@ +pytest +mock==5.2.0 +google-cloud-testutils diff --git a/packages/google-cloud-bigtable/samples/metricscaler/requirements.txt b/packages/google-cloud-bigtable/samples/metricscaler/requirements.txt new file mode 100644 index 000000000000..257fd1ef67aa --- /dev/null +++ b/packages/google-cloud-bigtable/samples/metricscaler/requirements.txt @@ -0,0 +1,2 @@ +google-cloud-bigtable==2.35.0 +google-cloud-monitoring==2.29.0 diff --git a/packages/google-cloud-bigtable/samples/quickstart/README.md b/packages/google-cloud-bigtable/samples/quickstart/README.md new file mode 100644 index 000000000000..f61000e135d0 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/quickstart/README.md @@ -0,0 +1,52 @@ +[//]: # "This README.md file is auto-generated, all changes to this file will be lost." +[//]: # "To regenerate it, use `python -m synthtool`." + +## Python Samples for Cloud Bigtable + +This directory contains samples for Cloud Bigtable, which may be used as a refererence for how to use this product. +Samples, quickstarts, and other documentation are available at cloud.google.com. + + +### Quickstart + +Demonstrates of Cloud Bigtable. This sample creates a Bigtable client, connects to an instance and then to a table, then closes the connection. + + +Open in Cloud Shell + + +To run this sample: + +1. If this is your first time working with GCP products, you will need to set up [the Cloud SDK][cloud_sdk] or utilize [Google Cloud Shell][gcloud_shell]. This sample may [require authetication][authentication] and you will need to [enable billing][enable_billing]. + +1. Make a fork of this repo and clone the branch locally, then navigate to the sample directory you want to use. + +1. Install the dependencies needed to run the samples. + + pip install -r requirements.txt + +1. Run the sample using + + python main.py + + + +
usage: main.py [-h] [--table TABLE] project_id instance_id 


positional arguments:
  project_id     Your Cloud Platform project ID.
  instance_id    ID of the Cloud Bigtable instance to connect to.


optional arguments:
  -h, --help     show this help message and exit
  --table TABLE  Existing table used in the quickstart. (default: my-table)
+ +## Additional Information + +You can read the documentation for more details on API usage and use GitHub +to browse the source and [report issues][issues]. + +### Contributing +View the [contributing guidelines][contrib_guide], the [Python style guide][py_style] for more information. + +[authentication]: https://cloud.google.com/docs/authentication/getting-started +[enable_billing]:https://cloud.google.com/apis/docs/getting-started#enabling_billing +[client_library_python]: https://googlecloudplatform.github.io/google-cloud-python/ +[issues]: https://github.com/GoogleCloudPlatform/google-cloud-python/issues +[contrib_guide]: https://github.com/googleapis/google-cloud-python/blob/main/CONTRIBUTING.rst +[py_style]: http://google.github.io/styleguide/pyguide.html +[cloud_sdk]: https://cloud.google.com/sdk/docs +[gcloud_shell]: https://cloud.google.com/shell/docs +[gcloud_shell]: https://cloud.google.com/shell/docs diff --git a/packages/google-cloud-bigtable/samples/quickstart/__init__.py b/packages/google-cloud-bigtable/samples/quickstart/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/samples/quickstart/diff b/packages/google-cloud-bigtable/samples/quickstart/diff new file mode 100644 index 000000000000..7d87fc52547e --- /dev/null +++ b/packages/google-cloud-bigtable/samples/quickstart/diff @@ -0,0 +1,3534 @@ +diff --git a/docs/classic_client/snippets.py b/docs/classic_client/snippets.py +index fa3aa362..f0558854 100644 +--- a/docs/classic_client/snippets.py ++++ b/docs/classic_client/snippets.py +@@ -57,8 +57,8 @@ SERVER_NODES = 3 + STORAGE_TYPE = enums.StorageType.SSD + LABEL_KEY = "python-snippet" + LABEL_STAMP = ( +- datetime.datetime.utcnow() +- .replace(microsecond=0, tzinfo=UTC) ++ datetime.datetime.now(datetime.timezone.utc) ++ .replace(microsecond=0) + .strftime("%Y-%m-%dt%H-%M-%S") + ) + LABELS = {LABEL_KEY: str(LABEL_STAMP)} +diff --git a/docs/classic_client/snippets_table.py b/docs/classic_client/snippets_table.py +index 89313527..0217c530 100644 +--- a/docs/classic_client/snippets_table.py ++++ b/docs/classic_client/snippets_table.py +@@ -37,7 +37,6 @@ from google.api_core.exceptions import ServiceUnavailable + from test_utils.system import unique_resource_id + from test_utils.retry import RetryErrors + +-from google.cloud._helpers import UTC + from google.cloud.bigtable import Client + from google.cloud.bigtable import enums + from google.cloud.bigtable import column_family +@@ -54,8 +53,8 @@ SERVER_NODES = 3 + STORAGE_TYPE = enums.StorageType.SSD + LABEL_KEY = "python-snippet" + LABEL_STAMP = ( +- datetime.datetime.utcnow() +- .replace(microsecond=0, tzinfo=UTC) ++ datetime.datetime.now(datetime.timezone.utc) ++ .replace(microsecond=0) + .strftime("%Y-%m-%dt%H-%M-%S") + ) + LABELS = {LABEL_KEY: str(LABEL_STAMP)} +@@ -179,7 +178,7 @@ def test_bigtable_write_read_drop_truncate(): + value = "value_{}".format(i).encode() + row = table.row(row_key) + row.set_cell( +- COLUMN_FAMILY_ID, col_name, value, timestamp=datetime.datetime.utcnow() ++ COLUMN_FAMILY_ID, col_name, value, timestamp=datetime.datetime.now(datetime.timezone.utc) + ) + rows.append(row) + response = table.mutate_rows(rows) +@@ -270,7 +269,7 @@ def test_bigtable_mutations_batcher(): + row_key = row_keys[0] + row = table.row(row_key) + row.set_cell( +- COLUMN_FAMILY_ID, column_name, "value-0", timestamp=datetime.datetime.utcnow() ++ COLUMN_FAMILY_ID, column_name, "value-0", timestamp=datetime.datetime.now(datetime.timezone.utc) + ) + batcher.mutate(row) + # Add a collections of rows +@@ -279,7 +278,7 @@ def test_bigtable_mutations_batcher(): + row = table.row(row_keys[i]) + value = "value_{}".format(i).encode() + row.set_cell( +- COLUMN_FAMILY_ID, column_name, value, timestamp=datetime.datetime.utcnow() ++ COLUMN_FAMILY_ID, column_name, value, timestamp=datetime.datetime.now(datetime.timezone.utc) + ) + rows.append(row) + batcher.mutate_rows(rows) +@@ -759,7 +758,7 @@ def test_bigtable_batcher_mutate_flush_mutate_rows(): + row_key = b"row_key_1" + row = table.row(row_key) + row.set_cell( +- COLUMN_FAMILY_ID, COL_NAME1, "value-0", timestamp=datetime.datetime.utcnow() ++ COLUMN_FAMILY_ID, COL_NAME1, "value-0", timestamp=datetime.datetime.now(datetime.timezone.utc) + ) + + # In batcher, mutate will flush current batch if it +@@ -967,12 +966,12 @@ def test_bigtable_row_data_cells_cell_value_cell_values(): + value = b"value_in_col1" + row = Config.TABLE.row(b"row_key_1") + row.set_cell( +- COLUMN_FAMILY_ID, COL_NAME1, value, timestamp=datetime.datetime.utcnow() ++ COLUMN_FAMILY_ID, COL_NAME1, value, timestamp=datetime.datetime.now(datetime.timezone.utc) + ) + row.commit() + + row.set_cell( +- COLUMN_FAMILY_ID, COL_NAME1, value, timestamp=datetime.datetime.utcnow() ++ COLUMN_FAMILY_ID, COL_NAME1, value, timestamp=datetime.datetime.now(datetime.timezone.utc) + ) + row.commit() + +@@ -1050,7 +1049,7 @@ def test_bigtable_row_setcell_rowkey(): + + cell_val = b"cell-val" + row.set_cell( +- COLUMN_FAMILY_ID, COL_NAME1, cell_val, timestamp=datetime.datetime.utcnow() ++ COLUMN_FAMILY_ID, COL_NAME1, cell_val, timestamp=datetime.datetime.now(datetime.timezone.utc) + ) + # [END bigtable_api_row_set_cell] + +diff --git a/google/cloud/bigtable/cluster.py b/google/cloud/bigtable/cluster.py +index 967ec707..11fb5492 100644 +--- a/google/cloud/bigtable/cluster.py ++++ b/google/cloud/bigtable/cluster.py +@@ -511,11 +511,9 @@ class Cluster(object): + def _to_pb(self): + """Create cluster proto buff message for API calls""" + client = self._instance._client +- location = None +- if self.location_id: +- location = client.instance_admin_client.common_location_path( +- client.project, self.location_id +- ) ++ location = client.instance_admin_client.common_location_path( ++ client.project, self.location_id ++ ) + + cluster_pb = instance.Cluster( + location=location, +diff --git a/google/cloud/bigtable/data/_async/client.py b/google/cloud/bigtable/data/_async/client.py +index f86c886f..54a41036 100644 +--- a/google/cloud/bigtable/data/_async/client.py ++++ b/google/cloud/bigtable/data/_async/client.py +@@ -88,7 +88,6 @@ from google.cloud.bigtable.data.row_filters import RowFilter + from google.cloud.bigtable.data.row_filters import StripValueTransformerFilter + from google.cloud.bigtable.data.row_filters import CellsRowLimitFilter + from google.cloud.bigtable.data.row_filters import RowFilterChain +-from google.cloud.bigtable.data._metrics import BigtableClientSideMetricsController + + from google.cloud.bigtable.data._cross_sync import CrossSync + +@@ -1040,8 +1039,6 @@ class _DataApiTargetAsync(abc.ABC): + default_retryable_errors or () + ) + +- self._metrics = BigtableClientSideMetricsController() +- + try: + self._register_instance_future = CrossSync.create_task( + self.client._register_instance, +@@ -1756,7 +1753,6 @@ class _DataApiTargetAsync(abc.ABC): + """ + Called to close the Table instance and release any resources held by it. + """ +- self._metrics.close() + if self._register_instance_future: + self._register_instance_future.cancel() + self.client._remove_instance_registration( +diff --git a/google/cloud/bigtable/data/_async/metrics_interceptor.py b/google/cloud/bigtable/data/_async/metrics_interceptor.py +index 249dcdcc..a154c008 100644 +--- a/google/cloud/bigtable/data/_async/metrics_interceptor.py ++++ b/google/cloud/bigtable/data/_async/metrics_interceptor.py +@@ -13,21 +13,11 @@ + # limitations under the License. + from __future__ import annotations + +-from typing import Sequence +- +-import time +-from functools import wraps +- +-from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric +-from google.cloud.bigtable.data._metrics.data_model import OperationState +-from google.cloud.bigtable.data._metrics.data_model import OperationType +- + from google.cloud.bigtable.data._cross_sync import CrossSync + + if CrossSync.is_async: + from grpc.aio import UnaryUnaryClientInterceptor + from grpc.aio import UnaryStreamClientInterceptor +- from grpc.aio import AioRpcError + else: + from grpc import UnaryUnaryClientInterceptor + from grpc import UnaryStreamClientInterceptor +@@ -36,57 +26,6 @@ else: + __CROSS_SYNC_OUTPUT__ = "google.cloud.bigtable.data._sync_autogen.metrics_interceptor" + + +-def _with_active_operation(func): +- """ +- Decorator for interceptor methods to extract the active operation associated with the +- in-scope contextvars, and pass it to the decorated function. +- """ +- +- @wraps(func) +- def wrapper(self, continuation, client_call_details, request): +- operation: ActiveOperationMetric | None = ActiveOperationMetric.from_context() +- +- if operation: +- # start a new attempt if not started +- if ( +- operation.state == OperationState.CREATED +- or operation.state == OperationState.BETWEEN_ATTEMPTS +- ): +- operation.start_attempt() +- # wrap continuation in logic to process the operation +- return func(self, operation, continuation, client_call_details, request) +- else: +- # if operation not found, return unwrapped continuation +- return continuation(client_call_details, request) +- +- return wrapper +- +- +-@CrossSync.convert +-async def _get_metadata(source) -> dict[str, str | bytes] | None: +- """Helper to extract metadata from a call or RpcError""" +- try: +- metadata: Sequence[tuple[str, str | bytes]] +- if CrossSync.is_async: +- # grpc.aio returns metadata in Metadata objects +- if isinstance(source, AioRpcError): +- metadata = list(source.trailing_metadata()) + list( +- source.initial_metadata() +- ) +- else: +- metadata = list(await source.trailing_metadata()) + list( +- await source.initial_metadata() +- ) +- else: +- # sync grpc returns metadata as a sequence of tuples +- metadata = source.trailing_metadata() + source.initial_metadata() +- # convert metadata to dict format +- return {k: v for (k, v) in metadata} +- except Exception: +- # ignore errors while fetching metadata +- return None +- +- + @CrossSync.convert_class(sync_name="BigtableMetricsInterceptor") + class AsyncBigtableMetricsInterceptor( + UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor +@@ -96,33 +35,21 @@ class AsyncBigtableMetricsInterceptor( + """ + + @CrossSync.convert +- @_with_active_operation +- async def intercept_unary_unary( +- self, operation, continuation, client_call_details, request +- ): ++ async def intercept_unary_unary(self, continuation, client_call_details, request): + """ + Interceptor for unary rpcs: + - MutateRow + - CheckAndMutateRow + - ReadModifyWriteRow + """ +- metadata = None + try: + call = await continuation(client_call_details, request) +- metadata = await _get_metadata(call) + return call + except Exception as rpc_error: +- metadata = await _get_metadata(rpc_error) + raise rpc_error +- finally: +- if metadata is not None: +- operation.add_response_metadata(metadata) + + @CrossSync.convert +- @_with_active_operation +- async def intercept_unary_stream( +- self, operation, continuation, client_call_details, request +- ): ++ async def intercept_unary_stream(self, continuation, client_call_details, request): + """ + Interceptor for streaming rpcs: + - ReadRows +@@ -131,42 +58,21 @@ class AsyncBigtableMetricsInterceptor( + """ + try: + return self._streaming_generator_wrapper( +- operation, await continuation(client_call_details, request) ++ await continuation(client_call_details, request) + ) + except Exception as rpc_error: + # handle errors while intializing stream +- metadata = await _get_metadata(rpc_error) +- if metadata is not None: +- operation.add_response_metadata(metadata) + raise rpc_error + + @staticmethod + @CrossSync.convert +- async def _streaming_generator_wrapper(operation, call): ++ async def _streaming_generator_wrapper(call): + """ + Wrapped generator to be returned by intercept_unary_stream. + """ +- # only track has_first response for READ_ROWS +- has_first_response = ( +- operation.first_response_latency_ns is not None +- or operation.op_type != OperationType.READ_ROWS +- ) +- encountered_exc = None + try: + async for response in call: +- # record time to first response. Currently only used for READ_ROWs +- if not has_first_response: +- operation.first_response_latency_ns = ( +- time.monotonic_ns() - operation.start_time_ns +- ) +- has_first_response = True + yield response + except Exception as e: + # handle errors while processing stream +- encountered_exc = e +- raise +- finally: +- if call is not None: +- metadata = await _get_metadata(encountered_exc or call) +- if metadata is not None: +- operation.add_response_metadata(metadata) ++ raise e +diff --git a/google/cloud/bigtable/data/_helpers.py b/google/cloud/bigtable/data/_helpers.py +index e848ebc6..424a3448 100644 +--- a/google/cloud/bigtable/data/_helpers.py ++++ b/google/cloud/bigtable/data/_helpers.py +@@ -23,7 +23,6 @@ from collections import namedtuple + from google.cloud.bigtable.data.read_rows_query import ReadRowsQuery + + from google.api_core import exceptions as core_exceptions +-from google.api_core.retry import exponential_sleep_generator + from google.api_core.retry import RetryFailureReason + from google.cloud.bigtable.data.exceptions import RetryExceptionGroup + +@@ -249,61 +248,3 @@ def _get_retryable_errors( + call_codes = table.default_mutate_rows_retryable_errors + + return [_get_error_type(e) for e in call_codes] +- +- +-class TrackedBackoffGenerator: +- """ +- Generator class for exponential backoff sleep times. +- This implementation builds on top of api_core.retries.exponential_sleep_generator, +- adding the ability to retrieve previous values using get_attempt_backoff(idx). +- This is used by the Metrics class to track the sleep times used for each attempt. +- """ +- +- def __init__(self, initial=0.01, maximum=60, multiplier=2): +- self.history = [] +- self.subgenerator = exponential_sleep_generator( +- initial=initial, maximum=maximum, multiplier=multiplier +- ) +- self._next_override: float | None = None +- +- def __iter__(self): +- return self +- +- def set_next(self, next_value: float): +- """ +- Set the next backoff value, instead of generating one from subgenerator. +- After the value is yielded, it will go back to using self.subgenerator. +- +- If set_next is called twice before the next() is called, only the latest +- value will be used and others discarded +- +- Args: +- next_value: the upcomming value to yield when next() is called +- Raises: +- ValueError: if next_value is negative +- """ +- if next_value < 0: +- raise ValueError("backoff value cannot be less than 0") +- self._next_override = next_value +- +- def __next__(self) -> float: +- if self._next_override is not None: +- next_backoff = self._next_override +- self._next_override = None +- else: +- next_backoff = next(self.subgenerator) +- self.history.append(next_backoff) +- return next_backoff +- +- def get_attempt_backoff(self, attempt_idx) -> float: +- """ +- returns the backoff time for a specific attempt index, starting at 0. +- +- Args: +- attempt_idx: the index of the attempt to return backoff for +- Raises: +- IndexError: if attempt_idx is negative, or not in history +- """ +- if attempt_idx < 0: +- raise IndexError("received negative attempt number") +- return self.history[attempt_idx] +diff --git a/google/cloud/bigtable/data/_metrics/__init__.py b/google/cloud/bigtable/data/_metrics/__init__.py +deleted file mode 100644 +index 26cfc132..00000000 +--- a/google/cloud/bigtable/data/_metrics/__init__.py ++++ /dev/null +@@ -1,35 +0,0 @@ +-# Copyright 2023 Google LLC +-# +-# 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.bigtable.data._metrics.metrics_controller import ( +- BigtableClientSideMetricsController, +-) +- +-from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric +-from google.cloud.bigtable.data._metrics.data_model import ActiveAttemptMetric +-from google.cloud.bigtable.data._metrics.data_model import CompletedOperationMetric +-from google.cloud.bigtable.data._metrics.data_model import CompletedAttemptMetric +-from google.cloud.bigtable.data._metrics.data_model import OperationState +-from google.cloud.bigtable.data._metrics.data_model import OperationType +-from google.cloud.bigtable.data._metrics.tracked_retry import tracked_retry +- +-__all__ = ( +- "BigtableClientSideMetricsController", +- "OperationType", +- "OperationState", +- "ActiveOperationMetric", +- "ActiveAttemptMetric", +- "CompletedOperationMetric", +- "CompletedAttemptMetric", +- "tracked_retry", +-) +diff --git a/google/cloud/bigtable/data/_metrics/data_model.py b/google/cloud/bigtable/data/_metrics/data_model.py +deleted file mode 100644 +index 64dd63bf..00000000 +--- a/google/cloud/bigtable/data/_metrics/data_model.py ++++ /dev/null +@@ -1,469 +0,0 @@ +-# Copyright 2023 Google LLC +-# +-# 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 __future__ import annotations +- +-from typing import ClassVar, Tuple, cast, TYPE_CHECKING +- +-import time +-import re +-import logging +-import contextvars +- +-from enum import Enum +-from functools import lru_cache +-from dataclasses import dataclass +-from dataclasses import field +-from grpc import StatusCode +-from grpc import RpcError +-from grpc.aio import AioRpcError +- +-import google.cloud.bigtable.data.exceptions as bt_exceptions +-from google.cloud.bigtable_v2.types.response_params import ResponseParams +-from google.cloud.bigtable.data._helpers import TrackedBackoffGenerator +-from google.protobuf.message import DecodeError +- +-if TYPE_CHECKING: +- from google.cloud.bigtable.data._metrics.handlers._base import MetricsHandler +- +- +-LOGGER = logging.getLogger(__name__) +- +-# default values for zone and cluster data, if not captured +-DEFAULT_ZONE = "global" +-DEFAULT_CLUSTER_ID = "" +- +-# keys for parsing metadata blobs +-BIGTABLE_LOCATION_METADATA_KEY = "x-goog-ext-425905942-bin" +-SERVER_TIMING_METADATA_KEY = "server-timing" +-SERVER_TIMING_REGEX = re.compile(r".*gfet4t7;\s*dur=(\d+\.?\d*).*") +- +-INVALID_STATE_ERROR = "Invalid state for {}: {}" +- +- +-class OperationType(Enum): +- """Enum for the type of operation being performed.""" +- +- READ_ROWS = "ReadRows" +- SAMPLE_ROW_KEYS = "SampleRowKeys" +- BULK_MUTATE_ROWS = "MutateRows" +- MUTATE_ROW = "MutateRow" +- CHECK_AND_MUTATE = "CheckAndMutateRow" +- READ_MODIFY_WRITE = "ReadModifyWriteRow" +- +- +-class OperationState(Enum): +- """Enum for the state of the active operation. +- +- ┌───────────┐ +- │ CREATED │────────┐ +- └─────┬─────┘ │ +- │ │ +- ▼ │ +- ┌▶ ACTIVE_ATTEMPT ───┐│ +- │ │ ││ +- │ ▼ ││ +- └─ BETWEEN_ATTEMPTS ││ +- │ ││ +- ▼ ││ +- ┌───────────┐ ││ +- │ COMPLETED │ ◀─────┘│ +- └───────────┘ ◀──────┘ +- """ +- +- CREATED = 0 +- ACTIVE_ATTEMPT = 1 +- BETWEEN_ATTEMPTS = 2 +- COMPLETED = 3 +- +- +-@dataclass(frozen=True) +-class CompletedAttemptMetric: +- """ +- An immutable dataclass representing the data associated with a +- completed rpc attempt. +- +- Operation-level fields (eg. type, cluster, zone) are stored on the +- corresponding CompletedOperationMetric or ActiveOperationMetric object. +- """ +- +- duration_ns: int +- end_status: StatusCode +- gfe_latency_ns: int | None = None +- application_blocking_time_ns: int = 0 +- backoff_before_attempt_ns: int = 0 +- +- +-@dataclass(frozen=True) +-class CompletedOperationMetric: +- """ +- An immutable dataclass representing the data associated with a +- completed rpc operation. +- +- Attempt-level fields (eg. duration, latencies, etc) are stored on the +- corresponding CompletedAttemptMetric object. +- """ +- +- op_type: OperationType +- duration_ns: int +- completed_attempts: list[CompletedAttemptMetric] +- final_status: StatusCode +- cluster_id: str +- zone: str +- is_streaming: bool +- first_response_latency_ns: int | None = None +- flow_throttling_time_ns: int = 0 +- +- +-@dataclass +-class ActiveAttemptMetric: +- """ +- A dataclass representing the data associated with an rpc attempt that is +- currently in progress. Fields are mutable and may be optional. +- """ +- +- # keep monotonic timestamps for active attempts +- start_time_ns: int = field(default_factory=lambda: time.monotonic_ns()) +- # the time taken by the backend, in nanoseconds. Taken from response header +- gfe_latency_ns: int | None = None +- # time waiting on user to process the response, in nanoseconds +- # currently only relevant for ReadRows +- application_blocking_time_ns: int = 0 +- # backoff time is added to application_blocking_time_ns +- backoff_before_attempt_ns: int = 0 +- +- +-@dataclass +-class ActiveOperationMetric: +- """ +- A dataclass representing the data associated with an rpc operation that is +- currently in progress. Fields are mutable and may be optional. +- """ +- +- op_type: OperationType +- state: OperationState = OperationState.CREATED +- # create a default backoff generator, initialized with standard default backoff values +- backoff_generator: TrackedBackoffGenerator = field( +- default_factory=lambda: TrackedBackoffGenerator( +- initial=0.01, maximum=60, multiplier=2 +- ) +- ) +- # keep monotonic timestamps for active operations +- start_time_ns: int = field(default_factory=lambda: time.monotonic_ns()) +- active_attempt: ActiveAttemptMetric | None = None +- cluster_id: str | None = None +- zone: str | None = None +- completed_attempts: list[CompletedAttemptMetric] = field(default_factory=list) +- is_streaming: bool = False # only True for read_rows operations +- handlers: list[MetricsHandler] = field(default_factory=list) +- # the time it takes to recieve the first response from the server, in nanoseconds +- # attached by interceptor +- # currently only tracked for ReadRows +- first_response_latency_ns: int | None = None +- # time waiting on flow control, in nanoseconds +- flow_throttling_time_ns: int = 0 +- +- _active_operation_context: ClassVar[ +- contextvars.ContextVar[ActiveOperationMetric] +- ] = contextvars.ContextVar("active_operation_context") +- +- @classmethod +- def from_context(cls) -> ActiveOperationMetric | None: +- """Retrieves the active operation from the current execution context. +- +- Because execution within a context is sequential, this guarantees +- retrieval of the single, unique operation, isolated from other +- concurrent RPCs. +- +- Note: +- This is intended to be called by gRPC interceptors at the start +- of an RPC. +- +- Returns: +- ActiveOperationMetric: The current active operation. +- None: If no operation is set, or if the current operation is +- already in the `COMPLETED` state. +- """ +- op = cls._active_operation_context.get(None) +- if op and op.state == OperationState.COMPLETED: +- return None +- return op +- +- def __post_init__(self): +- """ +- Save new instances to contextvars on init +- """ +- self._active_operation_context.set(self) +- +- def start(self) -> None: +- """ +- Optionally called to mark the start of the operation. If not called, +- the operation will be started at initialization. +- +- StartState: CREATED +- EndState: CREATED +- """ +- if self.state != OperationState.CREATED: +- return self._handle_error(INVALID_STATE_ERROR.format("start", self.state)) +- self.start_time_ns = time.monotonic_ns() +- # set as active operation in contextvars +- self._active_operation_context.set(self) +- +- def start_attempt(self) -> ActiveAttemptMetric | None: +- """ +- Called to initiate a new attempt for the operation. +- +- StartState: CREATED | BETWEEN_ATTEMPTS +- EndState: ACTIVE_ATTEMPT +- """ +- if ( +- self.state != OperationState.BETWEEN_ATTEMPTS +- and self.state != OperationState.CREATED +- ): +- return self._handle_error( +- INVALID_STATE_ERROR.format("start_attempt", self.state) +- ) +- # set as active operation in contextvars +- self._active_operation_context.set(self) +- +- try: +- # find backoff value before this attempt +- prev_attempt_idx = len(self.completed_attempts) - 1 +- backoff = self.backoff_generator.get_attempt_backoff(prev_attempt_idx) +- # generator will return the backoff time in seconds, so convert to nanoseconds +- backoff_ns = int(backoff * 1e9) +- except IndexError: +- # backoff value not found +- backoff_ns = 0 +- +- self.active_attempt = ActiveAttemptMetric(backoff_before_attempt_ns=backoff_ns) +- self.state = OperationState.ACTIVE_ATTEMPT +- return self.active_attempt +- +- def add_response_metadata(self, metadata: dict[str, bytes | str]) -> None: +- """ +- Attach trailing metadata to the active attempt. +- +- If not called, default values for the metadata will be used. +- +- StartState: ACTIVE_ATTEMPT +- EndState: ACTIVE_ATTEMPT +- +- Args: +- - metadata: the metadata as extracted from the grpc call +- """ +- if self.state != OperationState.ACTIVE_ATTEMPT: +- return self._handle_error( +- INVALID_STATE_ERROR.format("add_response_metadata", self.state) +- ) +- if self.cluster_id is None or self.zone is None: +- # BIGTABLE_LOCATION_METADATA_KEY should give a binary-encoded ResponseParams proto +- blob = cast(bytes, metadata.get(BIGTABLE_LOCATION_METADATA_KEY)) +- if blob: +- parse_result = self._parse_response_metadata_blob(blob) +- if parse_result is not None: +- cluster, zone = parse_result +- if cluster: +- self.cluster_id = cluster +- if zone: +- self.zone = zone +- else: +- self._handle_error( +- f"Failed to decode {BIGTABLE_LOCATION_METADATA_KEY} metadata: {blob!r}" +- ) +- # SERVER_TIMING_METADATA_KEY should give a string with the server-latency headers +- timing_header = cast(str, metadata.get(SERVER_TIMING_METADATA_KEY)) +- if timing_header: +- timing_data = SERVER_TIMING_REGEX.match(timing_header) +- if timing_data and self.active_attempt: +- gfe_latency_ms = float(timing_data.group(1)) +- self.active_attempt.gfe_latency_ns = int(gfe_latency_ms * 1e6) +- +- @staticmethod +- @lru_cache(maxsize=32) +- def _parse_response_metadata_blob(blob: bytes) -> Tuple[str, str] | None: +- """ +- Parse the response metadata blob and return a tuple of cluster and zone. +- +- Function is cached to avoid parsing the same blob multiple times. +- +- Args: +- - blob: the metadata blob as extracted from the grpc call +- Returns: +- - a tuple of cluster_id and zone, or None if parsing failed +- """ +- try: +- proto = ResponseParams.pb().FromString(blob) +- return proto.cluster_id, proto.zone_id +- except (DecodeError, TypeError): +- # failed to parse metadata +- return None +- +- def end_attempt_with_status(self, status: StatusCode | BaseException) -> None: +- """ +- Called to mark the end of an attempt for the operation. +- +- Typically, this is used to mark a retryable error. If a retry will not +- be attempted, `end_with_status` or `end_with_success` should be used +- to finalize the operation along with the attempt. +- +- StartState: ACTIVE_ATTEMPT +- EndState: BETWEEN_ATTEMPTS +- +- Args: +- - status: The status of the attempt. +- """ +- if self.state != OperationState.ACTIVE_ATTEMPT or self.active_attempt is None: +- return self._handle_error( +- INVALID_STATE_ERROR.format("end_attempt_with_status", self.state) +- ) +- if isinstance(status, BaseException): +- status = self._exc_to_status(status) +- duration_ns = self._ensure_positive( +- time.monotonic_ns() - self.active_attempt.start_time_ns, "duration" +- ) +- complete_attempt = CompletedAttemptMetric( +- duration_ns=duration_ns, +- end_status=status, +- gfe_latency_ns=self.active_attempt.gfe_latency_ns, +- application_blocking_time_ns=self.active_attempt.application_blocking_time_ns, +- backoff_before_attempt_ns=self.active_attempt.backoff_before_attempt_ns, +- ) +- self.completed_attempts.append(complete_attempt) +- self.active_attempt = None +- self.state = OperationState.BETWEEN_ATTEMPTS +- for handler in self.handlers: +- handler.on_attempt_complete(complete_attempt, self) +- +- def end_with_status(self, status: StatusCode | BaseException) -> None: +- """ +- Called to mark the end of the operation. If there is an active attempt, +- end_attempt_with_status will be called with the same status. +- +- StartState: CREATED | ACTIVE_ATTEMPT | BETWEEN_ATTEMPTS +- EndState: COMPLETED +- +- Causes on_operation_completed to be called for each registered handler. +- +- Args: +- - status: The status of the operation. +- """ +- if self.state == OperationState.COMPLETED: +- return self._handle_error( +- INVALID_STATE_ERROR.format("end_with_status", self.state) +- ) +- final_status = ( +- self._exc_to_status(status) if isinstance(status, BaseException) else status +- ) +- if self.state == OperationState.ACTIVE_ATTEMPT: +- self.end_attempt_with_status(final_status) +- duration_ns = self._ensure_positive( +- time.monotonic_ns() - self.start_time_ns, "duration" +- ) +- finalized = CompletedOperationMetric( +- op_type=self.op_type, +- completed_attempts=self.completed_attempts, +- duration_ns=duration_ns, +- final_status=final_status, +- cluster_id=self.cluster_id or DEFAULT_CLUSTER_ID, +- zone=self.zone or DEFAULT_ZONE, +- is_streaming=self.is_streaming, +- first_response_latency_ns=self.first_response_latency_ns, +- flow_throttling_time_ns=self.flow_throttling_time_ns, +- ) +- self.state = OperationState.COMPLETED +- for handler in self.handlers: +- handler.on_operation_complete(finalized) +- +- def end_with_success(self): +- """ +- Called to mark the end of the operation with a successful status. +- +- StartState: CREATED | ACTIVE_ATTEMPT | BETWEEN_ATTEMPTS +- EndState: COMPLETED +- +- Causes on_operation_completed to be called for each registered handler. +- """ +- return self.end_with_status(StatusCode.OK) +- +- @staticmethod +- def _exc_to_status(exc: BaseException) -> StatusCode: +- """ +- Extracts the grpc status code from an exception. +- +- Exception groups and wrappers will be parsed to find the underlying +- grpc Exception. +- +- If the exception is not a grpc exception, will return StatusCode.UNKNOWN. +- +- Args: +- - exc: The exception to extract the status code from. +- """ +- if isinstance(exc, bt_exceptions._BigtableExceptionGroup): +- exc = exc.exceptions[-1] +- if hasattr(exc, "grpc_status_code") and exc.grpc_status_code is not None: +- return exc.grpc_status_code +- if ( +- exc.__cause__ +- and hasattr(exc.__cause__, "grpc_status_code") +- and exc.__cause__.grpc_status_code is not None +- ): +- return exc.__cause__.grpc_status_code +- if isinstance(exc, AioRpcError) or isinstance(exc, RpcError): +- return exc.code() +- return StatusCode.UNKNOWN +- +- @staticmethod +- def _handle_error(message: str) -> None: +- """ +- log error metric system error messages +- +- Args: +- - message: The message to include in the exception or warning. +- """ +- full_message = f"Error in Bigtable Metrics: {message}" +- LOGGER.warning(full_message) +- +- def _ensure_positive(self, value: int, field_name: str) -> int: +- """ +- Helper to replace negative value with 0, and record an error +- """ +- if value < 0: +- self._handle_error(f"received negative value for {field_name}: {value}") +- return 0 +- return value +- +- def __enter__(self): +- """ +- Implements the async manager protocol +- +- Using the operation's context manager provides assurances that the operation +- is always closed when complete, with the proper status code automaticallty +- detected when an exception is raised. +- """ +- return self +- +- def __exit__(self, exc_type, exc_val, exc_tb): +- """ +- Implements the context manager protocol +- +- The operation is automatically ended on exit, with the status determined +- by the exception type and value. +- +- If operation was already ended manually, do nothing. +- """ +- if not self.state == OperationState.COMPLETED: +- if exc_val is None: +- self.end_with_success() +- else: +- self.end_with_status(exc_val) +diff --git a/google/cloud/bigtable/data/_metrics/handlers/_base.py b/google/cloud/bigtable/data/_metrics/handlers/_base.py +deleted file mode 100644 +index 884091fd..00000000 +--- a/google/cloud/bigtable/data/_metrics/handlers/_base.py ++++ /dev/null +@@ -1,38 +0,0 @@ +-# Copyright 2023 Google LLC +-# +-# 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.bigtable.data._metrics.data_model import ActiveOperationMetric +-from google.cloud.bigtable.data._metrics.data_model import CompletedAttemptMetric +-from google.cloud.bigtable.data._metrics.data_model import CompletedOperationMetric +- +- +-class MetricsHandler: +- """ +- Base class for all metrics handlers. Metrics handlers will receive callbacks +- when operations and attempts are completed, and can use this information to +- update some external metrics system. +- """ +- +- def __init__(self, **kwargs): +- pass +- +- def on_operation_complete(self, op: CompletedOperationMetric) -> None: +- pass +- +- def on_attempt_complete( +- self, attempt: CompletedAttemptMetric, op: ActiveOperationMetric +- ) -> None: +- pass +- +- def close(self): +- pass +diff --git a/google/cloud/bigtable/data/_metrics/metrics_controller.py b/google/cloud/bigtable/data/_metrics/metrics_controller.py +deleted file mode 100644 +index e9815f20..00000000 +--- a/google/cloud/bigtable/data/_metrics/metrics_controller.py ++++ /dev/null +@@ -1,63 +0,0 @@ +-# Copyright 2023 Google LLC +-# +-# 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 __future__ import annotations +- +-from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric +-from google.cloud.bigtable.data._metrics.handlers._base import MetricsHandler +-from google.cloud.bigtable.data._metrics.data_model import OperationType +- +- +-class BigtableClientSideMetricsController: +- """ +- BigtableClientSideMetricsController is responsible for managing the +- lifecycle of the metrics system. The Bigtable client library will +- use this class to create new operations. Each operation will be +- registered with the handlers associated with this controller. +- """ +- +- def __init__( +- self, +- handlers: list[MetricsHandler] | None = None, +- ): +- """ +- Initializes the metrics controller. +- +- Args: +- - handlers: A list of MetricsHandler objects to subscribe to metrics events. +- """ +- self.handlers: list[MetricsHandler] = handlers or [] +- +- def add_handler(self, handler: MetricsHandler) -> None: +- """ +- Add a new handler to the list of handlers. +- +- Args: +- - handler: A MetricsHandler object to add to the list of subscribed handlers. +- """ +- self.handlers.append(handler) +- +- def create_operation( +- self, op_type: OperationType, **kwargs +- ) -> ActiveOperationMetric: +- """ +- Creates a new operation and registers it with the subscribed handlers. +- """ +- return ActiveOperationMetric(op_type, **kwargs, handlers=self.handlers) +- +- def close(self): +- """ +- Close all handlers. +- """ +- for handler in self.handlers: +- handler.close() +diff --git a/google/cloud/bigtable/data/_metrics/tracked_retry.py b/google/cloud/bigtable/data/_metrics/tracked_retry.py +deleted file mode 100644 +index 94d2e5dc..00000000 +--- a/google/cloud/bigtable/data/_metrics/tracked_retry.py ++++ /dev/null +@@ -1,133 +0,0 @@ +-# Copyright 2025 Google LLC +-# +-# 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. +-""" +-Methods for instrumenting an google.api_core.retry.retry_target or +-google.api_core.retry.retry_target_stream method +- +-`tracked_retry` will intercept `on_error` and `exception_factory` +-methods to update the associated ActiveOperationMetric when exceptions +-are encountered through the retryable rpc. +-""" +-from __future__ import annotations +- +-from typing import Callable, List, Optional, Tuple, TypeVar +- +-from grpc import StatusCode +-from google.api_core.exceptions import GoogleAPICallError +-from google.api_core.retry import RetryFailureReason +-from google.cloud.bigtable.data.exceptions import _MutateRowsIncomplete +-from google.cloud.bigtable.data._helpers import _retry_exception_factory +-from google.cloud.bigtable.data._metrics import ActiveOperationMetric +-from google.cloud.bigtable.data._metrics import OperationState +- +- +-T = TypeVar("T") +- +- +-ExceptionFactoryType = Callable[ +- [List[Exception], RetryFailureReason, Optional[float]], +- Tuple[Exception, Optional[Exception]], +-] +- +- +-def _track_retryable_error( +- operation: ActiveOperationMetric, +-) -> Callable[[Exception], None]: +- """ +- Used as input to api_core.Retry classes, to track when retryable errors are encountered +- +- Should be passed as on_error callback +- """ +- +- def wrapper(exc: Exception) -> None: +- try: +- # record metadata from failed rpc +- if isinstance(exc, GoogleAPICallError) and exc.errors: +- rpc_error = exc.errors[-1] +- metadata = list(rpc_error.trailing_metadata()) + list( +- rpc_error.initial_metadata() +- ) +- operation.add_response_metadata({k: v for k, v in metadata}) +- except Exception: +- # ignore errors in metadata collection +- pass +- if isinstance(exc, _MutateRowsIncomplete): +- # _MutateRowsIncomplete represents a successful rpc with some failed mutations +- # mark the attempt as successful +- operation.end_attempt_with_status(StatusCode.OK) +- else: +- operation.end_attempt_with_status(exc) +- +- return wrapper +- +- +-def _track_terminal_error( +- operation: ActiveOperationMetric, exception_factory: ExceptionFactoryType +-) -> ExceptionFactoryType: +- """ +- Used as input to api_core.Retry classes, to track when terminal errors are encountered +- +- Should be used as a wrapper over an exception_factory callback +- """ +- +- def wrapper( +- exc_list: List[Exception], +- reason: RetryFailureReason, +- timeout_val: float | None, +- ) -> tuple[Exception, Exception | None]: +- source_exc, cause_exc = exception_factory(exc_list, reason, timeout_val) +- try: +- # record metadata from failed rpc +- if isinstance(source_exc, GoogleAPICallError) and source_exc.errors: +- rpc_error = source_exc.errors[-1] +- metadata = list(rpc_error.trailing_metadata()) + list( +- rpc_error.initial_metadata() +- ) +- operation.add_response_metadata({k: v for k, v in metadata}) +- except Exception: +- # ignore errors in metadata collection +- pass +- if ( +- reason == RetryFailureReason.TIMEOUT +- and operation.state == OperationState.ACTIVE_ATTEMPT +- and exc_list +- ): +- # record ending attempt for timeout failures +- attempt_exc = exc_list[-1] +- _track_retryable_error(operation)(attempt_exc) +- operation.end_with_status(source_exc) +- return source_exc, cause_exc +- +- return wrapper +- +- +-def tracked_retry( +- *, +- retry_fn: Callable[..., T], +- operation: ActiveOperationMetric, +- **kwargs, +-) -> T: +- """ +- Wrapper for retry_rarget or retry_target_stream, which injects methods to +- track the lifecycle of the retry using the provided ActiveOperationMetric +- """ +- in_exception_factory = kwargs.pop("exception_factory", _retry_exception_factory) +- kwargs.pop("on_error", None) +- kwargs.pop("sleep_generator", None) +- return retry_fn( +- sleep_generator=operation.backoff_generator, +- on_error=_track_retryable_error(operation), +- exception_factory=_track_terminal_error(operation, in_exception_factory), +- **kwargs, +- ) +diff --git a/google/cloud/bigtable/data/_sync_autogen/client.py b/google/cloud/bigtable/data/_sync_autogen/client.py +index 62200276..6a4da007 100644 +--- a/google/cloud/bigtable/data/_sync_autogen/client.py ++++ b/google/cloud/bigtable/data/_sync_autogen/client.py +@@ -75,7 +75,6 @@ from google.cloud.bigtable.data.row_filters import RowFilter + from google.cloud.bigtable.data.row_filters import StripValueTransformerFilter + from google.cloud.bigtable.data.row_filters import CellsRowLimitFilter + from google.cloud.bigtable.data.row_filters import RowFilterChain +-from google.cloud.bigtable.data._metrics import BigtableClientSideMetricsController + from google.cloud.bigtable.data._cross_sync import CrossSync + from typing import Iterable + from grpc import insecure_channel +@@ -825,7 +824,6 @@ class _DataApiTarget(abc.ABC): + self.default_retryable_errors: Sequence[type[Exception]] = ( + default_retryable_errors or () + ) +- self._metrics = BigtableClientSideMetricsController() + try: + self._register_instance_future = CrossSync._Sync_Impl.create_task( + self.client._register_instance, +@@ -1483,7 +1481,6 @@ class _DataApiTarget(abc.ABC): + + def close(self): + """Called to close the Table instance and release any resources held by it.""" +- self._metrics.close() + if self._register_instance_future: + self._register_instance_future.cancel() + self.client._remove_instance_registration( +diff --git a/google/cloud/bigtable/data/_sync_autogen/metrics_interceptor.py b/google/cloud/bigtable/data/_sync_autogen/metrics_interceptor.py +index c5a59787..9e47313b 100644 +--- a/google/cloud/bigtable/data/_sync_autogen/metrics_interceptor.py ++++ b/google/cloud/bigtable/data/_sync_autogen/metrics_interceptor.py +@@ -15,46 +15,10 @@ + # This file is automatically generated by CrossSync. Do not edit manually. + + from __future__ import annotations +-from typing import Sequence +-import time +-from functools import wraps +-from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric +-from google.cloud.bigtable.data._metrics.data_model import OperationState +-from google.cloud.bigtable.data._metrics.data_model import OperationType + from grpc import UnaryUnaryClientInterceptor + from grpc import UnaryStreamClientInterceptor + + +-def _with_active_operation(func): +- """Decorator for interceptor methods to extract the active operation associated with the +- in-scope contextvars, and pass it to the decorated function.""" +- +- @wraps(func) +- def wrapper(self, continuation, client_call_details, request): +- operation: ActiveOperationMetric | None = ActiveOperationMetric.from_context() +- if operation: +- if ( +- operation.state == OperationState.CREATED +- or operation.state == OperationState.BETWEEN_ATTEMPTS +- ): +- operation.start_attempt() +- return func(self, operation, continuation, client_call_details, request) +- else: +- return continuation(client_call_details, request) +- +- return wrapper +- +- +-def _get_metadata(source) -> dict[str, str | bytes] | None: +- """Helper to extract metadata from a call or RpcError""" +- try: +- metadata: Sequence[tuple[str, str | bytes]] +- metadata = source.trailing_metadata() + source.initial_metadata() +- return {k: v for (k, v) in metadata} +- except Exception: +- return None +- +- + class BigtableMetricsInterceptor( + UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor + ): +@@ -62,65 +26,34 @@ class BigtableMetricsInterceptor( + An async gRPC interceptor to add client metadata and print server metadata. + """ + +- @_with_active_operation +- def intercept_unary_unary( +- self, operation, continuation, client_call_details, request +- ): ++ def intercept_unary_unary(self, continuation, client_call_details, request): + """Interceptor for unary rpcs: + - MutateRow + - CheckAndMutateRow + - ReadModifyWriteRow""" +- metadata = None + try: + call = continuation(client_call_details, request) +- metadata = _get_metadata(call) + return call + except Exception as rpc_error: +- metadata = _get_metadata(rpc_error) + raise rpc_error +- finally: +- if metadata is not None: +- operation.add_response_metadata(metadata) + +- @_with_active_operation +- def intercept_unary_stream( +- self, operation, continuation, client_call_details, request +- ): ++ def intercept_unary_stream(self, continuation, client_call_details, request): + """Interceptor for streaming rpcs: + - ReadRows + - MutateRows + - SampleRowKeys""" + try: + return self._streaming_generator_wrapper( +- operation, continuation(client_call_details, request) ++ continuation(client_call_details, request) + ) + except Exception as rpc_error: +- metadata = _get_metadata(rpc_error) +- if metadata is not None: +- operation.add_response_metadata(metadata) + raise rpc_error + + @staticmethod +- def _streaming_generator_wrapper(operation, call): ++ def _streaming_generator_wrapper(call): + """Wrapped generator to be returned by intercept_unary_stream.""" +- has_first_response = ( +- operation.first_response_latency_ns is not None +- or operation.op_type != OperationType.READ_ROWS +- ) +- encountered_exc = None + try: + for response in call: +- if not has_first_response: +- operation.first_response_latency_ns = ( +- time.monotonic_ns() - operation.start_time_ns +- ) +- has_first_response = True + yield response + except Exception as e: +- encountered_exc = e +- raise +- finally: +- if call is not None: +- metadata = _get_metadata(encountered_exc or call) +- if metadata is not None: +- operation.add_response_metadata(metadata) ++ raise e +diff --git a/noxfile.py b/noxfile.py +index 77f59b3c..29de5901 100644 +--- a/noxfile.py ++++ b/noxfile.py +@@ -517,7 +517,6 @@ def prerelease_deps(session, protobuf_implementation): + # Remaining dependencies + other_deps = [ + "requests", +- "cryptography", + ] + session.install(*other_deps) + +diff --git a/samples/hello/async_main.py b/samples/hello/async_main.py +index e134e28d..af95898e 100644 +--- a/samples/hello/async_main.py ++++ b/samples/hello/async_main.py +@@ -80,9 +80,6 @@ async def main(project_id, instance_id, table_id): + # sequential keys can result in poor distribution of operations + # across nodes. + # +- # We recommend that you use bytestrings directly for row keys +- # where possible, rather than encoding strings. +- # + # For more information about how to design a Bigtable schema for + # the best performance, see the documentation: + # +diff --git a/samples/hello/main.py b/samples/hello/main.py +index d3cf91ba..42fe7509 100644 +--- a/samples/hello/main.py ++++ b/samples/hello/main.py +@@ -81,9 +81,6 @@ def main(project_id, instance_id, table_id): + # sequential keys can result in poor distribution of operations + # across nodes. + # +- # We recommend that you use bytestrings directly for row keys +- # where possible, rather than encoding strings. +- # + # For more information about how to design a Bigtable schema for + # the best performance, see the documentation: + # +@@ -91,7 +88,7 @@ def main(project_id, instance_id, table_id): + row_key = f"greeting{i}".encode() + row = table.direct_row(row_key) + row.set_cell( +- column_family_id, column, value, timestamp=datetime.datetime.utcnow(), ++ column_family_id, column, value, timestamp=datetime.datetime.now(datetime.timezone.utc), + ) + rows.append(row) + table.mutate_rows(rows) +diff --git a/samples/snippets/writes/write_batch.py b/samples/snippets/writes/write_batch.py +index 8ad4b07a..d0e8d196 100644 +--- a/samples/snippets/writes/write_batch.py ++++ b/samples/snippets/writes/write_batch.py +@@ -25,7 +25,7 @@ def write_batch(project_id, instance_id, table_id): + table = instance.table(table_id) + + with MutationsBatcher(table=table) as batcher: +- timestamp = datetime.datetime.utcnow() ++ timestamp = datetime.datetime.now(datetime.timezone.utc) + column_family_id = "stats_summary" + + rows = [ +diff --git a/samples/snippets/writes/write_conditionally.py b/samples/snippets/writes/write_conditionally.py +index 7fb640aa..791dd0c3 100644 +--- a/samples/snippets/writes/write_conditionally.py ++++ b/samples/snippets/writes/write_conditionally.py +@@ -24,7 +24,7 @@ def write_conditional(project_id, instance_id, table_id): + instance = client.instance(instance_id) + table = instance.table(table_id) + +- timestamp = datetime.datetime.utcnow() ++ timestamp = datetime.datetime.now(datetime.timezone.utc) + column_family_id = "stats_summary" + + row_key = "phone#4c410523#20190501" +diff --git a/samples/snippets/writes/write_simple.py b/samples/snippets/writes/write_simple.py +index 1aa5a810..4ca56f8f 100644 +--- a/samples/snippets/writes/write_simple.py ++++ b/samples/snippets/writes/write_simple.py +@@ -24,7 +24,7 @@ def write_simple(project_id, instance_id, table_id): + instance = client.instance(instance_id) + table = instance.table(table_id) + +- timestamp = datetime.datetime.utcnow() ++ timestamp = datetime.datetime.now(datetime.timezone.utc) + column_family_id = "stats_summary" + + row_key = "phone#4c410523#20190501" +diff --git a/tests/system/v2_client/_helpers.py b/tests/system/v2_client/_helpers.py +index 95261879..e6fe1034 100644 +--- a/tests/system/v2_client/_helpers.py ++++ b/tests/system/v2_client/_helpers.py +@@ -17,7 +17,6 @@ import datetime + import grpc + from google.api_core import exceptions + from google.cloud import exceptions as core_exceptions +-from google.cloud._helpers import UTC + from test_utils import retry + + +@@ -41,7 +40,7 @@ retry_grpc_unavailable = retry.RetryErrors( + + def label_stamp(): + return ( +- datetime.datetime.utcnow() +- .replace(microsecond=0, tzinfo=UTC) ++ datetime.datetime.now(datetime.timezone.utc) ++ .replace(microsecond=0) + .strftime("%Y-%m-%dt%H-%M-%S") + ) +diff --git a/tests/system/v2_client/test_data_api.py b/tests/system/v2_client/test_data_api.py +index 579837e3..b1563da1 100644 +--- a/tests/system/v2_client/test_data_api.py ++++ b/tests/system/v2_client/test_data_api.py +@@ -233,10 +233,9 @@ def test_table_read_row_large_cell(data_table, rows_to_delete, skip_on_emulator) + def _write_to_row(row1, row2, row3, row4): + from google.cloud._helpers import _datetime_from_microseconds + from google.cloud._helpers import _microseconds_from_datetime +- from google.cloud._helpers import UTC + from google.cloud.bigtable.row_data import Cell + +- timestamp1 = datetime.datetime.utcnow().replace(tzinfo=UTC) ++ timestamp1 = datetime.datetime.now(datetime.timezone.utc) + timestamp1_micros = _microseconds_from_datetime(timestamp1) + # Truncate to millisecond granularity. + timestamp1_micros -= timestamp1_micros % 1000 +diff --git a/tests/unit/data/_async/test_client.py b/tests/unit/data/_async/test_client.py +index 9f65d120..72b3ae73 100644 +--- a/tests/unit/data/_async/test_client.py ++++ b/tests/unit/data/_async/test_client.py +@@ -55,26 +55,18 @@ if CrossSync.is_async: + from google.cloud.bigtable.data._async._swappable_channel import ( + AsyncSwappableChannel, + ) +- from google.cloud.bigtable.data._async.metrics_interceptor import ( +- AsyncBigtableMetricsInterceptor, +- ) + + CrossSync.add_mapping("grpc_helpers", grpc_helpers_async) + CrossSync.add_mapping("SwappableChannel", AsyncSwappableChannel) +- CrossSync.add_mapping("MetricsInterceptor", AsyncBigtableMetricsInterceptor) + else: + from google.api_core import grpc_helpers + from google.cloud.bigtable.data._sync_autogen.client import Table # noqa: F401 + from google.cloud.bigtable.data._sync_autogen._swappable_channel import ( + SwappableChannel, + ) +- from google.cloud.bigtable.data._sync_autogen.metrics_interceptor import ( +- BigtableMetricsInterceptor, +- ) + + CrossSync.add_mapping("grpc_helpers", grpc_helpers) + CrossSync.add_mapping("SwappableChannel", SwappableChannel) +- CrossSync.add_mapping("MetricsInterceptor", BigtableMetricsInterceptor) + + __CROSS_SYNC_OUTPUT__ = "tests.unit.data._sync_autogen.test_client" + +@@ -122,7 +114,6 @@ class TestBigtableDataClientAsync: + assert not client._active_instances + assert client._channel_refresh_task is not None + assert client.transport._credentials == expected_credentials +- assert isinstance(client._metrics_interceptor, CrossSync.MetricsInterceptor) + await client.close() + + @CrossSync.pytest +@@ -1162,9 +1153,6 @@ class TestTableAsync: + @CrossSync.pytest + async def test_ctor(self): + from google.cloud.bigtable.data._helpers import _WarmedInstanceKey +- from google.cloud.bigtable.data._metrics import ( +- BigtableClientSideMetricsController, +- ) + + expected_table_id = "table-id" + expected_instance_id = "instance-id" +@@ -1206,7 +1194,6 @@ class TestTableAsync: + instance_key = _WarmedInstanceKey(table.instance_name, table.app_profile_id) + assert instance_key in client._active_instances + assert client._instance_owners[instance_key] == {id(table)} +- assert isinstance(table._metrics, BigtableClientSideMetricsController) + assert table.default_operation_timeout == expected_operation_timeout + assert table.default_attempt_timeout == expected_attempt_timeout + assert ( +@@ -1467,22 +1454,6 @@ class TestTableAsync: + # empty app_profile_id should send empty string + assert "app_profile_id=" in routing_str + +- @CrossSync.pytest +- async def test_close(self): +- client = self._make_client() +- table = self._make_one(client) +- with mock.patch.object( +- table._metrics, "close", mock.Mock() +- ) as metric_close_mock: +- with mock.patch.object( +- client, "_remove_instance_registration" +- ) as remove_mock: +- await table.close() +- remove_mock.assert_called_once_with( +- table.instance_id, table.app_profile_id, id(table) +- ) +- metric_close_mock.assert_called_once() +- + + @CrossSync.convert_class( + "TestAuthorizedView", add_mapping_for_name="TestAuthorizedView" +@@ -1513,9 +1484,6 @@ class TestAuthorizedViewsAsync(CrossSync.TestTable): + @CrossSync.pytest + async def test_ctor(self): + from google.cloud.bigtable.data._helpers import _WarmedInstanceKey +- from google.cloud.bigtable.data._metrics import ( +- BigtableClientSideMetricsController, +- ) + + expected_table_id = "table-id" + expected_instance_id = "instance-id" +@@ -1564,7 +1532,6 @@ class TestAuthorizedViewsAsync(CrossSync.TestTable): + instance_key = _WarmedInstanceKey(view.instance_name, view.app_profile_id) + assert instance_key in client._active_instances + assert client._instance_owners[instance_key] == {id(view)} +- assert isinstance(view._metrics, BigtableClientSideMetricsController) + assert view.default_operation_timeout == expected_operation_timeout + assert view.default_attempt_timeout == expected_attempt_timeout + assert ( +@@ -1778,8 +1745,9 @@ class TestReadRowsAsync: + @pytest.mark.parametrize( + "per_request_t, operation_t, expected_num", + [ +- (0.1, 0.19, 2), +- (0.1, 0.29, 3), ++ (0.05, 0.08, 2), ++ (0.05, 0.14, 3), ++ (0.05, 0.24, 5), + ], + ) + @CrossSync.pytest +diff --git a/tests/unit/data/_async/test_metrics_interceptor.py b/tests/unit/data/_async/test_metrics_interceptor.py +index 1593b8c9..6ea95835 100644 +--- a/tests/unit/data/_async/test_metrics_interceptor.py ++++ b/tests/unit/data/_async/test_metrics_interceptor.py +@@ -14,10 +14,7 @@ + + import pytest + from grpc import RpcError +-from grpc import ClientCallDetails + +-from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric +-from google.cloud.bigtable.data._metrics.data_model import OperationState + from google.cloud.bigtable.data._cross_sync import CrossSync + + # try/except added for compatibility with python < 3.8 +@@ -70,267 +67,102 @@ class TestMetricsInterceptorAsync: + def _make_one(self, *args, **kwargs): + return self._get_target_class()(*args, **kwargs) + +- @CrossSync.pytest +- async def test_unary_unary_interceptor_op_not_found(self): +- """Test that interceptor call continuation if op is not found""" +- instance = self._make_one() +- continuation = CrossSync.Mock() +- details = ClientCallDetails() +- details.metadata = [] +- request = mock.Mock() +- await instance.intercept_unary_unary(continuation, details, request) +- continuation.assert_called_once_with(details, request) +- + @CrossSync.pytest + async def test_unary_unary_interceptor_success(self): + """Test that interceptor handles successful unary-unary calls""" + instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- ActiveOperationMetric._active_operation_context.set(op) + continuation = CrossSync.Mock() + call = continuation.return_value +- call.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")]) +- call.initial_metadata = CrossSync.Mock(return_value=[("c", "d")]) +- details = ClientCallDetails() ++ details = mock.Mock() + request = mock.Mock() + result = await instance.intercept_unary_unary(continuation, details, request) + assert result == call + continuation.assert_called_once_with(details, request) +- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) +- op.end_attempt_with_status.assert_not_called() + + @CrossSync.pytest + async def test_unary_unary_interceptor_failure(self): +- """test a failed RpcError with metadata""" +- instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- ActiveOperationMetric._active_operation_context.set(op) +- exc = RpcError("test") +- exc.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")]) +- exc.initial_metadata = CrossSync.Mock(return_value=[("c", "d")]) +- continuation = CrossSync.Mock(side_effect=exc) +- details = ClientCallDetails() +- request = mock.Mock() +- with pytest.raises(RpcError) as e: +- await instance.intercept_unary_unary(continuation, details, request) +- assert e.value == exc +- continuation.assert_called_once_with(details, request) +- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) ++ """Test a failed RpcError with metadata""" + +- @CrossSync.pytest +- async def test_unary_unary_interceptor_failure_no_metadata(self): +- """test with RpcError without without metadata attached""" + instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- ActiveOperationMetric._active_operation_context.set(op) + exc = RpcError("test") + continuation = CrossSync.Mock(side_effect=exc) +- call = continuation.return_value +- call.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")]) +- call.initial_metadata = CrossSync.Mock(return_value=[("c", "d")]) +- details = ClientCallDetails() ++ details = mock.Mock() + request = mock.Mock() + with pytest.raises(RpcError) as e: + await instance.intercept_unary_unary(continuation, details, request) + assert e.value == exc + continuation.assert_called_once_with(details, request) +- op.add_response_metadata.assert_not_called() + + @CrossSync.pytest + async def test_unary_unary_interceptor_failure_generic(self): +- """test generic exception""" ++ """Test generic exception""" ++ + instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- ActiveOperationMetric._active_operation_context.set(op) + exc = ValueError("test") + continuation = CrossSync.Mock(side_effect=exc) +- call = continuation.return_value +- call.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")]) +- call.initial_metadata = CrossSync.Mock(return_value=[("c", "d")]) +- details = ClientCallDetails() ++ details = mock.Mock() + request = mock.Mock() + with pytest.raises(ValueError) as e: + await instance.intercept_unary_unary(continuation, details, request) + assert e.value == exc + continuation.assert_called_once_with(details, request) +- op.add_response_metadata.assert_not_called() +- +- @CrossSync.pytest +- async def test_unary_stream_interceptor_op_not_found(self): +- """Test that interceptor calls continuation if op is not found""" +- instance = self._make_one() +- continuation = CrossSync.Mock() +- details = ClientCallDetails() +- details.metadata = [] +- request = mock.Mock() +- await instance.intercept_unary_stream(continuation, details, request) +- continuation.assert_called_once_with(details, request) + + @CrossSync.pytest + async def test_unary_stream_interceptor_success(self): + """Test that interceptor handles successful unary-stream calls""" ++ + instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- op.start_time_ns = 0 +- op.first_response_latency = None +- ActiveOperationMetric._active_operation_context.set(op) + + continuation = CrossSync.Mock(return_value=_make_mock_stream_call([1, 2])) +- call = continuation.return_value +- call.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")]) +- call.initial_metadata = CrossSync.Mock(return_value=[("c", "d")]) +- details = ClientCallDetails() ++ details = mock.Mock() + request = mock.Mock() + wrapper = await instance.intercept_unary_stream(continuation, details, request) + results = [val async for val in wrapper] + assert results == [1, 2] + continuation.assert_called_once_with(details, request) +- assert op.first_response_latency_ns is not None +- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) +- op.end_attempt_with_status.assert_not_called() + + @CrossSync.pytest + async def test_unary_stream_interceptor_failure_mid_stream(self): + """Test that interceptor handles failures mid-stream""" +- from grpc.aio import AioRpcError, Metadata +- + instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- op.start_time_ns = 0 +- op.first_response_latency = None +- ActiveOperationMetric._active_operation_context.set(op) +- exc = AioRpcError(0, Metadata(), Metadata(("a", "b"), ("c", "d"))) ++ exc = ValueError("test") + continuation = CrossSync.Mock(return_value=_make_mock_stream_call([1], exc=exc)) +- details = ClientCallDetails() ++ details = mock.Mock() + request = mock.Mock() + wrapper = await instance.intercept_unary_stream(continuation, details, request) +- with pytest.raises(AioRpcError) as e: ++ with pytest.raises(ValueError) as e: + [val async for val in wrapper] + assert e.value == exc + continuation.assert_called_once_with(details, request) +- assert op.first_response_latency_ns is not None +- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) + + @CrossSync.pytest + async def test_unary_stream_interceptor_failure_start_stream(self): + """Test that interceptor handles failures at start of stream with RpcError with metadata""" +- instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- op.start_time_ns = 0 +- op.first_response_latency = None +- ActiveOperationMetric._active_operation_context.set(op) +- exc = RpcError("test") +- exc.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")]) +- exc.initial_metadata = CrossSync.Mock(return_value=[("c", "d")]) +- +- continuation = CrossSync.Mock() +- continuation.side_effect = exc +- details = ClientCallDetails() +- request = mock.Mock() +- with pytest.raises(RpcError) as e: +- await instance.intercept_unary_stream(continuation, details, request) +- assert e.value == exc +- continuation.assert_called_once_with(details, request) +- assert op.first_response_latency_ns is not None +- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) + +- @CrossSync.pytest +- async def test_unary_stream_interceptor_failure_start_stream_no_metadata(self): +- """Test that interceptor handles failures at start of stream with RpcError with no metadata""" + instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- op.start_time_ns = 0 +- op.first_response_latency = None +- ActiveOperationMetric._active_operation_context.set(op) + exc = RpcError("test") + + continuation = CrossSync.Mock() + continuation.side_effect = exc +- details = ClientCallDetails() ++ details = mock.Mock() + request = mock.Mock() + with pytest.raises(RpcError) as e: + await instance.intercept_unary_stream(continuation, details, request) + assert e.value == exc + continuation.assert_called_once_with(details, request) +- assert op.first_response_latency_ns is not None +- op.add_response_metadata.assert_not_called() + + @CrossSync.pytest + async def test_unary_stream_interceptor_failure_start_stream_generic(self): + """Test that interceptor handles failures at start of stream with generic exception""" ++ + instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- op.start_time_ns = 0 +- op.first_response_latency = None +- ActiveOperationMetric._active_operation_context.set(op) + exc = ValueError("test") + + continuation = CrossSync.Mock() + continuation.side_effect = exc +- details = ClientCallDetails() ++ details = mock.Mock() + request = mock.Mock() + with pytest.raises(ValueError) as e: + await instance.intercept_unary_stream(continuation, details, request) + assert e.value == exc + continuation.assert_called_once_with(details, request) +- assert op.first_response_latency_ns is not None +- op.add_response_metadata.assert_not_called() +- +- @CrossSync.pytest +- @pytest.mark.parametrize( +- "initial_state", [OperationState.CREATED, OperationState.BETWEEN_ATTEMPTS] +- ) +- async def test_unary_unary_interceptor_start_operation(self, initial_state): +- """if called with a newly created operation, it should be started""" +- instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = initial_state +- ActiveOperationMetric._active_operation_context.set(op) +- continuation = CrossSync.Mock() +- call = continuation.return_value +- call.trailing_metadata = CrossSync.Mock(return_value=[]) +- call.initial_metadata = CrossSync.Mock(return_value=[]) +- details = ClientCallDetails() +- request = mock.Mock() +- await instance.intercept_unary_unary(continuation, details, request) +- op.start_attempt.assert_called_once() +- +- @CrossSync.pytest +- @pytest.mark.parametrize( +- "initial_state", [OperationState.CREATED, OperationState.BETWEEN_ATTEMPTS] +- ) +- async def test_unary_stream_interceptor_start_operation(self, initial_state): +- """if called with a newly created operation, it should be started""" +- instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = initial_state +- ActiveOperationMetric._active_operation_context.set(op) +- +- continuation = CrossSync.Mock(return_value=_make_mock_stream_call([1, 2])) +- call = continuation.return_value +- call.trailing_metadata = CrossSync.Mock(return_value=[]) +- call.initial_metadata = CrossSync.Mock(return_value=[]) +- details = ClientCallDetails() +- request = mock.Mock() +- await instance.intercept_unary_stream(continuation, details, request) +- op.start_attempt.assert_called_once() +diff --git a/tests/unit/data/_metrics/__init__.py b/tests/unit/data/_metrics/__init__.py +deleted file mode 100644 +index e69de29b..00000000 +diff --git a/tests/unit/data/_metrics/test_data_model.py b/tests/unit/data/_metrics/test_data_model.py +deleted file mode 100644 +index 93e73c9d..00000000 +--- a/tests/unit/data/_metrics/test_data_model.py ++++ /dev/null +@@ -1,730 +0,0 @@ +-# Copyright 2023 Google LLC +-# +-# 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 pytest +-import mock +- +-from google.cloud.bigtable.data._metrics.data_model import OperationState as State +-from google.cloud.bigtable_v2.types import ResponseParams +- +- +-class TestActiveOperationMetric: +- def _make_one(self, *args, **kwargs): +- from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric +- +- return ActiveOperationMetric(*args, **kwargs) +- +- @mock.patch("time.monotonic_ns") +- def test_ctor_defaults(self, mock_monotonic_ns): +- """ +- create an instance with default values +- """ +- expected_timestamp = 123456789 +- mock_monotonic_ns.return_value = expected_timestamp +- mock_type = mock.Mock() +- metric = self._make_one(mock_type) +- assert metric.op_type == mock_type +- assert metric.start_time_ns == expected_timestamp +- assert metric.active_attempt is None +- assert metric.cluster_id is None +- assert metric.zone is None +- assert len(metric.completed_attempts) == 0 +- assert len(metric.handlers) == 0 +- assert metric.is_streaming is False +- assert metric.flow_throttling_time_ns == 0 +- assert metric.state == State.CREATED +- +- def test_ctor_explicit(self): +- """ +- test with explicit arguments +- """ +- expected_type = mock.Mock() +- expected_start_time_ns = 7 +- expected_active_attempt = mock.Mock() +- expected_cluster_id = "cluster" +- expected_zone = "zone" +- expected_completed_attempts = [mock.Mock()] +- expected_state = State.COMPLETED +- expected_handlers = [mock.Mock()] +- expected_is_streaming = True +- expected_flow_throttling = 12 +- metric = self._make_one( +- op_type=expected_type, +- start_time_ns=expected_start_time_ns, +- active_attempt=expected_active_attempt, +- cluster_id=expected_cluster_id, +- zone=expected_zone, +- state=expected_state, +- completed_attempts=expected_completed_attempts, +- handlers=expected_handlers, +- is_streaming=expected_is_streaming, +- flow_throttling_time_ns=expected_flow_throttling, +- ) +- assert metric.op_type == expected_type +- assert metric.start_time_ns == expected_start_time_ns +- assert metric.active_attempt == expected_active_attempt +- assert metric.cluster_id == expected_cluster_id +- assert metric.zone == expected_zone +- assert metric.completed_attempts == expected_completed_attempts +- assert metric.state == expected_state +- assert metric.handlers == expected_handlers +- assert metric.is_streaming == expected_is_streaming +- assert metric.flow_throttling_time_ns == expected_flow_throttling +- +- def test_state_machine_w_methods(self): +- """ +- Exercise the state machine by calling methods to move between states +- """ +- metric = self._make_one(mock.Mock()) +- assert metric.state == State.CREATED +- metric.start() +- assert metric.state == State.CREATED +- metric.start_attempt() +- assert metric.state == State.ACTIVE_ATTEMPT +- metric.end_attempt_with_status(Exception()) +- assert metric.state == State.BETWEEN_ATTEMPTS +- metric.start_attempt() +- assert metric.state == State.ACTIVE_ATTEMPT +- metric.end_with_success() +- assert metric.state == State.COMPLETED +- +- def test_state_machine(self): +- """ +- Exercise state machine by moving through states +- """ +- metric = self._make_one(mock.Mock()) +- assert metric.state == State.CREATED +- metric.start_attempt() +- assert metric.state == State.ACTIVE_ATTEMPT +- metric.end_attempt_with_status(0) +- assert metric.state == State.BETWEEN_ATTEMPTS +- metric.end_with_success() +- assert metric.state == State.COMPLETED +- +- @pytest.mark.parametrize( +- "method,args,valid_states,error_method_name", +- [ +- ("start", (), (State.CREATED,), None), +- ("start_attempt", (), (State.CREATED, State.BETWEEN_ATTEMPTS), None), +- ("add_response_metadata", ({},), (State.ACTIVE_ATTEMPT,), None), +- ("end_attempt_with_status", (mock.Mock(),), (State.ACTIVE_ATTEMPT,), None), +- ( +- "end_with_status", +- (mock.Mock(),), +- ( +- State.CREATED, +- State.ACTIVE_ATTEMPT, +- State.BETWEEN_ATTEMPTS, +- ), +- None, +- ), +- ( +- "end_with_success", +- (), +- ( +- State.CREATED, +- State.ACTIVE_ATTEMPT, +- State.BETWEEN_ATTEMPTS, +- ), +- "end_with_status", +- ), +- ], +- ids=lambda x: x if isinstance(x, str) else "", +- ) +- def test_error_invalid_states(self, method, args, valid_states, error_method_name): +- """ +- each method only works for certain states. Make sure _handle_error is called for invalid states +- """ +- cls = type(self._make_one(mock.Mock())) +- invalid_states = set(State) - set(valid_states) +- error_method_name = error_method_name or method +- for state in invalid_states: +- with mock.patch.object(cls, "_handle_error") as mock_handle_error: +- mock_handle_error.return_value = None +- metric = self._make_one(mock.Mock(), state=state) +- return_obj = getattr(metric, method)(*args) +- assert return_obj is None +- assert mock_handle_error.call_count == 1 +- assert ( +- mock_handle_error.call_args[0][0] +- == f"Invalid state for {error_method_name}: {state}" +- ) +- +- @mock.patch("time.monotonic_ns") +- def test_start(self, mock_monotonic_ns): +- """ +- calling start op operation should reset start_time +- """ +- expected_timestamp = 123456789 +- mock_monotonic_ns.return_value = expected_timestamp +- orig_time = 0 +- metric = self._make_one(mock.Mock(), start_time_ns=orig_time) +- assert metric.start_time_ns == 0 +- metric.start() +- assert metric.start_time_ns != orig_time +- assert metric.start_time_ns == expected_timestamp +- # should remain in CREATED state after completing +- assert metric.state == State.CREATED +- +- @mock.patch("time.monotonic_ns") +- def test_start_attempt(self, mock_monotonic_ns): +- """ +- calling start_attempt should create a new emptu atempt metric +- """ +- from google.cloud.bigtable.data._metrics.data_model import ActiveAttemptMetric +- +- expected_timestamp = 123456789 +- mock_monotonic_ns.return_value = expected_timestamp +- metric = self._make_one(mock.Mock()) +- assert metric.active_attempt is None +- metric.start_attempt() +- assert isinstance(metric.active_attempt, ActiveAttemptMetric) +- # make sure it was initialized with the correct values +- assert metric.active_attempt.start_time_ns == expected_timestamp +- assert metric.active_attempt.gfe_latency_ns is None +- # should be in ACTIVE_ATTEMPT state after completing +- assert metric.state == State.ACTIVE_ATTEMPT +- +- def test_start_attempt_with_backoff_generator(self): +- """ +- If operation has a backoff generator, it should be used to attach backoff +- times to attempts +- """ +- from google.cloud.bigtable.data._helpers import TrackedBackoffGenerator +- +- generator = TrackedBackoffGenerator() +- # pre-seed generator with exepcted values +- generator.history = list(range(10)) +- metric = self._make_one(mock.Mock(), backoff_generator=generator) +- metric.start_attempt() +- assert len(metric.completed_attempts) == 0 +- # first attempt should always be 0 +- assert metric.active_attempt.backoff_before_attempt_ns == 0 +- # later attempts should have their attempt number as backoff time +- for i in range(10): +- metric.end_attempt_with_status(mock.Mock()) +- assert len(metric.completed_attempts) == i + 1 +- metric.start_attempt() +- # expect the backoff to be converted froms seconds to ns +- assert metric.active_attempt.backoff_before_attempt_ns == (i * 1e9) +- +- @pytest.mark.parametrize( +- "start_cluster,start_zone,metadata_proto,end_cluster,end_zone", +- [ +- (None, None, None, None, None), +- ("orig_cluster", "orig_zone", None, "orig_cluster", "orig_zone"), +- (None, None, ResponseParams(), None, None), +- ( +- "orig_cluster", +- "orig_zone", +- ResponseParams(), +- "orig_cluster", +- "orig_zone", +- ), +- ( +- None, +- None, +- ResponseParams(cluster_id="test-cluster", zone_id="us-central1-b"), +- "test-cluster", +- "us-central1-b", +- ), +- ( +- None, +- "filled", +- ResponseParams(cluster_id="cluster", zone_id="zone"), +- "cluster", +- "zone", +- ), +- (None, "filled", ResponseParams(cluster_id="cluster"), "cluster", "filled"), +- (None, "filled", ResponseParams(zone_id="zone"), None, "zone"), +- ( +- "filled", +- None, +- ResponseParams(cluster_id="cluster", zone_id="zone"), +- "cluster", +- "zone", +- ), +- ("filled", None, ResponseParams(cluster_id="cluster"), "cluster", None), +- ("filled", None, ResponseParams(zone_id="zone"), "filled", "zone"), +- ], +- ) +- def test_add_response_metadata_cbt_header( +- self, start_cluster, start_zone, metadata_proto, end_cluster, end_zone +- ): +- """ +- calling add_response_metadata should update fields based on grpc response metadata +- The x-goog-ext-425905942-bin field contains cluster and zone info +- """ +- import grpc +- +- cls = type(self._make_one(mock.Mock())) +- with mock.patch.object(cls, "_handle_error") as mock_handle_error: +- metric = self._make_one( +- mock.Mock(), +- cluster_id=start_cluster, +- zone=start_zone, +- state=State.ACTIVE_ATTEMPT, +- ) +- metric.active_attempt = mock.Mock() +- metric.active_attempt.gfe_latency_ns = None +- metadata = grpc.aio.Metadata() +- if metadata_proto is not None: +- metadata["x-goog-ext-425905942-bin"] = ResponseParams.serialize( +- metadata_proto +- ) +- metric.add_response_metadata(metadata) +- assert metric.cluster_id == end_cluster +- assert metric.zone == end_zone +- # should remain in ACTIVE_ATTEMPT state after completing +- assert metric.state == State.ACTIVE_ATTEMPT +- # no errors encountered +- assert mock_handle_error.call_count == 0 +- # gfe latency should not be touched +- assert metric.active_attempt.gfe_latency_ns is None +- +- @pytest.mark.parametrize( +- "metadata_field", +- [ +- b"bad-input", +- "cluster zone", # expect bytes +- ], +- ) +- def test_add_response_metadata_cbt_header_w_error(self, metadata_field): +- """ +- If the x-goog-ext-425905942-bin field is present, but not structured properly, +- _handle_error should be called +- +- Extra fields should not result in parsingerror +- """ +- import grpc +- +- cls = type(self._make_one(mock.Mock())) +- with mock.patch.object(cls, "_handle_error") as mock_handle_error: +- metric = self._make_one(mock.Mock(), state=State.ACTIVE_ATTEMPT) +- metric.cluster_id = None +- metric.zone = None +- metric.active_attempt = mock.Mock() +- metadata = grpc.aio.Metadata() +- metadata["x-goog-ext-425905942-bin"] = metadata_field +- metric.add_response_metadata(metadata) +- # should remain in ACTIVE_ATTEMPT state after completing +- assert metric.state == State.ACTIVE_ATTEMPT +- # no errors encountered +- assert mock_handle_error.call_count == 1 +- assert ( +- "Failed to decode x-goog-ext-425905942-bin metadata:" +- in mock_handle_error.call_args[0][0] +- ) +- assert str(metadata_field) in mock_handle_error.call_args[0][0] +- +- @pytest.mark.parametrize( +- "metadata_field,expected_latency_ns", +- [ +- (None, None), +- ("gfet4t7; dur=1000", 1000e6), +- ("gfet4t7; dur=1000.0", 1000e6), +- ("gfet4t7; dur=1000.1", 1000.1e6), +- ("gcp; dur=15, gfet4t7; dur=300", 300e6), +- ("gfet4t7;dur=350,gcp;dur=12", 350e6), +- ("ignore_megfet4t7;dur=90ignore_me", 90e6), +- ("gfet4t7;dur=2000", 2000e6), +- ("gfet4t7; dur=0.001", 1000), +- ("gfet4t7; dur=0.000001", 1), +- ("gfet4t7; dur=0.0000001", 0), # below recording resolution +- ("gfet4t7; dur=0", 0), +- ("gfet4t7; dur=empty", None), +- ("gfet4t7;", None), +- ("", None), +- ], +- ) +- def test_add_response_metadata_server_timing_header( +- self, metadata_field, expected_latency_ns +- ): +- """ +- calling add_response_metadata should update fields based on grpc response metadata +- The server-timing field contains gfle latency info +- """ +- import grpc +- +- cls = type(self._make_one(mock.Mock())) +- with mock.patch.object(cls, "_handle_error") as mock_handle_error: +- metric = self._make_one(mock.Mock(), state=State.ACTIVE_ATTEMPT) +- metric.active_attempt = mock.Mock() +- metric.active_attempt.gfe_latency_ns = None +- metadata = grpc.aio.Metadata() +- if metadata_field: +- metadata["server-timing"] = metadata_field +- metric.add_response_metadata(metadata) +- if metric.active_attempt.gfe_latency_ns is None: +- assert expected_latency_ns is None +- else: +- assert metric.active_attempt.gfe_latency_ns == int(expected_latency_ns) +- # should remain in ACTIVE_ATTEMPT state after completing +- assert metric.state == State.ACTIVE_ATTEMPT +- # no errors encountered +- assert mock_handle_error.call_count == 0 +- # cluster and zone should not be touched +- assert metric.cluster_id is None +- assert metric.zone is None +- +- @mock.patch("time.monotonic_ns") +- def test_end_attempt_with_status(self, mock_monotonic_ns): +- """ +- ending the attempt should: +- - add one to completed_attempts +- - reset active_attempt to None +- - update state +- - notify handlers +- """ +- expected_mock_time = 123456789 +- mock_monotonic_ns.return_value = expected_mock_time +- expected_start_time = 1 +- expected_status = object() +- expected_gfe_latency_ns = 5 +- expected_app_blocking = 12 +- expected_backoff = 2 +- handlers = [mock.Mock(), mock.Mock()] +- +- metric = self._make_one(mock.Mock(), handlers=handlers) +- assert metric.active_attempt is None +- assert len(metric.completed_attempts) == 0 +- metric.start_attempt() +- metric.active_attempt.start_time_ns = expected_start_time +- metric.active_attempt.gfe_latency_ns = expected_gfe_latency_ns +- metric.active_attempt.application_blocking_time_ns = expected_app_blocking +- metric.active_attempt.backoff_before_attempt_ns = expected_backoff +- metric.end_attempt_with_status(expected_status) +- assert len(metric.completed_attempts) == 1 +- got_attempt = metric.completed_attempts[0] +- expected_duration = expected_mock_time - expected_start_time +- assert got_attempt.duration_ns == expected_duration +- assert got_attempt.end_status == expected_status +- assert got_attempt.gfe_latency_ns == expected_gfe_latency_ns +- assert got_attempt.application_blocking_time_ns == expected_app_blocking +- assert got_attempt.backoff_before_attempt_ns == expected_backoff +- # state should be changed to BETWEEN_ATTEMPTS +- assert metric.state == State.BETWEEN_ATTEMPTS +- # check handlers +- for h in handlers: +- assert h.on_attempt_complete.call_count == 1 +- assert h.on_attempt_complete.call_args[0][0] == got_attempt +- assert h.on_attempt_complete.call_args[0][1] == metric +- +- def test_end_attempt_with_status_w_exception(self): +- """ +- exception inputs should be converted to grpc status objects +- """ +- input_status = ValueError("test") +- expected_status = object() +- +- metric = self._make_one(mock.Mock()) +- metric.start_attempt() +- with mock.patch.object( +- metric, "_exc_to_status", return_value=expected_status +- ) as mock_exc_to_status: +- metric.end_attempt_with_status(input_status) +- assert mock_exc_to_status.call_count == 1 +- assert mock_exc_to_status.call_args[0][0] == input_status +- assert metric.completed_attempts[0].end_status == expected_status +- +- @mock.patch("time.monotonic_ns") +- def test_end_attempt_with_negative_duration_ns(self, mock_monotonic_ns): +- """ +- If duration_ns is negative, it should be set to 0 and _handle_error should be called +- """ +- cls = type(self._make_one(mock.Mock())) +- with mock.patch.object(cls, "_handle_error") as mock_handle_error: +- metric = self._make_one(mock.Mock()) +- metric.start_attempt() +- metric.active_attempt.start_time_ns = 100 +- mock_monotonic_ns.return_value = 50 # Simulate time going backwards +- metric.end_attempt_with_status(mock.Mock()) +- +- assert mock_handle_error.call_count == 1 +- assert ( +- "received negative value for duration" +- in mock_handle_error.call_args[0][0] +- ) +- assert metric.completed_attempts[0].duration_ns == 0 +- +- @mock.patch("time.monotonic_ns") +- def test_end_with_status(self, mock_monotonic_ns): +- """ +- ending the operation should: +- - end active attempt +- - mark operation as completed +- - update handlers +- """ +- from google.cloud.bigtable.data._metrics.data_model import ActiveAttemptMetric +- +- expected_mock_time = 123456789 +- mock_monotonic_ns.return_value = expected_mock_time +- expected_attempt_start_time = 0 +- expected_attempt_gfe_latency_ns = 5 +- expected_flow_time = 16 +- +- expected_first_response_latency_ns = 9 +- expected_status = object() +- expected_type = object() +- expected_start_time = 1 +- expected_cluster = object() +- expected_zone = object() +- is_streaming = object() +- +- handlers = [mock.Mock(), mock.Mock()] +- metric = self._make_one( +- expected_type, +- handlers=handlers, +- start_time_ns=expected_start_time, +- state=State.ACTIVE_ATTEMPT, +- ) +- metric.cluster_id = expected_cluster +- metric.zone = expected_zone +- metric.is_streaming = is_streaming +- metric.flow_throttling_time_ns = expected_flow_time +- metric.first_response_latency_ns = expected_first_response_latency_ns +- attempt = ActiveAttemptMetric( +- start_time_ns=expected_attempt_start_time, +- gfe_latency_ns=expected_attempt_gfe_latency_ns, +- ) +- metric.active_attempt = attempt +- metric.end_with_status(expected_status) +- # test that ActiveOperation was updated to terminal state +- assert metric.state == State.COMPLETED +- assert metric.active_attempt is None +- assert len(metric.completed_attempts) == 1 +- # check that finalized operation was passed to handlers +- for h in handlers: +- assert h.on_operation_complete.call_count == 1 +- assert len(h.on_operation_complete.call_args[0]) == 1 +- called_with = h.on_operation_complete.call_args[0][0] +- assert called_with.op_type == expected_type +- expected_duration = expected_mock_time - expected_start_time +- assert called_with.duration_ns == expected_duration +- assert called_with.final_status == expected_status +- assert called_with.cluster_id == expected_cluster +- assert called_with.zone == expected_zone +- assert called_with.is_streaming == is_streaming +- assert called_with.flow_throttling_time_ns == expected_flow_time +- assert ( +- called_with.first_response_latency_ns +- == expected_first_response_latency_ns +- ) +- # check the attempt +- assert len(called_with.completed_attempts) == 1 +- final_attempt = called_with.completed_attempts[0] +- assert final_attempt.gfe_latency_ns == expected_attempt_gfe_latency_ns +- assert final_attempt.end_status == expected_status +- expected_duration = expected_mock_time - expected_attempt_start_time +- assert final_attempt.duration_ns == expected_duration +- +- @mock.patch("time.monotonic_ns") +- def test_end_with_negative_duration_ns(self, mock_monotonic_ns): +- """ +- If operation duration_ns is negative, it should be set to 0 and _handle_error should be called +- """ +- cls = type(self._make_one(mock.Mock())) +- with mock.patch.object(cls, "_handle_error") as mock_handle_error: +- metric = self._make_one(mock.Mock(), handlers=[mock.Mock()]) +- metric.start_time_ns = 100 +- mock_monotonic_ns.return_value = 50 # Simulate time going backwards +- metric.end_with_status(mock.Mock()) +- +- assert mock_handle_error.call_count == 1 +- assert ( +- "received negative value for duration" +- in mock_handle_error.call_args[0][0] +- ) +- final_op = metric.handlers[0].on_operation_complete.call_args[0][0] +- assert final_op.duration_ns == 0 +- +- def test_end_with_status_w_exception(self): +- """ +- exception inputs should be converted to grpc status objects +- """ +- input_status = ValueError("test") +- expected_status = object() +- handlers = [mock.Mock()] +- +- metric = self._make_one(mock.Mock(), handlers=handlers) +- metric.start_attempt() +- with mock.patch.object( +- metric, "_exc_to_status", return_value=expected_status +- ) as mock_exc_to_status: +- metric.end_with_status(input_status) +- assert mock_exc_to_status.call_count == 1 +- assert mock_exc_to_status.call_args[0][0] == input_status +- assert metric.completed_attempts[0].end_status == expected_status +- final_op = handlers[0].on_operation_complete.call_args[0][0] +- assert final_op.final_status == expected_status +- +- def test_end_with_status_with_default_cluster_zone(self): +- """ +- ending the operation should use default cluster and zone if not set +- """ +- from google.cloud.bigtable.data._metrics.data_model import ( +- DEFAULT_CLUSTER_ID, +- DEFAULT_ZONE, +- ) +- +- handlers = [mock.Mock()] +- metric = self._make_one(mock.Mock(), handlers=handlers) +- assert metric.cluster_id is None +- assert metric.zone is None +- metric.end_with_status(mock.Mock()) +- assert metric.state == State.COMPLETED +- # check that finalized operation was passed to handlers +- for h in handlers: +- assert h.on_operation_complete.call_count == 1 +- called_with = h.on_operation_complete.call_args[0][0] +- assert called_with.cluster_id == DEFAULT_CLUSTER_ID +- assert called_with.zone == DEFAULT_ZONE +- +- def test_end_with_success(self): +- """ +- end with success should be a pass-through helper for end_with_status +- """ +- from grpc import StatusCode +- +- inner_result = object() +- +- metric = self._make_one(mock.Mock()) +- with mock.patch.object(metric, "end_with_status") as mock_end_with_status: +- mock_end_with_status.return_value = inner_result +- got_result = metric.end_with_success() +- assert mock_end_with_status.call_count == 1 +- assert mock_end_with_status.call_args[0][0] == StatusCode.OK +- assert got_result is inner_result +- +- def test_end_on_empty_operation(self): +- """ +- Should be able to end an operation without any attempts +- """ +- from grpc import StatusCode +- +- handlers = [mock.Mock()] +- metric = self._make_one(mock.Mock(), handlers=handlers) +- metric.end_with_success() +- assert metric.state == State.COMPLETED +- final_op = handlers[0].on_operation_complete.call_args[0][0] +- assert final_op.final_status == StatusCode.OK +- assert final_op.completed_attempts == [] +- +- def test__exc_to_status(self): +- """ +- Should return grpc_status_code if grpc error, otherwise UNKNOWN +- +- If BigtableExceptionGroup, use the most recent exception in the group +- """ +- from grpc import StatusCode +- from google.api_core import exceptions as core_exc +- from google.cloud.bigtable.data import exceptions as bt_exc +- +- cls = type(self._make_one(object())) +- # unknown for non-grpc errors +- assert cls._exc_to_status(ValueError()) == StatusCode.UNKNOWN +- assert cls._exc_to_status(RuntimeError()) == StatusCode.UNKNOWN +- # grpc status code for grpc errors +- assert ( +- cls._exc_to_status(core_exc.InvalidArgument("msg")) +- == StatusCode.INVALID_ARGUMENT +- ) +- assert cls._exc_to_status(core_exc.NotFound("msg")) == StatusCode.NOT_FOUND +- assert ( +- cls._exc_to_status(core_exc.AlreadyExists("msg")) +- == StatusCode.ALREADY_EXISTS +- ) +- assert ( +- cls._exc_to_status(core_exc.PermissionDenied("msg")) +- == StatusCode.PERMISSION_DENIED +- ) +- cause_exc = core_exc.AlreadyExists("msg") +- w_cause = core_exc.DeadlineExceeded("msg") +- w_cause.__cause__ = cause_exc +- assert cls._exc_to_status(w_cause) == StatusCode.DEADLINE_EXCEEDED +- # use cause if available +- w_cause = ValueError("msg") +- w_cause.__cause__ = cause_exc +- cause_exc.grpc_status_code = object() +- custom_excs = [ +- bt_exc.FailedMutationEntryError(1, mock.Mock(), cause=cause_exc), +- bt_exc.FailedQueryShardError(1, {}, cause=cause_exc), +- w_cause, +- ] +- for exc in custom_excs: +- assert cls._exc_to_status(exc) == cause_exc.grpc_status_code, exc +- # extract most recent exception for bigtable exception groups +- exc_groups = [ +- bt_exc._BigtableExceptionGroup("", [ValueError(), cause_exc]), +- bt_exc.RetryExceptionGroup([RuntimeError(), cause_exc]), +- bt_exc.ShardedReadRowsExceptionGroup( +- [bt_exc.FailedQueryShardError(1, {}, cause=cause_exc)], [], 2 +- ), +- bt_exc.MutationsExceptionGroup( +- [bt_exc.FailedMutationEntryError(1, mock.Mock(), cause=cause_exc)], 2 +- ), +- ] +- for exc in exc_groups: +- assert cls._exc_to_status(exc) == cause_exc.grpc_status_code, exc +- +- def test__handle_error(self): +- """ +- handle_error should write log +- """ +- input_message = "test message" +- expected_message = f"Error in Bigtable Metrics: {input_message}" +- with mock.patch( +- "google.cloud.bigtable.data._metrics.data_model.LOGGER" +- ) as logger_mock: +- type(self._make_one(object()))._handle_error(input_message) +- assert logger_mock.warning.call_count == 1 +- assert logger_mock.warning.call_args[0][0] == expected_message +- assert len(logger_mock.warning.call_args[0]) == 1 +- +- @pytest.mark.asyncio +- async def test_context_manager(self): +- """ +- Should implement context manager protocol +- """ +- metric = self._make_one(object()) +- with mock.patch.object(metric, "end_with_success") as end_with_success_mock: +- end_with_success_mock.side_effect = lambda: metric.end_with_status(object()) +- with metric as context: +- assert context == metric +- # inside context manager, still active +- assert end_with_success_mock.call_count == 0 +- assert metric.state == State.CREATED +- # outside context manager, should be ended +- assert end_with_success_mock.call_count == 1 +- assert metric.state == State.COMPLETED +- +- @pytest.mark.asyncio +- async def test_context_manager_exception(self): +- """ +- Exception within context manager causes end_with_status to be called with error +- """ +- expected_exc = ValueError("expected") +- metric = self._make_one(object()) +- with mock.patch.object(metric, "end_with_status") as end_with_status_mock: +- try: +- with metric: +- # inside context manager, still active +- assert end_with_status_mock.call_count == 0 +- assert metric.state == State.CREATED +- raise expected_exc +- except ValueError as e: +- assert e == expected_exc +- # outside context manager, should be ended +- assert end_with_status_mock.call_count == 1 +- assert end_with_status_mock.call_args[0][0] == expected_exc +diff --git a/tests/unit/data/_metrics/test_metrics_controller.py b/tests/unit/data/_metrics/test_metrics_controller.py +deleted file mode 100644 +index 125c2be1..00000000 +--- a/tests/unit/data/_metrics/test_metrics_controller.py ++++ /dev/null +@@ -1,96 +0,0 @@ +-# Copyright 2025 Google LLC +-# +-# 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 mock +- +- +-class TestBigtableClientSideMetricsController: +- def _make_one(self, *args, **kwargs): +- from google.cloud.bigtable.data._metrics import ( +- BigtableClientSideMetricsController, +- ) +- +- return BigtableClientSideMetricsController(*args, **kwargs) +- +- def test_ctor_defaults(self): +- """ +- should create instance with GCP Exporter handler by default +- """ +- instance = self._make_one() +- assert len(instance.handlers) == 0 +- +- def ctor_custom_handlers(self): +- """ +- if handlers are passed to init, use those instead +- """ +- custom_handler = object() +- custom_interceptor = object() +- controller = self._make_one(custom_interceptor, handlers=[custom_handler]) +- assert controller.interceptor == custom_interceptor +- assert len(controller.handlers) == 1 +- assert controller.handlers[0] is custom_handler +- +- def test_add_handler(self): +- """ +- New handlers should be added to list +- """ +- controller = self._make_one(handlers=[object()]) +- initial_handler_count = len(controller.handlers) +- new_handler = object() +- controller.add_handler(new_handler) +- assert len(controller.handlers) == initial_handler_count + 1 +- assert controller.handlers[-1] is new_handler +- +- def test_create_operation_mock(self): +- """ +- All args should be passed through, as well as the handlers +- """ +- from google.cloud.bigtable.data._metrics import ActiveOperationMetric +- +- controller = self._make_one(handlers=[object()]) +- arg = object() +- kwargs = {"a": 1, "b": 2} +- with mock.patch( +- "google.cloud.bigtable.data._metrics.ActiveOperationMetric.__init__" +- ) as mock_op: +- mock_op.return_value = None +- op = controller.create_operation(arg, **kwargs) +- assert isinstance(op, ActiveOperationMetric) +- assert mock_op.call_count == 1 +- mock_op.assert_called_with(arg, **kwargs, handlers=controller.handlers) +- +- def test_create_operation(self): +- from google.cloud.bigtable.data._metrics import ActiveOperationMetric +- +- handler = object() +- expected_type = object() +- expected_is_streaming = True +- expected_zone = object() +- controller = self._make_one(handlers=[handler]) +- op = controller.create_operation( +- expected_type, is_streaming=expected_is_streaming, zone=expected_zone +- ) +- assert isinstance(op, ActiveOperationMetric) +- assert op.op_type is expected_type +- assert op.is_streaming is expected_is_streaming +- assert op.zone is expected_zone +- assert len(op.handlers) == 1 +- assert op.handlers[0] is handler +- +- def test_close(self): +- handlers = [mock.Mock() for _ in range(3)] +- controller = self._make_one(handlers=handlers) +- controller.close() +- for handler in handlers: +- handler.close.assert_called_once() +diff --git a/tests/unit/data/_metrics/test_tracked_retry.py b/tests/unit/data/_metrics/test_tracked_retry.py +deleted file mode 100644 +index 39713dc6..00000000 +--- a/tests/unit/data/_metrics/test_tracked_retry.py ++++ /dev/null +@@ -1,232 +0,0 @@ +-# Copyright 2025 Google LLC +-# +-# 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 pytest +-import inspect +-import mock +-import sys +-from grpc import StatusCode +-from google.api_core import exceptions as core_exceptions +-from google.api_core.retry import RetryFailureReason +-import google.api_core.retry as retry_module +- +- +-class TestTrackRetryableError: +- def _call_fut(self, operation): +- from google.cloud.bigtable.data._metrics.tracked_retry import ( +- _track_retryable_error, +- ) +- +- return _track_retryable_error(operation) +- +- def test_basic_exception(self): +- """should call operation.end_attempt_with_status with the exception for basic exceptions.""" +- operation = mock.Mock() +- wrapper = self._call_fut(operation) +- +- exc = RuntimeError("test") +- wrapper(exc) +- +- operation.end_attempt_with_status.assert_called_once_with(exc) +- +- def test_mutate_rows_incomplete(self): +- """should call operation.end_attempt_with_status with StatusCode.OK for _MutateRowsIncomplete exceptions.""" +- from google.cloud.bigtable.data.exceptions import _MutateRowsIncomplete +- +- operation = mock.Mock() +- wrapper = self._call_fut(operation) +- +- exc = _MutateRowsIncomplete("test") +- wrapper(exc) +- +- operation.end_attempt_with_status.assert_called_once_with(StatusCode.OK) +- +- def test_rpc_error_metadata(self): +- """should extract and add metadata from GoogleAPICallError.""" +- operation = mock.Mock() +- wrapper = self._call_fut(operation) +- +- rpc_error = mock.Mock() +- rpc_error.trailing_metadata.return_value = (("key1", "val1"),) +- rpc_error.initial_metadata.return_value = (("key2", "val2"),) +- +- exc = core_exceptions.GoogleAPICallError("test", errors=[rpc_error]) +- wrapper(exc) +- +- operation.add_response_metadata.assert_called_once_with( +- {"key1": "val1", "key2": "val2"} +- ) +- operation.end_attempt_with_status.assert_called_once_with(exc) +- +- def test_metadata_error_ignored(self): +- """should ignore errors during metadata collection.""" +- operation = mock.Mock() +- operation.add_response_metadata.side_effect = RuntimeError("metadata error") +- wrapper = self._call_fut(operation) +- +- rpc_error = mock.Mock() +- rpc_error.trailing_metadata.return_value = () +- rpc_error.initial_metadata.return_value = () +- exc = core_exceptions.GoogleAPICallError("test", errors=[rpc_error]) +- +- # should not raise +- wrapper(exc) +- +- operation.end_attempt_with_status.assert_called_once_with(exc) +- +- +-class TestTrackTerminalError: +- def _call_fut(self, operation, factory): +- from google.cloud.bigtable.data._metrics.tracked_retry import ( +- _track_terminal_error, +- ) +- +- return _track_terminal_error(operation, factory) +- +- def test_basic_pass_through(self): +- """should call the exception_factory and end the operation with its result.""" +- operation = mock.Mock() +- factory = mock.Mock() +- expected_exc = RuntimeError("source") +- expected_cause = RuntimeError("cause") +- factory.return_value = (expected_exc, expected_cause) +- +- wrapper = self._call_fut(operation, factory) +- +- exc_list = [RuntimeError("attempt1")] +- reason = RetryFailureReason.TIMEOUT +- timeout_val = 1.0 +- +- result = wrapper(exc_list, reason, timeout_val) +- +- assert result == (expected_exc, expected_cause) +- factory.assert_called_once_with(exc_list, reason, timeout_val) +- operation.end_with_status.assert_called_once_with(expected_exc) +- +- def test_timeout_active_attempt(self): +- """should end attempt if fails on timeout.""" +- from google.cloud.bigtable.data._metrics import OperationState +- +- operation = mock.Mock() +- operation.state = OperationState.ACTIVE_ATTEMPT +- factory = mock.Mock() +- factory.return_value = (RuntimeError("timeout"), None) +- +- wrapper = self._call_fut(operation, factory) +- +- last_exc = RuntimeError("last attempt error") +- exc_list = [last_exc] +- +- wrapper(exc_list, RetryFailureReason.TIMEOUT, 1.0) +- +- # expect call to end_attempt_with_status via the _track_retryable_error logic +- operation.end_attempt_with_status.assert_called_once_with(last_exc) +- operation.end_with_status.assert_called_once() +- +- def test_rpc_error_metadata(self): +- """should extract and add metadata from GoogleAPICallError in terminal errors.""" +- operation = mock.Mock() +- factory = mock.Mock() +- +- rpc_error = mock.Mock() +- rpc_error.trailing_metadata.return_value = (("k", "v"),) +- rpc_error.initial_metadata.return_value = () +- source_exc = core_exceptions.GoogleAPICallError("test", errors=[rpc_error]) +- +- factory.return_value = (source_exc, None) +- +- wrapper = self._call_fut(operation, factory) +- wrapper([], RetryFailureReason.NON_RETRYABLE_ERROR, None) +- +- operation.add_response_metadata.assert_called_once_with({"k": "v"}) +- operation.end_with_status.assert_called_once_with(source_exc) +- +- +-class TestTrackedRetry: +- def _call_fut(self, **kwargs): +- from google.cloud.bigtable.data._metrics.tracked_retry import tracked_retry +- +- return tracked_retry(**kwargs) +- +- def test_call_args(self): +- """should correctly pass arguments to the retry_fn.""" +- operation = mock.Mock() +- retry_fn = mock.Mock() +- retry_fn.return_value = "result" +- +- result = self._call_fut(retry_fn=retry_fn, operation=operation, other_arg=123) +- +- assert result == "result" +- retry_fn.assert_called_once() +- call_kwargs = retry_fn.call_args[1] +- +- assert call_kwargs["sleep_generator"] == operation.backoff_generator +- assert "on_error" in call_kwargs +- assert "exception_factory" in call_kwargs +- assert call_kwargs["other_arg"] == 123 +- +- def test_tracked_retry_wraps_components(self): +- """should wrap on_error and exception_factory with tracking logic.""" +- from google.cloud.bigtable.data._metrics import tracked_retry +- +- module = sys.modules[tracked_retry.__module__] +- +- with mock.patch.object(module, "_track_retryable_error") as mock_track_retry: +- with mock.patch.object( +- module, "_track_terminal_error" +- ) as mock_track_terminal: +- operation = mock.Mock() +- retry_fn = mock.Mock() +- custom_factory = mock.Mock() +- +- self._call_fut( +- retry_fn=retry_fn, +- operation=operation, +- exception_factory=custom_factory, +- arg=1, +- ) +- +- mock_track_retry.assert_called_once_with(operation) +- mock_track_terminal.assert_called_once_with(operation, custom_factory) +- +- retry_fn.assert_called_once_with( +- sleep_generator=operation.backoff_generator, +- on_error=mock_track_retry.return_value, +- exception_factory=mock_track_terminal.return_value, +- arg=1, +- ) +- +- @pytest.mark.parametrize( +- "fn_name,type_verifier", +- [ +- ("retry_target", callable), +- ("retry_target_stream", inspect.isgenerator), +- ("retry_target_async", inspect.iscoroutine), +- ("retry_target_stream_async", inspect.isasyncgen), +- ], +- ) +- def test_wrapping_api_core(self, fn_name, type_verifier): +- """Test building tracked retry from different supported retry functions""" +- from google.cloud.bigtable.data._metrics import ActiveOperationMetric +- +- operation = ActiveOperationMetric("type") +- fn = getattr(retry_module, fn_name) +- tracked_retry = self._call_fut( +- retry_fn=fn, +- operation=operation, +- target=mock.Mock(), +- timeout=None, +- predicate=lambda x: False, +- ) +- assert type_verifier(tracked_retry) +diff --git a/tests/unit/data/_sync_autogen/test_client.py b/tests/unit/data/_sync_autogen/test_client.py +index 54be1f17..49ed41ad 100644 +--- a/tests/unit/data/_sync_autogen/test_client.py ++++ b/tests/unit/data/_sync_autogen/test_client.py +@@ -47,13 +47,9 @@ from tests.unit.data.execute_query.sql_helpers import ( + ) + from google.api_core import grpc_helpers + from google.cloud.bigtable.data._sync_autogen._swappable_channel import SwappableChannel +-from google.cloud.bigtable.data._sync_autogen.metrics_interceptor import ( +- BigtableMetricsInterceptor, +-) + + CrossSync._Sync_Impl.add_mapping("grpc_helpers", grpc_helpers) + CrossSync._Sync_Impl.add_mapping("SwappableChannel", SwappableChannel) +-CrossSync._Sync_Impl.add_mapping("MetricsInterceptor", BigtableMetricsInterceptor) + + + @CrossSync._Sync_Impl.add_mapping_decorator("TestBigtableDataClient") +@@ -89,9 +85,6 @@ class TestBigtableDataClient: + assert not client._active_instances + assert client._channel_refresh_task is not None + assert client.transport._credentials == expected_credentials +- assert isinstance( +- client._metrics_interceptor, CrossSync._Sync_Impl.MetricsInterceptor +- ) + client.close() + + def test_ctor_super_inits(self): +@@ -940,9 +933,6 @@ class TestTable: + + def test_ctor(self): + from google.cloud.bigtable.data._helpers import _WarmedInstanceKey +- from google.cloud.bigtable.data._metrics import ( +- BigtableClientSideMetricsController, +- ) + + expected_table_id = "table-id" + expected_instance_id = "instance-id" +@@ -983,7 +973,6 @@ class TestTable: + instance_key = _WarmedInstanceKey(table.instance_name, table.app_profile_id) + assert instance_key in client._active_instances + assert client._instance_owners[instance_key] == {id(table)} +- assert isinstance(table._metrics, BigtableClientSideMetricsController) + assert table.default_operation_timeout == expected_operation_timeout + assert table.default_attempt_timeout == expected_attempt_timeout + assert ( +@@ -1176,21 +1165,6 @@ class TestTable: + else: + assert "app_profile_id=" in routing_str + +- def test_close(self): +- client = self._make_client() +- table = self._make_one(client) +- with mock.patch.object( +- table._metrics, "close", mock.Mock() +- ) as metric_close_mock: +- with mock.patch.object( +- client, "_remove_instance_registration" +- ) as remove_mock: +- table.close() +- remove_mock.assert_called_once_with( +- table.instance_id, table.app_profile_id, id(table) +- ) +- metric_close_mock.assert_called_once() +- + + @CrossSync._Sync_Impl.add_mapping_decorator("TestAuthorizedView") + class TestAuthorizedView(CrossSync._Sync_Impl.TestTable): +@@ -1217,9 +1191,6 @@ class TestAuthorizedView(CrossSync._Sync_Impl.TestTable): + + def test_ctor(self): + from google.cloud.bigtable.data._helpers import _WarmedInstanceKey +- from google.cloud.bigtable.data._metrics import ( +- BigtableClientSideMetricsController, +- ) + + expected_table_id = "table-id" + expected_instance_id = "instance-id" +@@ -1267,7 +1238,6 @@ class TestAuthorizedView(CrossSync._Sync_Impl.TestTable): + instance_key = _WarmedInstanceKey(view.instance_name, view.app_profile_id) + assert instance_key in client._active_instances + assert client._instance_owners[instance_key] == {id(view)} +- assert isinstance(view._metrics, BigtableClientSideMetricsController) + assert view.default_operation_timeout == expected_operation_timeout + assert view.default_attempt_timeout == expected_attempt_timeout + assert ( +@@ -1463,7 +1433,8 @@ class TestReadRows: + ) + + @pytest.mark.parametrize( +- "per_request_t, operation_t, expected_num", [(0.1, 0.19, 2), (0.1, 0.29, 3)] ++ "per_request_t, operation_t, expected_num", ++ [(0.05, 0.08, 2), (0.05, 0.14, 3), (0.05, 0.24, 5)], + ) + def test_read_rows_attempt_timeout(self, per_request_t, operation_t, expected_num): + """Ensures that the attempt_timeout is respected and that the number of +diff --git a/tests/unit/data/_sync_autogen/test_metrics_interceptor.py b/tests/unit/data/_sync_autogen/test_metrics_interceptor.py +index c4efcc5b..56a6f365 100644 +--- a/tests/unit/data/_sync_autogen/test_metrics_interceptor.py ++++ b/tests/unit/data/_sync_autogen/test_metrics_interceptor.py +@@ -17,9 +17,6 @@ + + import pytest + from grpc import RpcError +-from grpc import ClientCallDetails +-from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric +-from google.cloud.bigtable.data._metrics.data_model import OperationState + from google.cloud.bigtable.data._cross_sync import CrossSync + + try: +@@ -53,255 +50,91 @@ class TestMetricsInterceptor: + def _make_one(self, *args, **kwargs): + return self._get_target_class()(*args, **kwargs) + +- def test_unary_unary_interceptor_op_not_found(self): +- """Test that interceptor call continuation if op is not found""" +- instance = self._make_one() +- continuation = CrossSync._Sync_Impl.Mock() +- details = ClientCallDetails() +- details.metadata = [] +- request = mock.Mock() +- instance.intercept_unary_unary(continuation, details, request) +- continuation.assert_called_once_with(details, request) +- + def test_unary_unary_interceptor_success(self): + """Test that interceptor handles successful unary-unary calls""" + instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- ActiveOperationMetric._active_operation_context.set(op) + continuation = CrossSync._Sync_Impl.Mock() + call = continuation.return_value +- call.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[("a", "b")]) +- call.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[("c", "d")]) +- details = ClientCallDetails() ++ details = mock.Mock() + request = mock.Mock() + result = instance.intercept_unary_unary(continuation, details, request) + assert result == call + continuation.assert_called_once_with(details, request) +- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) +- op.end_attempt_with_status.assert_not_called() + + def test_unary_unary_interceptor_failure(self): +- """test a failed RpcError with metadata""" +- instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- ActiveOperationMetric._active_operation_context.set(op) +- exc = RpcError("test") +- exc.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[("a", "b")]) +- exc.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[("c", "d")]) +- continuation = CrossSync._Sync_Impl.Mock(side_effect=exc) +- details = ClientCallDetails() +- request = mock.Mock() +- with pytest.raises(RpcError) as e: +- instance.intercept_unary_unary(continuation, details, request) +- assert e.value == exc +- continuation.assert_called_once_with(details, request) +- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) +- +- def test_unary_unary_interceptor_failure_no_metadata(self): +- """test with RpcError without without metadata attached""" ++ """Test a failed RpcError with metadata""" + instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- ActiveOperationMetric._active_operation_context.set(op) + exc = RpcError("test") + continuation = CrossSync._Sync_Impl.Mock(side_effect=exc) +- call = continuation.return_value +- call.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[("a", "b")]) +- call.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[("c", "d")]) +- details = ClientCallDetails() ++ details = mock.Mock() + request = mock.Mock() + with pytest.raises(RpcError) as e: + instance.intercept_unary_unary(continuation, details, request) + assert e.value == exc + continuation.assert_called_once_with(details, request) +- op.add_response_metadata.assert_not_called() + + def test_unary_unary_interceptor_failure_generic(self): +- """test generic exception""" ++ """Test generic exception""" + instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- ActiveOperationMetric._active_operation_context.set(op) + exc = ValueError("test") + continuation = CrossSync._Sync_Impl.Mock(side_effect=exc) +- call = continuation.return_value +- call.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[("a", "b")]) +- call.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[("c", "d")]) +- details = ClientCallDetails() ++ details = mock.Mock() + request = mock.Mock() + with pytest.raises(ValueError) as e: + instance.intercept_unary_unary(continuation, details, request) + assert e.value == exc + continuation.assert_called_once_with(details, request) +- op.add_response_metadata.assert_not_called() +- +- def test_unary_stream_interceptor_op_not_found(self): +- """Test that interceptor calls continuation if op is not found""" +- instance = self._make_one() +- continuation = CrossSync._Sync_Impl.Mock() +- details = ClientCallDetails() +- details.metadata = [] +- request = mock.Mock() +- instance.intercept_unary_stream(continuation, details, request) +- continuation.assert_called_once_with(details, request) + + def test_unary_stream_interceptor_success(self): + """Test that interceptor handles successful unary-stream calls""" + instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- op.start_time_ns = 0 +- op.first_response_latency = None +- ActiveOperationMetric._active_operation_context.set(op) + continuation = CrossSync._Sync_Impl.Mock( + return_value=_make_mock_stream_call([1, 2]) + ) +- call = continuation.return_value +- call.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[("a", "b")]) +- call.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[("c", "d")]) +- details = ClientCallDetails() ++ details = mock.Mock() + request = mock.Mock() + wrapper = instance.intercept_unary_stream(continuation, details, request) + results = [val for val in wrapper] + assert results == [1, 2] + continuation.assert_called_once_with(details, request) +- assert op.first_response_latency_ns is not None +- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) +- op.end_attempt_with_status.assert_not_called() + + def test_unary_stream_interceptor_failure_mid_stream(self): + """Test that interceptor handles failures mid-stream""" +- from grpc.aio import AioRpcError, Metadata +- + instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- op.start_time_ns = 0 +- op.first_response_latency = None +- ActiveOperationMetric._active_operation_context.set(op) +- exc = AioRpcError(0, Metadata(), Metadata(("a", "b"), ("c", "d"))) ++ exc = ValueError("test") + continuation = CrossSync._Sync_Impl.Mock( + return_value=_make_mock_stream_call([1], exc=exc) + ) +- details = ClientCallDetails() ++ details = mock.Mock() + request = mock.Mock() + wrapper = instance.intercept_unary_stream(continuation, details, request) +- with pytest.raises(AioRpcError) as e: ++ with pytest.raises(ValueError) as e: + [val for val in wrapper] + assert e.value == exc + continuation.assert_called_once_with(details, request) +- assert op.first_response_latency_ns is not None +- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) + + def test_unary_stream_interceptor_failure_start_stream(self): + """Test that interceptor handles failures at start of stream with RpcError with metadata""" + instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- op.start_time_ns = 0 +- op.first_response_latency = None +- ActiveOperationMetric._active_operation_context.set(op) +- exc = RpcError("test") +- exc.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[("a", "b")]) +- exc.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[("c", "d")]) +- continuation = CrossSync._Sync_Impl.Mock() +- continuation.side_effect = exc +- details = ClientCallDetails() +- request = mock.Mock() +- with pytest.raises(RpcError) as e: +- instance.intercept_unary_stream(continuation, details, request) +- assert e.value == exc +- continuation.assert_called_once_with(details, request) +- assert op.first_response_latency_ns is not None +- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) +- +- def test_unary_stream_interceptor_failure_start_stream_no_metadata(self): +- """Test that interceptor handles failures at start of stream with RpcError with no metadata""" +- instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- op.start_time_ns = 0 +- op.first_response_latency = None +- ActiveOperationMetric._active_operation_context.set(op) + exc = RpcError("test") + continuation = CrossSync._Sync_Impl.Mock() + continuation.side_effect = exc +- details = ClientCallDetails() ++ details = mock.Mock() + request = mock.Mock() + with pytest.raises(RpcError) as e: + instance.intercept_unary_stream(continuation, details, request) + assert e.value == exc + continuation.assert_called_once_with(details, request) +- assert op.first_response_latency_ns is not None +- op.add_response_metadata.assert_not_called() + + def test_unary_stream_interceptor_failure_start_stream_generic(self): + """Test that interceptor handles failures at start of stream with generic exception""" + instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = OperationState.ACTIVE_ATTEMPT +- op.start_time_ns = 0 +- op.first_response_latency = None +- ActiveOperationMetric._active_operation_context.set(op) + exc = ValueError("test") + continuation = CrossSync._Sync_Impl.Mock() + continuation.side_effect = exc +- details = ClientCallDetails() ++ details = mock.Mock() + request = mock.Mock() + with pytest.raises(ValueError) as e: + instance.intercept_unary_stream(continuation, details, request) + assert e.value == exc + continuation.assert_called_once_with(details, request) +- assert op.first_response_latency_ns is not None +- op.add_response_metadata.assert_not_called() +- +- @pytest.mark.parametrize( +- "initial_state", [OperationState.CREATED, OperationState.BETWEEN_ATTEMPTS] +- ) +- def test_unary_unary_interceptor_start_operation(self, initial_state): +- """if called with a newly created operation, it should be started""" +- instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = initial_state +- ActiveOperationMetric._active_operation_context.set(op) +- continuation = CrossSync._Sync_Impl.Mock() +- call = continuation.return_value +- call.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[]) +- call.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[]) +- details = ClientCallDetails() +- request = mock.Mock() +- instance.intercept_unary_unary(continuation, details, request) +- op.start_attempt.assert_called_once() +- +- @pytest.mark.parametrize( +- "initial_state", [OperationState.CREATED, OperationState.BETWEEN_ATTEMPTS] +- ) +- def test_unary_stream_interceptor_start_operation(self, initial_state): +- """if called with a newly created operation, it should be started""" +- instance = self._make_one() +- op = mock.Mock() +- op.uuid = "test-uuid" +- op.state = initial_state +- ActiveOperationMetric._active_operation_context.set(op) +- continuation = CrossSync._Sync_Impl.Mock( +- return_value=_make_mock_stream_call([1, 2]) +- ) +- call = continuation.return_value +- call.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[]) +- call.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[]) +- details = ClientCallDetails() +- request = mock.Mock() +- instance.intercept_unary_stream(continuation, details, request) +- op.start_attempt.assert_called_once() +diff --git a/tests/unit/data/test__helpers.py b/tests/unit/data/test__helpers.py +index c8540024..96c726a2 100644 +--- a/tests/unit/data/test__helpers.py ++++ b/tests/unit/data/test__helpers.py +@@ -266,98 +266,3 @@ class TestGetRetryableErrors: + setattr(fake_table, f"{key}_retryable_errors", input_table[key]) + result = _helpers._get_retryable_errors(input_codes, fake_table) + assert result == expected +- +- +-class TestTrackedBackoffGenerator: +- def test_tracked_backoff_generator_history(self): +- """ +- Should be able to retrieve historical results from backoff generator +- """ +- generator = _helpers.TrackedBackoffGenerator( +- initial=0, multiplier=2, maximum=10 +- ) +- got_list = [next(generator) for _ in range(20)] +- +- # check all values are correct +- for i in range(19, 0, -1): +- assert generator.get_attempt_backoff(i) == got_list[i] +- # check a random value out of order +- assert generator.get_attempt_backoff(5) == got_list[5] +- +- @mock.patch("random.uniform", side_effect=lambda a, b: b) +- def test_tracked_backoff_generator_defaults(self, mock_uniform): +- """ +- Should generate values with default parameters +- +- initial=0.01, multiplier=2, maximum=60 +- """ +- generator = _helpers.TrackedBackoffGenerator() +- expected_values = [0.01, 0.02, 0.04, 0.08, 0.16] +- for expected in expected_values: +- assert next(generator) == pytest.approx(expected) +- +- @mock.patch("random.uniform", side_effect=lambda a, b: b) +- def test_tracked_backoff_generator_with_maximum(self, mock_uniform): +- """ +- Should cap the backoff at the maximum value +- """ +- generator = _helpers.TrackedBackoffGenerator(initial=1, multiplier=2, maximum=5) +- expected_values = [1, 2, 4, 5, 5, 5] +- for expected in expected_values: +- assert next(generator) == expected +- +- def test_get_attempt_backoff_out_of_bounds(self): +- """ +- get_attempt_backoff should raise IndexError for out of bounds index +- """ +- generator = _helpers.TrackedBackoffGenerator() +- next(generator) +- next(generator) +- with pytest.raises(IndexError): +- generator.get_attempt_backoff(2) +- with pytest.raises(IndexError): +- generator.get_attempt_backoff(-3) +- +- def test_set_next_full_set(self): +- """ +- try always using set_next to populate generator +- """ +- generator = _helpers.TrackedBackoffGenerator() +- for idx, val in enumerate(range(100, 0, -1)): +- generator.set_next(val) +- got = next(generator) +- assert got == val +- assert generator.get_attempt_backoff(idx) == val +- +- def test_set_next_negative_value(self): +- generator = _helpers.TrackedBackoffGenerator() +- with pytest.raises(ValueError): +- generator.set_next(-1) +- +- @mock.patch("random.uniform", side_effect=lambda a, b: b) +- def test_interleaved_set_next(self, mock_uniform): +- import itertools +- +- generator = _helpers.TrackedBackoffGenerator( +- initial=1, multiplier=2, maximum=128 +- ) +- # values we expect generator to create +- expected_values = [2**i for i in range(8)] +- # values we will insert +- inserted_values = [9, 61, 0, 4, 33, 12, 18, 2] +- for idx in range(8): +- assert next(generator) == expected_values[idx] +- generator.set_next(inserted_values[idx]) +- assert next(generator) == inserted_values[idx] +- # check to make sure history is as we expect +- generator.history = itertools.chain.from_iterable( +- zip(expected_values, inserted_values) +- ) +- +- @mock.patch("random.uniform", side_effect=lambda a, b: b) +- def test_set_next_replacement(self, mock_uniform): +- generator = _helpers.TrackedBackoffGenerator(initial=1) +- generator.set_next(99) +- generator.set_next(88) +- assert next(generator) == 88 +- assert next(generator) == 1 +diff --git a/tests/unit/v2_client/test_backup.py b/tests/unit/v2_client/test_backup.py +index cc9251a3..a5d205af 100644 +--- a/tests/unit/v2_client/test_backup.py ++++ b/tests/unit/v2_client/test_backup.py +@@ -19,7 +19,6 @@ import mock + import pytest + + from ._testing import _make_credentials +-from google.cloud._helpers import UTC + + PROJECT_ID = "project-id" + INSTANCE_ID = "instance-id" +@@ -38,7 +37,7 @@ ALT_BACKUP_NAME = ALT_CLUSTER_NAME + "/backups/" + BACKUP_ID + + + def _make_timestamp(): +- return datetime.datetime.utcnow().replace(tzinfo=UTC) ++ return datetime.datetime.now(datetime.timezone.utc) + + + def _make_table_admin_client(): +diff --git a/tests/unit/v2_client/test_cluster.py b/tests/unit/v2_client/test_cluster.py +index 65ed4743..a2110454 100644 +--- a/tests/unit/v2_client/test_cluster.py ++++ b/tests/unit/v2_client/test_cluster.py +@@ -420,7 +420,7 @@ def test_cluster_create(): + from google.cloud.bigtable_admin_v2.types import instance as instance_v2_pb2 + from google.cloud.bigtable.enums import StorageType + +- NOW = datetime.datetime.utcnow() ++ NOW = datetime.datetime.now(datetime.timezone.utc) + NOW_PB = _datetime_to_pb_timestamp(NOW) + credentials = _make_credentials() + client = _make_client(project=PROJECT, credentials=credentials, admin=True) +@@ -475,7 +475,7 @@ def test_cluster_create_w_cmek(): + from google.cloud.bigtable_admin_v2.types import instance as instance_v2_pb2 + from google.cloud.bigtable.enums import StorageType + +- NOW = datetime.datetime.utcnow() ++ NOW = datetime.datetime.now(datetime.timezone.utc) + NOW_PB = _datetime_to_pb_timestamp(NOW) + credentials = _make_credentials() + client = _make_client(project=PROJECT, credentials=credentials, admin=True) +@@ -535,7 +535,7 @@ def test_cluster_create_w_autoscaling(): + from google.cloud.bigtable_admin_v2.types import instance as instance_v2_pb2 + from google.cloud.bigtable.enums import StorageType + +- NOW = datetime.datetime.utcnow() ++ NOW = datetime.datetime.now(datetime.timezone.utc) + NOW_PB = _datetime_to_pb_timestamp(NOW) + credentials = _make_credentials() + client = _make_client(project=PROJECT, credentials=credentials, admin=True) +@@ -602,7 +602,7 @@ def test_cluster_update(): + ) + from google.cloud.bigtable.enums import StorageType + +- NOW = datetime.datetime.utcnow() ++ NOW = datetime.datetime.now(datetime.timezone.utc) + NOW_PB = _datetime_to_pb_timestamp(NOW) + + credentials = _make_credentials() +@@ -669,7 +669,7 @@ def test_cluster_update_w_autoscaling(): + ) + from google.cloud.bigtable.enums import StorageType + +- NOW = datetime.datetime.utcnow() ++ NOW = datetime.datetime.now(datetime.timezone.utc) + NOW_PB = _datetime_to_pb_timestamp(NOW) + + credentials = _make_credentials() +@@ -728,7 +728,7 @@ def test_cluster_update_w_partial_autoscaling_config(): + ) + from google.cloud.bigtable.enums import StorageType + +- NOW = datetime.datetime.utcnow() ++ NOW = datetime.datetime.now(datetime.timezone.utc) + NOW_PB = _datetime_to_pb_timestamp(NOW) + + credentials = _make_credentials() +@@ -812,7 +812,7 @@ def test_cluster_update_w_both_manual_and_autoscaling(): + ) + from google.cloud.bigtable.enums import StorageType + +- NOW = datetime.datetime.utcnow() ++ NOW = datetime.datetime.now(datetime.timezone.utc) + NOW_PB = _datetime_to_pb_timestamp(NOW) + + credentials = _make_credentials() +@@ -873,7 +873,7 @@ def test_cluster_disable_autoscaling(): + from google.cloud.bigtable.instance import Instance + from google.cloud.bigtable.enums import StorageType + +- NOW = datetime.datetime.utcnow() ++ NOW = datetime.datetime.now(datetime.timezone.utc) + NOW_PB = _datetime_to_pb_timestamp(NOW) + credentials = _make_credentials() + client = _make_client(project=PROJECT, credentials=credentials, admin=True) +diff --git a/tests/unit/v2_client/test_instance.py b/tests/unit/v2_client/test_instance.py +index 712fab1f..c5ef9c9b 100644 +--- a/tests/unit/v2_client/test_instance.py ++++ b/tests/unit/v2_client/test_instance.py +@@ -277,7 +277,7 @@ def _instance_api_response_for_create(): + ) + from google.cloud.bigtable_admin_v2.types import instance + +- NOW = datetime.datetime.utcnow() ++ NOW = datetime.datetime.now(datetime.timezone.utc) + NOW_PB = _datetime_to_pb_timestamp(NOW) + metadata = messages_v2_pb2.CreateInstanceMetadata(request_time=NOW_PB) + type_url = "type.googleapis.com/{}".format( +@@ -503,7 +503,7 @@ def _instance_api_response_for_update(): + ) + from google.cloud.bigtable_admin_v2.types import instance + +- NOW = datetime.datetime.utcnow() ++ NOW = datetime.datetime.now(datetime.timezone.utc) + NOW_PB = _datetime_to_pb_timestamp(NOW) + metadata = messages_v2_pb2.UpdateInstanceMetadata(request_time=NOW_PB) + type_url = "type.googleapis.com/{}".format( +diff --git a/tests/unit/v2_client/test_table.py b/tests/unit/v2_client/test_table.py +index 1d183e2f..6b31a5e2 100644 +--- a/tests/unit/v2_client/test_table.py ++++ b/tests/unit/v2_client/test_table.py +@@ -1378,13 +1378,12 @@ def test_table_backup_factory_defaults(): + + def test_table_backup_factory_non_defaults(): + import datetime +- from google.cloud._helpers import UTC + from google.cloud.bigtable.backup import Backup + from google.cloud.bigtable.instance import Instance + + instance = Instance(INSTANCE_ID, None) + table = _make_table(TABLE_ID, instance) +- timestamp = datetime.datetime.utcnow().replace(tzinfo=UTC) ++ timestamp = datetime.datetime.now(datetime.timezone.utc) + backup = table.backup( + BACKUP_ID, + cluster_id=CLUSTER_ID, diff --git a/packages/google-cloud-bigtable/samples/quickstart/main.py b/packages/google-cloud-bigtable/samples/quickstart/main.py new file mode 100644 index 000000000000..50bfe639426c --- /dev/null +++ b/packages/google-cloud-bigtable/samples/quickstart/main.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python + +# Copyright 2018 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. + +# [START bigtable_quickstart] +import argparse + +from google.cloud import bigtable + + +def main(project_id="project-id", instance_id="instance-id", table_id="my-table"): + # Create a Cloud Bigtable client. + client = bigtable.Client(project=project_id) + + # Connect to an existing Cloud Bigtable instance. + instance = client.instance(instance_id) + + # Open an existing table. + table = instance.table(table_id) + + row_key = "r1" + row = table.read_row(row_key.encode("utf-8")) + + column_family_id = "cf1" + column_id = "c1".encode("utf-8") + value = row.cells[column_family_id][column_id][0].value.decode("utf-8") + + print("Row key: {}\nData: {}".format(row_key, value)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("project_id", help="Your Cloud Platform project ID.") + parser.add_argument( + "instance_id", help="ID of the Cloud Bigtable instance to connect to." + ) + parser.add_argument( + "--table", help="Existing table used in the quickstart.", default="my-table" + ) + + args = parser.parse_args() + main(args.project_id, args.instance_id, args.table) +# [END bigtable_quickstart] diff --git a/packages/google-cloud-bigtable/samples/quickstart/main_async.py b/packages/google-cloud-bigtable/samples/quickstart/main_async.py new file mode 100644 index 000000000000..c38985592e42 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/quickstart/main_async.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +# Copyright 2024 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. + +# [START bigtable_quickstart_asyncio] +import argparse +import asyncio + +from google.cloud.bigtable.data import BigtableDataClientAsync + + +async def main(project_id="project-id", instance_id="instance-id", table_id="my-table"): + # Create a Cloud Bigtable client. + client = BigtableDataClientAsync(project=project_id) + + # Open an existing table. + table = client.get_table(instance_id, table_id) + + row_key = "r1" + row = await table.read_row(row_key) + + column_family_id = "cf1" + column_id = b"c1" + value = row.get_cells(column_family_id, column_id)[0].value.decode("utf-8") + + await table.close() + await client.close() + + print("Row key: {}\nData: {}".format(row_key, value)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("project_id", help="Your Cloud Platform project ID.") + parser.add_argument( + "instance_id", help="ID of the Cloud Bigtable instance to connect to." + ) + parser.add_argument( + "--table", help="Existing table used in the quickstart.", default="my-table" + ) + + args = parser.parse_args() + asyncio.get_event_loop().run_until_complete( + main(args.project_id, args.instance_id, args.table) + ) + +# [END bigtable_quickstart_asyncio] diff --git a/packages/google-cloud-bigtable/samples/quickstart/main_async_test.py b/packages/google-cloud-bigtable/samples/quickstart/main_async_test.py new file mode 100644 index 000000000000..0749cbd316a3 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/quickstart/main_async_test.py @@ -0,0 +1,49 @@ +# Copyright 2024 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 os +import uuid +from typing import AsyncGenerator + +from google.cloud.bigtable.data import BigtableDataClientAsync, SetCell +import pytest +import pytest_asyncio + +from .main_async import main +from ..utils import create_table_cm + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] +TABLE_ID = f"quickstart-async-test-{str(uuid.uuid4())[:16]}" + + +@pytest_asyncio.fixture +async def table_id() -> AsyncGenerator[str, None]: + with create_table_cm(PROJECT, BIGTABLE_INSTANCE, TABLE_ID, {"cf1": None}): + await _populate_table(TABLE_ID) + yield TABLE_ID + + +async def _populate_table(table_id: str): + async with BigtableDataClientAsync(project=PROJECT) as client: + async with client.get_table(BIGTABLE_INSTANCE, table_id) as table: + await table.mutate_row("r1", SetCell("cf1", "c1", "test-value")) + + +@pytest.mark.asyncio +async def test_main(capsys, table_id): + await main(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + assert "Row key: r1\nData: test-value\n" in out diff --git a/packages/google-cloud-bigtable/samples/quickstart/main_test.py b/packages/google-cloud-bigtable/samples/quickstart/main_test.py new file mode 100644 index 000000000000..f58161f231b1 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/quickstart/main_test.py @@ -0,0 +1,46 @@ +# Copyright 2018 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 os +import uuid +import pytest + +from .main import main + +from ..utils import create_table_cm + + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] +TABLE_ID = f"quickstart-test-{str(uuid.uuid4())[:16]}" + + +@pytest.fixture() +def table(): + column_family_id = "cf1" + column_families = {column_family_id: None} + with create_table_cm(PROJECT, BIGTABLE_INSTANCE, TABLE_ID, column_families) as table: + row = table.direct_row("r1") + row.set_cell(column_family_id, "c1", "test-value") + row.commit() + + yield TABLE_ID + + +def test_main(capsys, table): + table_id = table + main(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + assert "Row key: r1\nData: test-value\n" in out diff --git a/packages/google-cloud-bigtable/samples/quickstart/noxfile.py b/packages/google-cloud-bigtable/samples/quickstart/noxfile.py new file mode 100644 index 000000000000..a169b5b5b464 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/quickstart/noxfile.py @@ -0,0 +1,292 @@ +# Copyright 2019 Google LLC +# +# 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 __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, Optional + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in ( + "True", + "true", +) + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8") + else: + session.install("flake8", "flake8-annotations") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# format = isort + black +# + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list.extend(glob.glob("**/tests", recursive=True)) + + if len(test_list) == 0: + print("No tests found, skipping directory.") + return + + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + concurrent_args = [] + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + with open("requirements.txt") as rfile: + packages = rfile.read() + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install( + "-r", "requirements-test.txt", "-c", "constraints-test.txt" + ) + else: + session.install("-r", "requirements-test.txt") + with open("requirements-test.txt") as rtfile: + packages += rtfile.read() + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + if "pytest-parallel" in packages: + concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + elif "pytest-xdist" in packages: + concurrent_args.extend(['-n', 'auto']) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs + concurrent_args), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/packages/google-cloud-bigtable/samples/quickstart/requirements-test.txt b/packages/google-cloud-bigtable/samples/quickstart/requirements-test.txt new file mode 100644 index 000000000000..ee4ba018603b --- /dev/null +++ b/packages/google-cloud-bigtable/samples/quickstart/requirements-test.txt @@ -0,0 +1,2 @@ +pytest +pytest-asyncio diff --git a/packages/google-cloud-bigtable/samples/quickstart/requirements.txt b/packages/google-cloud-bigtable/samples/quickstart/requirements.txt new file mode 100644 index 000000000000..730d25dec63f --- /dev/null +++ b/packages/google-cloud-bigtable/samples/quickstart/requirements.txt @@ -0,0 +1 @@ +google-cloud-bigtable==2.35.0 diff --git a/packages/google-cloud-bigtable/samples/quickstart_happybase/README.md b/packages/google-cloud-bigtable/samples/quickstart_happybase/README.md new file mode 100644 index 000000000000..6d4d8871e3cb --- /dev/null +++ b/packages/google-cloud-bigtable/samples/quickstart_happybase/README.md @@ -0,0 +1,52 @@ +[//]: # "This README.md file is auto-generated, all changes to this file will be lost." +[//]: # "To regenerate it, use `python -m synthtool`." + +## Python Samples for Cloud Bigtable + +This directory contains samples for Cloud Bigtable, which may be used as a refererence for how to use this product. +Samples, quickstarts, and other documentation are available at cloud.google.com. + + +### Quickstart using HappyBase + +Demonstrates of Cloud Bigtable using HappyBase. This sample creates a Bigtable client, connects to an instance and then to a table, then closes the connection. + + +Open in Cloud Shell + + +To run this sample: + +1. If this is your first time working with GCP products, you will need to set up [the Cloud SDK][cloud_sdk] or utilize [Google Cloud Shell][gcloud_shell]. This sample may [require authetication][authentication] and you will need to [enable billing][enable_billing]. + +1. Make a fork of this repo and clone the branch locally, then navigate to the sample directory you want to use. + +1. Install the dependencies needed to run the samples. + + pip install -r requirements.txt + +1. Run the sample using + + python main.py + + + +
usage: main.py [-h] [--table TABLE] project_id instance_id
usage: main.py [-h] [--table TABLE] project_id instance_id


positional arguments:
  project_id     Your Cloud Platform project ID.
  instance_id    ID of the Cloud Bigtable instance to connect to.


optional arguments:
  -h, --help     show this help message and exit
  --table TABLE  Existing table used in the quickstart. (default: my-table)browse the source and [report issues][issues]. + +### Contributing +View the [contributing guidelines][contrib_guide], the [Python style guide][py_style] for more information. + +[authentication]: https://cloud.google.com/docs/authentication/getting-started +[enable_billing]:https://cloud.google.com/apis/docs/getting-started#enabling_billing +[client_library_python]: https://googlecloudplatform.github.io/google-cloud-python/ +[issues]: https://github.com/GoogleCloudPlatform/google-cloud-python/issues +[contrib_guide]: https://github.com/googleapis/google-cloud-python/blob/main/CONTRIBUTING.rst +[py_style]: http://google.github.io/styleguide/pyguide.html +[cloud_sdk]: https://cloud.google.com/sdk/docs +[gcloud_shell]: https://cloud.google.com/shell/docs +[gcloud_shell]: https://cloud.google.com/shell/docs diff --git a/packages/google-cloud-bigtable/samples/quickstart_happybase/__init__.py b/packages/google-cloud-bigtable/samples/quickstart_happybase/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/samples/quickstart_happybase/main.py b/packages/google-cloud-bigtable/samples/quickstart_happybase/main.py new file mode 100644 index 000000000000..6a05c4cbd46b --- /dev/null +++ b/packages/google-cloud-bigtable/samples/quickstart_happybase/main.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +# Copyright 2018 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. +# [START bigtable_quickstart_happybase] +import argparse + +from google.cloud import bigtable +from google.cloud import happybase + + +def main(project_id="project-id", instance_id="instance-id", table_id="my-table"): + # Creates a Bigtable client + client = bigtable.Client(project=project_id) + + # Connect to an existing instance:my-bigtable-instance + instance = client.instance(instance_id) + + connection = happybase.Connection(instance=instance) + + try: + # Connect to an existing table:my-table + table = connection.table(table_id) + + key = "r1" + row = table.row(key.encode("utf-8")) + + column = "cf1:c1".encode("utf-8") + value = row[column].decode("utf-8") + print("Row key: {}\nData: {}".format(key, value)) + + finally: + connection.close() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("project_id", help="Your Cloud Platform project ID.") + parser.add_argument( + "instance_id", help="ID of the Cloud Bigtable instance to connect to." + ) + parser.add_argument( + "--table", help="Existing table used in the quickstart.", default="my-table" + ) + + args = parser.parse_args() + main(args.project_id, args.instance_id, args.table) +# [END bigtable_quickstart_happybase] diff --git a/packages/google-cloud-bigtable/samples/quickstart_happybase/main_test.py b/packages/google-cloud-bigtable/samples/quickstart_happybase/main_test.py new file mode 100644 index 000000000000..343ec800a96d --- /dev/null +++ b/packages/google-cloud-bigtable/samples/quickstart_happybase/main_test.py @@ -0,0 +1,44 @@ +# Copyright 2018 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 os +import uuid +import pytest + +from .main import main +from ..utils import create_table_cm + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] +TABLE_ID = f"quickstart-hb-test-{str(uuid.uuid4())[:16]}" + + +@pytest.fixture() +def table(): + column_family_id = "cf1" + column_families = {column_family_id: None} + with create_table_cm(PROJECT, BIGTABLE_INSTANCE, TABLE_ID, column_families) as table: + row = table.direct_row("r1") + row.set_cell(column_family_id, "c1", "test-value") + row.commit() + + yield TABLE_ID + + +def test_main(capsys, table): + table_id = table + main(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + assert "Row key: r1\nData: test-value\n" in out diff --git a/packages/google-cloud-bigtable/samples/quickstart_happybase/noxfile.py b/packages/google-cloud-bigtable/samples/quickstart_happybase/noxfile.py new file mode 100644 index 000000000000..a169b5b5b464 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/quickstart_happybase/noxfile.py @@ -0,0 +1,292 @@ +# Copyright 2019 Google LLC +# +# 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 __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, Optional + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in ( + "True", + "true", +) + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8") + else: + session.install("flake8", "flake8-annotations") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# format = isort + black +# + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list.extend(glob.glob("**/tests", recursive=True)) + + if len(test_list) == 0: + print("No tests found, skipping directory.") + return + + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + concurrent_args = [] + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + with open("requirements.txt") as rfile: + packages = rfile.read() + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install( + "-r", "requirements-test.txt", "-c", "constraints-test.txt" + ) + else: + session.install("-r", "requirements-test.txt") + with open("requirements-test.txt") as rtfile: + packages += rtfile.read() + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + if "pytest-parallel" in packages: + concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + elif "pytest-xdist" in packages: + concurrent_args.extend(['-n', 'auto']) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs + concurrent_args), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/packages/google-cloud-bigtable/samples/quickstart_happybase/requirements-test.txt b/packages/google-cloud-bigtable/samples/quickstart_happybase/requirements-test.txt new file mode 100644 index 000000000000..55b033e901cd --- /dev/null +++ b/packages/google-cloud-bigtable/samples/quickstart_happybase/requirements-test.txt @@ -0,0 +1 @@ +pytest \ No newline at end of file diff --git a/packages/google-cloud-bigtable/samples/quickstart_happybase/requirements.txt b/packages/google-cloud-bigtable/samples/quickstart_happybase/requirements.txt new file mode 100644 index 000000000000..dc1a04f30378 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/quickstart_happybase/requirements.txt @@ -0,0 +1,2 @@ +google-cloud-happybase==0.33.0 +six==1.17.0 # See https://github.com/googleapis/google-cloud-python-happybase/issues/128 diff --git a/packages/google-cloud-bigtable/samples/snippets/README.md b/packages/google-cloud-bigtable/samples/snippets/README.md new file mode 100644 index 000000000000..7c0dd4463214 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/README.md @@ -0,0 +1,33 @@ +[//]: # "This README.md file is auto-generated, all changes to this file will be lost." +[//]: # "To regenerate it, use `python -m synthtool`." + +## Python Samples for Cloud Bigtable + +This directory contains samples for Cloud Bigtable, which may be used as a reference for how to use this product. +Samples, quickstarts, and other documentation are available at [cloud.google.com](https://cloud.google.com/bigtable). + + +### Snippets + +This folder contains snippets for Python Cloud Bigtable. + + + + +## Additional Information + +You can read the documentation for more details on API usage and use GitHub +to [browse the source](https://github.com/googleapis/python-bigtable) and [report issues][issues]. + +### Contributing +View the [contributing guidelines][contrib_guide], the [Python style guide][py_style] for more information. + +[authentication]: https://cloud.google.com/docs/authentication/getting-started +[enable_billing]:https://cloud.google.com/apis/docs/getting-started#enabling_billing +[client_library_python]: https://googlecloudplatform.github.io/google-cloud-python/ +[issues]: https://github.com/GoogleCloudPlatform/google-cloud-python/issues +[contrib_guide]: https://github.com/googleapis/google-cloud-python/blob/main/CONTRIBUTING.rst +[py_style]: http://google.github.io/styleguide/pyguide.html +[cloud_sdk]: https://cloud.google.com/sdk/docs +[gcloud_shell]: https://cloud.google.com/shell/docs +[gcloud_shell]: https://cloud.google.com/shell/docs diff --git a/packages/google-cloud-bigtable/samples/snippets/__init__.py b/packages/google-cloud-bigtable/samples/snippets/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/samples/snippets/data_client/__init__.py b/packages/google-cloud-bigtable/samples/snippets/data_client/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async.py b/packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async.py new file mode 100644 index 000000000000..332dbd56fb23 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python + +# Copyright 2024, Google LLC +# 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. + + +async def write_simple(table): + # [START bigtable_async_write_simple] + from google.cloud.bigtable.data import BigtableDataClientAsync + from google.cloud.bigtable.data import SetCell + + async def write_simple(project_id, instance_id, table_id): + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + family_id = "stats_summary" + row_key = b"phone#4c410523#20190501" + + cell_mutation = SetCell(family_id, "connected_cell", 1) + wifi_mutation = SetCell(family_id, "connected_wifi", 1) + os_mutation = SetCell(family_id, "os_build", "PQ2A.190405.003") + + await table.mutate_row(row_key, cell_mutation) + await table.mutate_row(row_key, wifi_mutation) + await table.mutate_row(row_key, os_mutation) + + # [END bigtable_async_write_simple] + await write_simple(table.client.project, table.instance_id, table.table_id) + + +async def write_batch(table): + # [START bigtable_async_writes_batch] + from google.cloud.bigtable.data import BigtableDataClientAsync + from google.cloud.bigtable.data.mutations import SetCell + from google.cloud.bigtable.data.mutations import RowMutationEntry + from google.cloud.bigtable.data.exceptions import MutationsExceptionGroup + + async def write_batch(project_id, instance_id, table_id): + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + family_id = "stats_summary" + try: + async with table.mutations_batcher() as batcher: + mutation_list = [ + SetCell(family_id, "connected_cell", 1), + SetCell(family_id, "connected_wifi", 1), + SetCell(family_id, "os_build", "12155.0.0-rc1"), + ] + # awaiting the batcher.append method adds the RowMutationEntry + # to the batcher's queue to be written in the next flush. + await batcher.append( + RowMutationEntry("tablet#a0b81f74#20190501", mutation_list) + ) + await batcher.append( + RowMutationEntry("tablet#a0b81f74#20190502", mutation_list) + ) + except MutationsExceptionGroup as e: + # MutationsExceptionGroup contains a FailedMutationEntryError for + # each mutation that failed. + for sub_exception in e.exceptions: + failed_entry: RowMutationEntry = sub_exception.entry + cause: Exception = sub_exception.__cause__ + print( + f"Failed mutation: {failed_entry.row_key} with error: {cause!r}" + ) + + # [END bigtable_async_writes_batch] + await write_batch(table.client.project, table.instance_id, table.table_id) + + +async def write_increment(table): + # [START bigtable_async_write_increment] + from google.cloud.bigtable.data import BigtableDataClientAsync + from google.cloud.bigtable.data.read_modify_write_rules import IncrementRule + + async def write_increment(project_id, instance_id, table_id): + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + family_id = "stats_summary" + row_key = "phone#4c410523#20190501" + + # Decrement the connected_wifi value by 1. + increment_rule = IncrementRule( + family_id, "connected_wifi", increment_amount=-1 + ) + result_row = await table.read_modify_write_row(row_key, increment_rule) + + # check result + cell = result_row[0] + print(f"{cell.row_key} value: {int(cell)}") + + # [END bigtable_async_write_increment] + await write_increment(table.client.project, table.instance_id, table.table_id) + + +async def write_conditional(table): + # [START bigtable_async_writes_conditional] + from google.cloud.bigtable.data import BigtableDataClientAsync + from google.cloud.bigtable.data import row_filters + from google.cloud.bigtable.data import SetCell + + async def write_conditional(project_id, instance_id, table_id): + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + family_id = "stats_summary" + row_key = "phone#4c410523#20190501" + + row_filter = row_filters.RowFilterChain( + filters=[ + row_filters.FamilyNameRegexFilter(family_id), + row_filters.ColumnQualifierRegexFilter("os_build"), + row_filters.ValueRegexFilter("PQ2A\\..*"), + ] + ) + + if_true = SetCell(family_id, "os_name", "android") + result = await table.check_and_mutate_row( + row_key, + row_filter, + true_case_mutations=if_true, + false_case_mutations=None, + ) + if result is True: + print("The row os_name was set to android") + + # [END bigtable_async_writes_conditional] + await write_conditional(table.client.project, table.instance_id, table.table_id) + + +async def write_aggregate(table): + # [START bigtable_async_write_aggregate] + import time + from google.cloud.bigtable.data import BigtableDataClientAsync + from google.cloud.bigtable.data.mutations import AddToCell, RowMutationEntry + from google.cloud.bigtable.data.exceptions import MutationsExceptionGroup + + async def write_aggregate(project_id, instance_id, table_id): + """Increments a value in a Bigtable table using AddToCell mutation.""" + async with BigtableDataClientAsync(project=project_id) as client: + table = client.get_table(instance_id, table_id) + row_key = "unique_device_ids_1" + try: + async with table.mutations_batcher() as batcher: + # The AddToCell mutation increments the value of a cell. + # The `counters` family must be set up to be an aggregate + # family with an int64 input type. + reading = AddToCell( + family="counters", + qualifier="odometer", + value=32304, + # Convert nanoseconds to microseconds + timestamp_micros=time.time_ns() // 1000, + ) + await batcher.append( + RowMutationEntry(row_key.encode("utf-8"), [reading]) + ) + except MutationsExceptionGroup as e: + # MutationsExceptionGroup contains a FailedMutationEntryError for + # each mutation that failed. + for sub_exception in e.exceptions: + failed_entry: RowMutationEntry = sub_exception.entry + cause: Exception = sub_exception.__cause__ + print( + f"Failed mutation for row {failed_entry.row_key!r} with error: {cause!r}" + ) + + # [END bigtable_async_write_aggregate] + await write_aggregate(table.client.project, table.instance_id, table.table_id) + + +async def read_row(table): + # [START bigtable_async_reads_row] + from google.cloud.bigtable.data import BigtableDataClientAsync + + async def read_row(project_id, instance_id, table_id): + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + row_key = "phone#4c410523#20190501" + row = await table.read_row(row_key) + print(row) + + # [END bigtable_async_reads_row] + await read_row(table.client.project, table.instance_id, table.table_id) + + +async def read_row_partial(table): + # [START bigtable_async_reads_row_partial] + from google.cloud.bigtable.data import BigtableDataClientAsync + from google.cloud.bigtable.data import row_filters + + async def read_row_partial(project_id, instance_id, table_id): + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + row_key = "phone#4c410523#20190501" + col_filter = row_filters.ColumnQualifierRegexFilter(b"os_build") + + row = await table.read_row(row_key, row_filter=col_filter) + print(row) + + # [END bigtable_async_reads_row_partial] + await read_row_partial(table.client.project, table.instance_id, table.table_id) + + +async def read_rows_multiple(table): + # [START bigtable_async_reads_rows] + from google.cloud.bigtable.data import BigtableDataClientAsync + from google.cloud.bigtable.data import ReadRowsQuery + + async def read_rows(project_id, instance_id, table_id): + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + + query = ReadRowsQuery( + row_keys=[b"phone#4c410523#20190501", b"phone#4c410523#20190502"] + ) + async for row in await table.read_rows_stream(query): + print(row) + + # [END bigtable_async_reads_rows] + await read_rows(table.client.project, table.instance_id, table.table_id) + + +async def read_row_range(table): + # [START bigtable_async_reads_row_range] + from google.cloud.bigtable.data import BigtableDataClientAsync + from google.cloud.bigtable.data import ReadRowsQuery + from google.cloud.bigtable.data import RowRange + + async def read_row_range(project_id, instance_id, table_id): + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + + row_range = RowRange( + start_key=b"phone#4c410523#20190501", + end_key=b"phone#4c410523#201906201", + ) + query = ReadRowsQuery(row_ranges=[row_range]) + + async for row in await table.read_rows_stream(query): + print(row) + + # [END bigtable_async_reads_row_range] + await read_row_range(table.client.project, table.instance_id, table.table_id) + + +async def read_with_prefix(table): + # [START bigtable_async_reads_prefix] + from google.cloud.bigtable.data import BigtableDataClientAsync + from google.cloud.bigtable.data import ReadRowsQuery + from google.cloud.bigtable.data import RowRange + + async def read_prefix(project_id, instance_id, table_id): + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + + prefix = "phone#" + end_key = prefix[:-1] + chr(ord(prefix[-1]) + 1) + prefix_range = RowRange(start_key=prefix, end_key=end_key) + query = ReadRowsQuery(row_ranges=[prefix_range]) + + async for row in await table.read_rows_stream(query): + print(row) + + # [END bigtable_async_reads_prefix] + await read_prefix(table.client.project, table.instance_id, table.table_id) + + +async def read_with_filter(table): + # [START bigtable_async_reads_filter] + from google.cloud.bigtable.data import BigtableDataClientAsync + from google.cloud.bigtable.data import ReadRowsQuery + from google.cloud.bigtable.data import row_filters + + async def read_with_filter(project_id, instance_id, table_id): + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + + row_filter = row_filters.ValueRegexFilter(b"PQ2A.*$") + query = ReadRowsQuery(row_filter=row_filter) + + async for row in await table.read_rows_stream(query): + print(row) + + # [END bigtable_async_reads_filter] + await read_with_filter(table.client.project, table.instance_id, table.table_id) + + +async def execute_query(table): + # [START bigtable_async_execute_query] + from google.cloud.bigtable.data import BigtableDataClientAsync + + async def execute_query(project_id, instance_id, table_id): + async with BigtableDataClientAsync(project=project_id) as client: + query = ( + "SELECT _key, stats_summary['os_build'], " + "stats_summary['connected_cell'], " + "stats_summary['connected_wifi'] " + f"from `{table_id}` WHERE _key=@row_key" + ) + result = await client.execute_query( + query, + instance_id, + parameters={"row_key": b"phone#4c410523#20190501"}, + ) + results = [r async for r in result] + print(results) + + # [END bigtable_async_execute_query] + await execute_query(table.client.project, table.instance_id, table.table_id) diff --git a/packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async_test.py b/packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async_test.py new file mode 100644 index 000000000000..2761bd487114 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async_test.py @@ -0,0 +1,117 @@ +# Copyright 2024, Google LLC +# 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 pytest +import pytest_asyncio +import os +import uuid + +from . import data_client_snippets_async as data_snippets +from ...utils import create_table_cm + + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] +TABLE_ID = f"data-client-{str(uuid.uuid4())[:16]}" + + +@pytest.fixture(scope="session") +def column_family_config(): + from google.cloud.bigtable_admin_v2 import types + + int_aggregate_type = types.Type.Aggregate( + input_type=types.Type(int64_type={"encoding": {"big_endian_bytes": {}}}), + sum={}, + ) + + return { + "family": types.ColumnFamily(), + "stats_summary": types.ColumnFamily(), + "counters": types.ColumnFamily( + value_type=types.Type(aggregate_type=int_aggregate_type) + ), + } + + +@pytest.fixture(scope="session") +def table_id(column_family_config): + with create_table_cm(PROJECT, BIGTABLE_INSTANCE, TABLE_ID, column_family_config): + yield TABLE_ID + + +@pytest_asyncio.fixture +async def table(table_id): + from google.cloud.bigtable.data import BigtableDataClientAsync + + async with BigtableDataClientAsync(project=PROJECT) as client: + async with client.get_table(BIGTABLE_INSTANCE, table_id) as table: + yield table + + +@pytest.mark.asyncio +async def test_write_simple(table): + await data_snippets.write_simple(table) + + +@pytest.mark.asyncio +async def test_write_batch(table): + await data_snippets.write_batch(table) + + +@pytest.mark.asyncio +async def test_write_increment(table): + await data_snippets.write_increment(table) + + +@pytest.mark.asyncio +async def test_write_conditional(table): + await data_snippets.write_conditional(table) + + +@pytest.mark.asyncio +async def test_write_aggregate(table): + await data_snippets.write_aggregate(table) + + +@pytest.mark.asyncio +async def test_read_row(table): + await data_snippets.read_row(table) + + +@pytest.mark.asyncio +async def test_read_row_partial(table): + await data_snippets.read_row_partial(table) + + +@pytest.mark.asyncio +async def test_read_rows_multiple(table): + await data_snippets.read_rows_multiple(table) + + +@pytest.mark.asyncio +async def test_read_row_range(table): + await data_snippets.read_row_range(table) + + +@pytest.mark.asyncio +async def test_read_with_prefix(table): + await data_snippets.read_with_prefix(table) + + +@pytest.mark.asyncio +async def test_read_with_filter(table): + await data_snippets.read_with_filter(table) + + +@pytest.mark.asyncio +async def test_execute_query(table): + await data_snippets.execute_query(table) diff --git a/packages/google-cloud-bigtable/samples/snippets/data_client/noxfile.py b/packages/google-cloud-bigtable/samples/snippets/data_client/noxfile.py new file mode 100644 index 000000000000..a169b5b5b464 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/data_client/noxfile.py @@ -0,0 +1,292 @@ +# Copyright 2019 Google LLC +# +# 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 __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, Optional + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in ( + "True", + "true", +) + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8") + else: + session.install("flake8", "flake8-annotations") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# format = isort + black +# + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list.extend(glob.glob("**/tests", recursive=True)) + + if len(test_list) == 0: + print("No tests found, skipping directory.") + return + + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + concurrent_args = [] + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + with open("requirements.txt") as rfile: + packages = rfile.read() + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install( + "-r", "requirements-test.txt", "-c", "constraints-test.txt" + ) + else: + session.install("-r", "requirements-test.txt") + with open("requirements-test.txt") as rtfile: + packages += rtfile.read() + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + if "pytest-parallel" in packages: + concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + elif "pytest-xdist" in packages: + concurrent_args.extend(['-n', 'auto']) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs + concurrent_args), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/packages/google-cloud-bigtable/samples/snippets/data_client/requirements-test.txt b/packages/google-cloud-bigtable/samples/snippets/data_client/requirements-test.txt new file mode 100644 index 000000000000..ee4ba018603b --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/data_client/requirements-test.txt @@ -0,0 +1,2 @@ +pytest +pytest-asyncio diff --git a/packages/google-cloud-bigtable/samples/snippets/data_client/requirements.txt b/packages/google-cloud-bigtable/samples/snippets/data_client/requirements.txt new file mode 100644 index 000000000000..730d25dec63f --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/data_client/requirements.txt @@ -0,0 +1 @@ +google-cloud-bigtable==2.35.0 diff --git a/packages/google-cloud-bigtable/samples/snippets/deletes/__init__.py b/packages/google-cloud-bigtable/samples/snippets/deletes/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_async_test.py b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_async_test.py new file mode 100644 index 000000000000..4fb4898e5270 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_async_test.py @@ -0,0 +1,274 @@ +# Copyright 2024, Google LLC + +# 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 datetime +import os +import uuid +from typing import AsyncGenerator + +from google.cloud._helpers import _microseconds_from_datetime +import pytest +import pytest_asyncio + +from . import deletes_snippets_async +from ...utils import create_table_cm + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] +TABLE_ID = f"mobile-time-series-deletes-async-{str(uuid.uuid4())[:16]}" + + +@pytest.fixture(scope="module") +def event_loop(): + import asyncio + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + + +@pytest_asyncio.fixture(scope="module", autouse=True) +async def table_id() -> AsyncGenerator[str, None]: + with create_table_cm(PROJECT, BIGTABLE_INSTANCE, TABLE_ID, {"stats_summary": None, "cell_plan": None}, verbose=False): + await _populate_table(TABLE_ID) + yield TABLE_ID + + +async def _populate_table(table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + RowMutationEntry, + SetCell, + ) + + timestamp = datetime.datetime(2019, 5, 1) + timestamp_minus_hr = timestamp - datetime.timedelta(hours=1) + + async with BigtableDataClientAsync(project=PROJECT) as client: + async with client.get_table(BIGTABLE_INSTANCE, table_id) as table: + async with table.mutations_batcher() as batcher: + await batcher.append( + RowMutationEntry( + "phone#4c410523#20190501", + [ + SetCell( + "stats_summary", + "connected_cell", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "connected_cell", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "connected_wifi", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "os_build", + "PQ2A.190405.003", + _microseconds_from_datetime(timestamp), + ), + SetCell( + "cell_plan", + "data_plan_01gb", + "true", + _microseconds_from_datetime(timestamp_minus_hr), + ), + SetCell( + "cell_plan", + "data_plan_01gb", + "false", + _microseconds_from_datetime(timestamp), + ), + SetCell( + "cell_plan", + "data_plan_05gb", + "true", + _microseconds_from_datetime(timestamp), + ), + ], + ) + ) + await batcher.append( + RowMutationEntry( + "phone#4c410523#20190502", + [ + SetCell( + "stats_summary", + "connected_cell", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "connected_wifi", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "os_build", + "PQ2A.190405.004", + _microseconds_from_datetime(timestamp), + ), + SetCell( + "cell_plan", + "data_plan_05gb", + "true", + _microseconds_from_datetime(timestamp), + ), + ], + ) + ) + await batcher.append( + RowMutationEntry( + "phone#4c410523#20190505", + [ + SetCell( + "stats_summary", + "connected_cell", + 0, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "connected_wifi", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "os_build", + "PQ2A.190406.000", + _microseconds_from_datetime(timestamp), + ), + SetCell( + "cell_plan", + "data_plan_05gb", + "true", + _microseconds_from_datetime(timestamp), + ), + ], + ) + ) + await batcher.append( + RowMutationEntry( + "phone#5c10102#20190501", + [ + SetCell( + "stats_summary", + "connected_cell", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "connected_wifi", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "os_build", + "PQ2A.190401.002", + _microseconds_from_datetime(timestamp), + ), + SetCell( + "cell_plan", + "data_plan_10gb", + "true", + _microseconds_from_datetime(timestamp), + ), + ], + ) + ) + await batcher.append( + RowMutationEntry( + "phone#5c10102#20190502", + [ + SetCell( + "stats_summary", + "connected_cell", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "connected_wifi", + 0, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "os_build", + "PQ2A.190406.000", + _microseconds_from_datetime(timestamp), + ), + SetCell( + "cell_plan", + "data_plan_10gb", + "true", + _microseconds_from_datetime(timestamp), + ), + ], + ) + ) + + +def assert_output_match(capsys, expected): + out, _ = capsys.readouterr() + assert out == expected + + +@pytest.mark.asyncio +async def test_delete_from_column(capsys, table_id): + await deletes_snippets_async.delete_from_column( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + assert_output_match(capsys, "") + + +@pytest.mark.asyncio +async def test_delete_from_column_family(capsys, table_id): + await deletes_snippets_async.delete_from_column_family( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + assert_output_match(capsys, "") + + +@pytest.mark.asyncio +async def test_delete_from_row(capsys, table_id): + await deletes_snippets_async.delete_from_row(PROJECT, BIGTABLE_INSTANCE, table_id) + assert_output_match(capsys, "") + + +@pytest.mark.asyncio +async def test_streaming_and_batching(capsys, table_id): + await deletes_snippets_async.streaming_and_batching( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + assert_output_match(capsys, "") + + +@pytest.mark.asyncio +async def test_check_and_mutate(capsys, table_id): + await deletes_snippets_async.check_and_mutate(PROJECT, BIGTABLE_INSTANCE, table_id) + assert_output_match(capsys, "") diff --git a/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets.py b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets.py new file mode 100644 index 000000000000..6cdbf33a69db --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python + +# Copyright 2022, Google LLC +# 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 bigtable_delete_from_column] +def delete_from_column(project_id, instance_id, table_id): + from google.cloud.bigtable import Client + + client = Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + row = table.row("phone#4c410523#20190501") + row.delete_cell(column_family_id="cell_plan", column="data_plan_01gb") + row.commit() + + +# [END bigtable_delete_from_column] + +# [START bigtable_delete_from_column_family] +def delete_from_column_family(project_id, instance_id, table_id): + from google.cloud.bigtable import Client + + client = Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + row = table.row("phone#4c410523#20190501") + row.delete_cells(column_family_id="cell_plan", columns=row.ALL_COLUMNS) + row.commit() + + +# [END bigtable_delete_from_column_family] + + +# [START bigtable_delete_from_row] +def delete_from_row(project_id, instance_id, table_id): + from google.cloud.bigtable import Client + + client = Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + row = table.row("phone#4c410523#20190501") + row.delete() + row.commit() + + +# [END bigtable_delete_from_row] + +# [START bigtable_streaming_and_batching] +def streaming_and_batching(project_id, instance_id, table_id): + from google.cloud.bigtable import Client + + client = Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + batcher = table.mutations_batcher(flush_count=2) + rows = table.read_rows() + for row in rows: + row = table.row(row.row_key) + row.delete_cell(column_family_id="cell_plan", column="data_plan_01gb") + + batcher.mutate_rows(rows) + + +# [END bigtable_streaming_and_batching] + +# [START bigtable_check_and_mutate] +def check_and_mutate(project_id, instance_id, table_id): + from google.cloud.bigtable import Client + + client = Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + row = table.row("phone#4c410523#20190501") + row.delete_cell(column_family_id="cell_plan", column="data_plan_01gb") + row.delete_cell(column_family_id="cell_plan", column="data_plan_05gb") + row.commit() + + +# [END bigtable_check_and_mutate] + + +# [START bigtable_drop_row_range] +def drop_row_range(project_id, instance_id, table_id): + from google.cloud.bigtable import Client + + client = Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + row_key_prefix = "phone#4c410523" + table.drop_by_prefix(row_key_prefix, timeout=200) + + +# [END bigtable_drop_row_range] + +# [START bigtable_delete_column_family] +def delete_column_family(project_id, instance_id, table_id): + from google.cloud.bigtable import Client + + client = Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + column_family_id = "stats_summary" + column_family_obj = table.column_family(column_family_id) + column_family_obj.delete() + + +# [END bigtable_delete_column_family] + +# [START bigtable_delete_table] +def delete_table(project_id, instance_id, table_id): + from google.cloud.bigtable import Client + + client = Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + table.delete() + + +# [END bigtable_delete_table] diff --git a/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets_async.py b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets_async.py new file mode 100644 index 000000000000..2241fab4a71b --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets_async.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +# Copyright 2024, Google LLC +# 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 bigtable_delete_from_column_asyncio] +async def delete_from_column(project_id, instance_id, table_id): + from google.cloud.bigtable.data import BigtableDataClientAsync + from google.cloud.bigtable.data import DeleteRangeFromColumn + + client = BigtableDataClientAsync(project=project_id) + table = client.get_table(instance_id, table_id) + + await table.mutate_row( + "phone#4c410523#20190501", + DeleteRangeFromColumn(family="cell_plan", qualifier=b"data_plan_01gb"), + ) + + await table.close() + await client.close() + + +# [END bigtable_delete_from_column_asyncio] + +# [START bigtable_delete_from_column_family_asyncio] +async def delete_from_column_family(project_id, instance_id, table_id): + from google.cloud.bigtable.data import BigtableDataClientAsync + from google.cloud.bigtable.data import DeleteAllFromFamily + + client = BigtableDataClientAsync(project=project_id) + table = client.get_table(instance_id, table_id) + + await table.mutate_row("phone#4c410523#20190501", DeleteAllFromFamily("cell_plan")) + + await table.close() + await client.close() + + +# [END bigtable_delete_from_column_family_asyncio] + + +# [START bigtable_delete_from_row_asyncio] +async def delete_from_row(project_id, instance_id, table_id): + from google.cloud.bigtable.data import BigtableDataClientAsync + from google.cloud.bigtable.data import DeleteAllFromRow + + client = BigtableDataClientAsync(project=project_id) + table = client.get_table(instance_id, table_id) + + await table.mutate_row("phone#4c410523#20190501", DeleteAllFromRow()) + + await table.close() + await client.close() + + +# [END bigtable_delete_from_row_asyncio] + +# [START bigtable_streaming_and_batching_asyncio] +async def streaming_and_batching(project_id, instance_id, table_id): + from google.cloud.bigtable.data import BigtableDataClientAsync + from google.cloud.bigtable.data import DeleteRangeFromColumn + from google.cloud.bigtable.data import RowMutationEntry + from google.cloud.bigtable.data import ReadRowsQuery + + client = BigtableDataClientAsync(project=project_id) + table = client.get_table(instance_id, table_id) + + async with table.mutations_batcher() as batcher: + async for row in await table.read_rows_stream(ReadRowsQuery(limit=10)): + await batcher.append( + RowMutationEntry( + row.row_key, + DeleteRangeFromColumn( + family="cell_plan", qualifier=b"data_plan_01gb" + ), + ) + ) + + await table.close() + await client.close() + + +# [END bigtable_streaming_and_batching_asyncio] + +# [START bigtable_check_and_mutate_asyncio] +async def check_and_mutate(project_id, instance_id, table_id): + from google.cloud.bigtable.data import BigtableDataClientAsync + from google.cloud.bigtable.data import DeleteRangeFromColumn + from google.cloud.bigtable.data.row_filters import LiteralValueFilter + + client = BigtableDataClientAsync(project=project_id) + table = client.get_table(instance_id, table_id) + + await table.check_and_mutate_row( + "phone#4c410523#20190501", + predicate=LiteralValueFilter("PQ2A.190405.003"), + true_case_mutations=DeleteRangeFromColumn( + family="cell_plan", qualifier=b"data_plan_01gb" + ), + ) + + await table.close() + await client.close() + + +# [END bigtable_check_and_mutate_asyncio] diff --git a/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_test.py b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_test.py new file mode 100644 index 000000000000..3284c37da739 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_test.py @@ -0,0 +1,133 @@ +# Copyright 2020, Google LLC + +# 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 datetime +import os +import time +import uuid + +import pytest + +from . import deletes_snippets +from ...utils import create_table_cm + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] +TABLE_ID = f"mobile-time-series-deletes-{str(uuid.uuid4())[:16]}" + + +@pytest.fixture(scope="module") +def table_id(): + from google.cloud.bigtable.row_set import RowSet + + with create_table_cm(PROJECT, BIGTABLE_INSTANCE, TABLE_ID, {"stats_summary": None, "cell_plan": None}, verbose=False) as table: + timestamp = datetime.datetime(2019, 5, 1) + timestamp_minus_hr = datetime.datetime(2019, 5, 1) - datetime.timedelta(hours=1) + + row_keys = [ + "phone#4c410523#20190501", + "phone#4c410523#20190502", + "phone#4c410523#20190505", + "phone#5c10102#20190501", + "phone#5c10102#20190502", + ] + + rows = [table.direct_row(row_key) for row_key in row_keys] + + rows[0].set_cell("stats_summary", "connected_cell", 1, timestamp) + rows[0].set_cell("stats_summary", "connected_wifi", 1, timestamp) + rows[0].set_cell("stats_summary", "os_build", "PQ2A.190405.003", timestamp) + rows[0].set_cell("cell_plan", "data_plan_01gb", "true", timestamp_minus_hr) + rows[0].set_cell("cell_plan", "data_plan_01gb", "false", timestamp) + rows[0].set_cell("cell_plan", "data_plan_05gb", "true", timestamp) + rows[1].set_cell("stats_summary", "connected_cell", 1, timestamp) + rows[1].set_cell("stats_summary", "connected_wifi", 1, timestamp) + rows[1].set_cell("stats_summary", "os_build", "PQ2A.190405.004", timestamp) + rows[1].set_cell("cell_plan", "data_plan_05gb", "true", timestamp) + rows[2].set_cell("stats_summary", "connected_cell", 0, timestamp) + rows[2].set_cell("stats_summary", "connected_wifi", 1, timestamp) + rows[2].set_cell("stats_summary", "os_build", "PQ2A.190406.000", timestamp) + rows[2].set_cell("cell_plan", "data_plan_05gb", "true", timestamp) + rows[3].set_cell("stats_summary", "connected_cell", 1, timestamp) + rows[3].set_cell("stats_summary", "connected_wifi", 1, timestamp) + rows[3].set_cell("stats_summary", "os_build", "PQ2A.190401.002", timestamp) + rows[3].set_cell("cell_plan", "data_plan_10gb", "true", timestamp) + rows[4].set_cell("stats_summary", "connected_cell", 1, timestamp) + rows[4].set_cell("stats_summary", "connected_wifi", 0, timestamp) + rows[4].set_cell("stats_summary", "os_build", "PQ2A.190406.000", timestamp) + rows[4].set_cell("cell_plan", "data_plan_10gb", "true", timestamp) + + table.mutate_rows(rows) + + # Ensure mutations have propagated. + row_set = RowSet() + + for row_key in row_keys: + row_set.add_row_key(row_key) + + fetched = list(table.read_rows(row_set=row_set)) + + while len(fetched) < len(rows): + time.sleep(5) + fetched = list(table.read_rows(row_set=row_set)) + + yield TABLE_ID + + +def assert_output_match(capsys, expected): + out, _ = capsys.readouterr() + assert out == expected + + +def test_delete_from_column(capsys, table_id): + deletes_snippets.delete_from_column(PROJECT, BIGTABLE_INSTANCE, table_id) + assert_output_match(capsys, "") + + +def test_delete_from_column_family(capsys, table_id): + deletes_snippets.delete_from_column_family(PROJECT, BIGTABLE_INSTANCE, table_id) + assert_output_match(capsys, "") + + +def test_delete_from_row(capsys, table_id): + deletes_snippets.delete_from_row(PROJECT, BIGTABLE_INSTANCE, table_id) + assert_output_match(capsys, "") + + +def test_streaming_and_batching(capsys, table_id): + deletes_snippets.streaming_and_batching(PROJECT, BIGTABLE_INSTANCE, table_id) + assert_output_match(capsys, "") + + +def test_check_and_mutate(capsys, table_id): + deletes_snippets.check_and_mutate(PROJECT, BIGTABLE_INSTANCE, table_id) + assert_output_match(capsys, "") + + +def test_drop_row_range(capsys, table_id): + deletes_snippets.drop_row_range(PROJECT, BIGTABLE_INSTANCE, table_id) + assert_output_match(capsys, "") + + +def test_delete_column_family(capsys, table_id): + deletes_snippets.delete_column_family(PROJECT, BIGTABLE_INSTANCE, table_id) + assert_output_match(capsys, "") + + +def test_delete_table(capsys): + delete_table_id = f"to-delete-table-{str(uuid.uuid4())[:16]}" + with create_table_cm(PROJECT, BIGTABLE_INSTANCE, delete_table_id, verbose=False): + deletes_snippets.delete_table(PROJECT, BIGTABLE_INSTANCE, delete_table_id) + assert_output_match(capsys, "") diff --git a/packages/google-cloud-bigtable/samples/snippets/deletes/noxfile.py b/packages/google-cloud-bigtable/samples/snippets/deletes/noxfile.py new file mode 100644 index 000000000000..a169b5b5b464 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/deletes/noxfile.py @@ -0,0 +1,292 @@ +# Copyright 2019 Google LLC +# +# 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 __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, Optional + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in ( + "True", + "true", +) + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8") + else: + session.install("flake8", "flake8-annotations") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# format = isort + black +# + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list.extend(glob.glob("**/tests", recursive=True)) + + if len(test_list) == 0: + print("No tests found, skipping directory.") + return + + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + concurrent_args = [] + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + with open("requirements.txt") as rfile: + packages = rfile.read() + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install( + "-r", "requirements-test.txt", "-c", "constraints-test.txt" + ) + else: + session.install("-r", "requirements-test.txt") + with open("requirements-test.txt") as rtfile: + packages += rtfile.read() + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + if "pytest-parallel" in packages: + concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + elif "pytest-xdist" in packages: + concurrent_args.extend(['-n', 'auto']) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs + concurrent_args), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/packages/google-cloud-bigtable/samples/snippets/deletes/requirements-test.txt b/packages/google-cloud-bigtable/samples/snippets/deletes/requirements-test.txt new file mode 100644 index 000000000000..ee4ba018603b --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/deletes/requirements-test.txt @@ -0,0 +1,2 @@ +pytest +pytest-asyncio diff --git a/packages/google-cloud-bigtable/samples/snippets/deletes/requirements.txt b/packages/google-cloud-bigtable/samples/snippets/deletes/requirements.txt new file mode 100644 index 000000000000..730d25dec63f --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/deletes/requirements.txt @@ -0,0 +1 @@ +google-cloud-bigtable==2.35.0 diff --git a/packages/google-cloud-bigtable/samples/snippets/filters/__init__.py b/packages/google-cloud-bigtable/samples/snippets/filters/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets.py b/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets.py new file mode 100644 index 000000000000..d17c773a4730 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python + +# Copyright 2020, Google LLC +# 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 bigtable_filters_limit_row_sample] +def filter_limit_row_sample(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows(filter_=row_filters.RowSampleFilter(0.75)) + for row in rows: + print_row(row) + + +# [END bigtable_filters_limit_row_sample] +# [START bigtable_filters_limit_row_regex] +def filter_limit_row_regex(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows( + filter_=row_filters.RowKeyRegexFilter(".*#20190501$".encode("utf-8")) + ) + for row in rows: + print_row(row) + + +# [END bigtable_filters_limit_row_regex] +# [START bigtable_filters_limit_cells_per_col] +def filter_limit_cells_per_col(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows(filter_=row_filters.CellsColumnLimitFilter(2)) + for row in rows: + print_row(row) + + +# [END bigtable_filters_limit_cells_per_col] +# [START bigtable_filters_limit_cells_per_row] +def filter_limit_cells_per_row(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows(filter_=row_filters.CellsRowLimitFilter(2)) + for row in rows: + print_row(row) + + +# [END bigtable_filters_limit_cells_per_row] +# [START bigtable_filters_limit_cells_per_row_offset] +def filter_limit_cells_per_row_offset(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows(filter_=row_filters.CellsRowOffsetFilter(2)) + for row in rows: + print_row(row) + + +# [END bigtable_filters_limit_cells_per_row_offset] +# [START bigtable_filters_limit_col_family_regex] +def filter_limit_col_family_regex(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows( + filter_=row_filters.FamilyNameRegexFilter("stats_.*$".encode("utf-8")) + ) + for row in rows: + print_row(row) + + +# [END bigtable_filters_limit_col_family_regex] +# [START bigtable_filters_limit_col_qualifier_regex] +def filter_limit_col_qualifier_regex(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows( + filter_=row_filters.ColumnQualifierRegexFilter("connected_.*$".encode("utf-8")) + ) + for row in rows: + print_row(row) + + +# [END bigtable_filters_limit_col_qualifier_regex] +# [START bigtable_filters_limit_col_range] +def filter_limit_col_range(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows( + filter_=row_filters.ColumnRangeFilter( + "cell_plan", b"data_plan_01gb", b"data_plan_10gb", inclusive_end=False + ) + ) + for row in rows: + print_row(row) + + +# [END bigtable_filters_limit_col_range] +# [START bigtable_filters_limit_value_range] +def filter_limit_value_range(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows( + filter_=row_filters.ValueRangeFilter(b"PQ2A.190405", b"PQ2A.190406") + ) + + for row in rows: + print_row(row) + + +# [END bigtable_filters_limit_value_range] +# [START bigtable_filters_limit_value_regex] + + +def filter_limit_value_regex(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows( + filter_=row_filters.ValueRegexFilter("PQ2A.*$".encode("utf-8")) + ) + for row in rows: + print_row(row) + + +# [END bigtable_filters_limit_value_regex] +# [START bigtable_filters_limit_timestamp_range] +def filter_limit_timestamp_range(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + import datetime + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + end = datetime.datetime(2019, 5, 1) + + rows = table.read_rows( + filter_=row_filters.TimestampRangeFilter(row_filters.TimestampRange(end=end)) + ) + for row in rows: + print_row(row) + + +# [END bigtable_filters_limit_timestamp_range] +# [START bigtable_filters_limit_block_all] +def filter_limit_block_all(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows(filter_=row_filters.BlockAllFilter(True)) + for row in rows: + print_row(row) + + +# [END bigtable_filters_limit_block_all] +# [START bigtable_filters_limit_pass_all] +def filter_limit_pass_all(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows(filter_=row_filters.PassAllFilter(True)) + for row in rows: + print_row(row) + + +# [END bigtable_filters_limit_pass_all] +# [START bigtable_filters_modify_strip_value] +def filter_modify_strip_value(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows(filter_=row_filters.StripValueTransformerFilter(True)) + for row in rows: + print_row(row) + + +# [END bigtable_filters_modify_strip_value] +# [START bigtable_filters_modify_apply_label] +def filter_modify_apply_label(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows(filter_=row_filters.ApplyLabelFilter(label="labelled")) + for row in rows: + print_row(row) + + +# [END bigtable_filters_modify_apply_label] +# [START bigtable_filters_composing_chain] +def filter_composing_chain(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows( + filter_=row_filters.RowFilterChain( + filters=[ + row_filters.CellsColumnLimitFilter(1), + row_filters.FamilyNameRegexFilter("cell_plan"), + ] + ) + ) + for row in rows: + print_row(row) + + +# [END bigtable_filters_composing_chain] +# [START bigtable_filters_composing_interleave] +def filter_composing_interleave(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows( + filter_=row_filters.RowFilterUnion( + filters=[ + row_filters.ValueRegexFilter("true"), + row_filters.ColumnQualifierRegexFilter("os_build"), + ] + ) + ) + for row in rows: + print_row(row) + + +# [END bigtable_filters_composing_interleave] +# [START bigtable_filters_composing_condition] +def filter_composing_condition(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows( + filter_=row_filters.ConditionalRowFilter( + base_filter=row_filters.RowFilterChain( + filters=[ + row_filters.ColumnQualifierRegexFilter("data_plan_10gb"), + row_filters.ValueRegexFilter("true"), + ] + ), + true_filter=row_filters.ApplyLabelFilter(label="passed-filter"), + false_filter=row_filters.ApplyLabelFilter(label="filtered-out"), + ) + ) + for row in rows: + print_row(row) + + +# [END bigtable_filters_composing_condition] + +# [START bigtable_filters_print] +def print_row(row): + print("Reading data for {}:".format(row.row_key.decode("utf-8"))) + for cf, cols in sorted(row.cells.items()): + print("Column Family {}".format(cf)) + for col, cells in sorted(cols.items()): + for cell in cells: + labels = ( + " [{}]".format(",".join(cell.labels)) if len(cell.labels) else "" + ) + print( + "\t{}: {} @{}{}".format( + col.decode("utf-8"), + cell.value.decode("utf-8"), + cell.timestamp, + labels, + ) + ) + print("") + + +# [END bigtable_filters_print] diff --git a/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets_async.py b/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets_async.py new file mode 100644 index 000000000000..899d4c5c78e9 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets_async.py @@ -0,0 +1,389 @@ +# Copyright 2024, Google LLC +# 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 bigtable_filters_limit_row_sample_asyncio] +async def filter_limit_row_sample(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery(row_filter=row_filters.RowSampleFilter(0.75)) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_limit_row_sample_asyncio] +# [START bigtable_filters_limit_row_regex_asyncio] +async def filter_limit_row_regex(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery( + row_filter=row_filters.RowKeyRegexFilter(".*#20190501$".encode("utf-8")) + ) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_limit_row_regex_asyncio] +# [START bigtable_filters_limit_cells_per_col_asyncio] +async def filter_limit_cells_per_col(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery(row_filter=row_filters.CellsColumnLimitFilter(2)) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_limit_cells_per_col_asyncio] +# [START bigtable_filters_limit_cells_per_row_asyncio] +async def filter_limit_cells_per_row(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery(row_filter=row_filters.CellsRowLimitFilter(2)) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_limit_cells_per_row_asyncio] +# [START bigtable_filters_limit_cells_per_row_offset_asyncio] +async def filter_limit_cells_per_row_offset(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery(row_filter=row_filters.CellsRowOffsetFilter(2)) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_limit_cells_per_row_offset_asyncio] +# [START bigtable_filters_limit_col_family_regex_asyncio] +async def filter_limit_col_family_regex(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery( + row_filter=row_filters.FamilyNameRegexFilter("stats_.*$".encode("utf-8")) + ) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_limit_col_family_regex_asyncio] +# [START bigtable_filters_limit_col_qualifier_regex_asyncio] +async def filter_limit_col_qualifier_regex(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery( + row_filter=row_filters.ColumnQualifierRegexFilter( + "connected_.*$".encode("utf-8") + ) + ) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_limit_col_qualifier_regex_asyncio] +# [START bigtable_filters_limit_col_range_asyncio] +async def filter_limit_col_range(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery( + row_filter=row_filters.ColumnRangeFilter( + "cell_plan", b"data_plan_01gb", b"data_plan_10gb", inclusive_end=False + ) + ) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_limit_col_range_asyncio] +# [START bigtable_filters_limit_value_range_asyncio] +async def filter_limit_value_range(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery( + row_filter=row_filters.ValueRangeFilter(b"PQ2A.190405", b"PQ2A.190406") + ) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_limit_value_range_asyncio] +# [START bigtable_filters_limit_value_regex_asyncio] + + +async def filter_limit_value_regex(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery( + row_filter=row_filters.ValueRegexFilter("PQ2A.*$".encode("utf-8")) + ) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_limit_value_regex_asyncio] +# [START bigtable_filters_limit_timestamp_range_asyncio] +async def filter_limit_timestamp_range(project_id, instance_id, table_id): + import datetime + + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + end = datetime.datetime(2019, 5, 1) + + query = ReadRowsQuery(row_filter=row_filters.TimestampRangeFilter(end=end)) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_limit_timestamp_range_asyncio] +# [START bigtable_filters_limit_block_all_asyncio] +async def filter_limit_block_all(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery(row_filter=row_filters.BlockAllFilter(True)) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_limit_block_all_asyncio] +# [START bigtable_filters_limit_pass_all_asyncio] +async def filter_limit_pass_all(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery(row_filter=row_filters.PassAllFilter(True)) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_limit_pass_all_asyncio] +# [START bigtable_filters_modify_strip_value_asyncio] +async def filter_modify_strip_value(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery(row_filter=row_filters.StripValueTransformerFilter(True)) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_modify_strip_value_asyncio] +# [START bigtable_filters_modify_apply_label_asyncio] +async def filter_modify_apply_label(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery(row_filter=row_filters.ApplyLabelFilter(label="labelled")) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_modify_apply_label_asyncio] +# [START bigtable_filters_composing_chain_asyncio] +async def filter_composing_chain(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery( + row_filter=row_filters.RowFilterChain( + filters=[ + row_filters.CellsColumnLimitFilter(1), + row_filters.FamilyNameRegexFilter("cell_plan"), + ] + ) + ) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_composing_chain_asyncio] +# [START bigtable_filters_composing_interleave_asyncio] +async def filter_composing_interleave(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery( + row_filter=row_filters.RowFilterUnion( + filters=[ + row_filters.ValueRegexFilter("true"), + row_filters.ColumnQualifierRegexFilter("os_build"), + ] + ) + ) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_composing_interleave_asyncio] +# [START bigtable_filters_composing_condition_asyncio] +async def filter_composing_condition(project_id, instance_id, table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) + + query = ReadRowsQuery( + row_filter=row_filters.ConditionalRowFilter( + predicate_filter=row_filters.RowFilterChain( + filters=[ + row_filters.ColumnQualifierRegexFilter("data_plan_10gb"), + row_filters.ValueRegexFilter("true"), + ] + ), + true_filter=row_filters.ApplyLabelFilter(label="passed-filter"), + false_filter=row_filters.ApplyLabelFilter(label="filtered-out"), + ) + ) + + async with BigtableDataClientAsync(project=project_id) as client: + async with client.get_table(instance_id, table_id) as table: + for row in await table.read_rows(query): + print_row(row) + + +# [END bigtable_filters_composing_condition_asyncio] + + +def print_row(row): + from google.cloud._helpers import _datetime_from_microseconds + + print("Reading data for {}:".format(row.row_key.decode("utf-8"))) + last_family = None + for cell in row.cells: + if last_family != cell.family: + print("Column Family {}".format(cell.family)) + last_family = cell.family + + labels = " [{}]".format(",".join(cell.labels)) if len(cell.labels) else "" + print( + "\t{}: {} @{}{}".format( + cell.qualifier.decode("utf-8"), + cell.value.decode("utf-8"), + _datetime_from_microseconds(cell.timestamp_micros), + labels, + ) + ) + print("") diff --git a/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets_async_test.py b/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets_async_test.py new file mode 100644 index 000000000000..a3f83a6f2404 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets_async_test.py @@ -0,0 +1,447 @@ +# Copyright 2020, Google LLC +# 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 datetime +import os +import uuid + +import inspect +from typing import AsyncGenerator + +import pytest +import pytest_asyncio +from .snapshots.snap_filters_test import snapshots + +from . import filter_snippets_async +from ...utils import create_table_cm +from google.cloud._helpers import ( + _microseconds_from_datetime, +) + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] +TABLE_ID = f"mobile-time-series-filters-async-{str(uuid.uuid4())[:16]}" + + +@pytest.fixture(scope="module") +def event_loop(): + import asyncio + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + + +@pytest_asyncio.fixture(scope="module", autouse=True) +async def table_id() -> AsyncGenerator[str, None]: + with create_table_cm(PROJECT, BIGTABLE_INSTANCE, TABLE_ID, {"stats_summary": None, "cell_plan": None}): + await _populate_table(TABLE_ID) + yield TABLE_ID + + +async def _populate_table(table_id): + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + RowMutationEntry, + SetCell, + ) + + timestamp = datetime.datetime(2019, 5, 1) + timestamp_minus_hr = timestamp - datetime.timedelta(hours=1) + + async with BigtableDataClientAsync(project=PROJECT) as client: + async with client.get_table(BIGTABLE_INSTANCE, table_id) as table: + async with table.mutations_batcher() as batcher: + await batcher.append( + RowMutationEntry( + "phone#4c410523#20190501", + [ + SetCell( + "stats_summary", + "connected_cell", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "connected_cell", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "connected_wifi", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "os_build", + "PQ2A.190405.003", + _microseconds_from_datetime(timestamp), + ), + SetCell( + "cell_plan", + "data_plan_01gb", + "true", + _microseconds_from_datetime(timestamp_minus_hr), + ), + SetCell( + "cell_plan", + "data_plan_01gb", + "false", + _microseconds_from_datetime(timestamp), + ), + SetCell( + "cell_plan", + "data_plan_05gb", + "true", + _microseconds_from_datetime(timestamp), + ), + ], + ) + ) + await batcher.append( + RowMutationEntry( + "phone#4c410523#20190502", + [ + SetCell( + "stats_summary", + "connected_cell", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "connected_wifi", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "os_build", + "PQ2A.190405.004", + _microseconds_from_datetime(timestamp), + ), + SetCell( + "cell_plan", + "data_plan_05gb", + "true", + _microseconds_from_datetime(timestamp), + ), + ], + ) + ) + await batcher.append( + RowMutationEntry( + "phone#4c410523#20190505", + [ + SetCell( + "stats_summary", + "connected_cell", + 0, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "connected_wifi", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "os_build", + "PQ2A.190406.000", + _microseconds_from_datetime(timestamp), + ), + SetCell( + "cell_plan", + "data_plan_05gb", + "true", + _microseconds_from_datetime(timestamp), + ), + ], + ) + ) + await batcher.append( + RowMutationEntry( + "phone#5c10102#20190501", + [ + SetCell( + "stats_summary", + "connected_cell", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "connected_wifi", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "os_build", + "PQ2A.190401.002", + _microseconds_from_datetime(timestamp), + ), + SetCell( + "cell_plan", + "data_plan_10gb", + "true", + _microseconds_from_datetime(timestamp), + ), + ], + ) + ) + await batcher.append( + RowMutationEntry( + "phone#5c10102#20190502", + [ + SetCell( + "stats_summary", + "connected_cell", + 1, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "connected_wifi", + 0, + _microseconds_from_datetime(timestamp), + ), + SetCell( + "stats_summary", + "os_build", + "PQ2A.190406.000", + _microseconds_from_datetime(timestamp), + ), + SetCell( + "cell_plan", + "data_plan_10gb", + "true", + _microseconds_from_datetime(timestamp), + ), + ], + ) + ) + + +def _datetime_to_micros(value: datetime.datetime) -> int: + """Uses the same conversion rules as the old client in""" + import calendar + import datetime as dt + if not value.tzinfo: + value = value.replace(tzinfo=datetime.timezone.utc) + # Regardless of what timezone is on the value, convert it to UTC. + value = value.astimezone(datetime.timezone.utc) + # Convert the datetime to a microsecond timestamp. + return int(calendar.timegm(value.timetuple()) * 1e6) + value.microsecond + return int(dt.timestamp() * 1000 * 1000) + + +@pytest.mark.asyncio +async def test_filter_limit_row_sample(capsys, table_id): + await filter_snippets_async.filter_limit_row_sample( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + assert "Reading data for" in out + + +@pytest.mark.asyncio +async def test_filter_limit_row_regex(capsys, table_id): + await filter_snippets_async.filter_limit_row_regex( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_limit_cells_per_col(capsys, table_id): + await filter_snippets_async.filter_limit_cells_per_col( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_limit_cells_per_row(capsys, table_id): + await filter_snippets_async.filter_limit_cells_per_row( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_limit_cells_per_row_offset(capsys, table_id): + await filter_snippets_async.filter_limit_cells_per_row_offset( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_limit_col_family_regex(capsys, table_id): + await filter_snippets_async.filter_limit_col_family_regex( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_limit_col_qualifier_regex(capsys, table_id): + await filter_snippets_async.filter_limit_col_qualifier_regex( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_limit_col_range(capsys, table_id): + await filter_snippets_async.filter_limit_col_range( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_limit_value_range(capsys, table_id): + await filter_snippets_async.filter_limit_value_range( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_limit_value_regex(capsys, table_id): + await filter_snippets_async.filter_limit_value_regex( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_limit_timestamp_range(capsys, table_id): + await filter_snippets_async.filter_limit_timestamp_range( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_limit_block_all(capsys, table_id): + await filter_snippets_async.filter_limit_block_all( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_limit_pass_all(capsys, table_id): + await filter_snippets_async.filter_limit_pass_all( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_modify_strip_value(capsys, table_id): + await filter_snippets_async.filter_modify_strip_value( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_modify_apply_label(capsys, table_id): + await filter_snippets_async.filter_modify_apply_label( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_composing_chain(capsys, table_id): + await filter_snippets_async.filter_composing_chain( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_composing_interleave(capsys, table_id): + await filter_snippets_async.filter_composing_interleave( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +@pytest.mark.asyncio +async def test_filter_composing_condition(capsys, table_id): + await filter_snippets_async.filter_composing_condition( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected diff --git a/packages/google-cloud-bigtable/samples/snippets/filters/filters_test.py b/packages/google-cloud-bigtable/samples/snippets/filters/filters_test.py new file mode 100644 index 000000000000..fe99886bdb0c --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/filters/filters_test.py @@ -0,0 +1,236 @@ +# Copyright 2020, Google LLC +# 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 datetime +import inspect +import os +import time +import uuid + +import pytest + +from . import filter_snippets +from .snapshots.snap_filters_test import snapshots +from ...utils import create_table_cm + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] +TABLE_ID = f"mobile-time-series-filters-{str(uuid.uuid4())[:16]}" + + +@pytest.fixture(scope="module", autouse=True) +def table_id(): + from google.cloud.bigtable.row_set import RowSet + + table_id = TABLE_ID + with create_table_cm(PROJECT, BIGTABLE_INSTANCE, table_id, {"stats_summary": None, "cell_plan": None}) as table: + + timestamp = datetime.datetime(2019, 5, 1) + timestamp_minus_hr = datetime.datetime(2019, 5, 1) - datetime.timedelta(hours=1) + + row_keys = [ + "phone#4c410523#20190501", + "phone#4c410523#20190502", + "phone#4c410523#20190505", + "phone#5c10102#20190501", + "phone#5c10102#20190502", + ] + + rows = [table.direct_row(row_key) for row_key in row_keys] + + rows[0].set_cell("stats_summary", "connected_cell", 1, timestamp) + rows[0].set_cell("stats_summary", "connected_wifi", 1, timestamp) + rows[0].set_cell("stats_summary", "os_build", "PQ2A.190405.003", timestamp) + rows[0].set_cell("cell_plan", "data_plan_01gb", "true", timestamp_minus_hr) + rows[0].set_cell("cell_plan", "data_plan_01gb", "false", timestamp) + rows[0].set_cell("cell_plan", "data_plan_05gb", "true", timestamp) + rows[1].set_cell("stats_summary", "connected_cell", 1, timestamp) + rows[1].set_cell("stats_summary", "connected_wifi", 1, timestamp) + rows[1].set_cell("stats_summary", "os_build", "PQ2A.190405.004", timestamp) + rows[1].set_cell("cell_plan", "data_plan_05gb", "true", timestamp) + rows[2].set_cell("stats_summary", "connected_cell", 0, timestamp) + rows[2].set_cell("stats_summary", "connected_wifi", 1, timestamp) + rows[2].set_cell("stats_summary", "os_build", "PQ2A.190406.000", timestamp) + rows[2].set_cell("cell_plan", "data_plan_05gb", "true", timestamp) + rows[3].set_cell("stats_summary", "connected_cell", 1, timestamp) + rows[3].set_cell("stats_summary", "connected_wifi", 1, timestamp) + rows[3].set_cell("stats_summary", "os_build", "PQ2A.190401.002", timestamp) + rows[3].set_cell("cell_plan", "data_plan_10gb", "true", timestamp) + rows[4].set_cell("stats_summary", "connected_cell", 1, timestamp) + rows[4].set_cell("stats_summary", "connected_wifi", 0, timestamp) + rows[4].set_cell("stats_summary", "os_build", "PQ2A.190406.000", timestamp) + rows[4].set_cell("cell_plan", "data_plan_10gb", "true", timestamp) + + table.mutate_rows(rows) + + # Ensure mutations have propagated. + row_set = RowSet() + + for row_key in row_keys: + row_set.add_row_key(row_key) + + fetched = list(table.read_rows(row_set=row_set)) + + while len(fetched) < len(rows): + time.sleep(5) + fetched = list(table.read_rows(row_set=row_set)) + + yield table_id + + +def test_filter_limit_row_sample(capsys, table_id): + filter_snippets.filter_limit_row_sample(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + assert "Reading data for" in out + + +def test_filter_limit_row_regex(capsys, table_id): + filter_snippets.filter_limit_row_regex(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_limit_cells_per_col(capsys, table_id): + filter_snippets.filter_limit_cells_per_col(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_limit_cells_per_row(capsys, table_id): + filter_snippets.filter_limit_cells_per_row(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_limit_cells_per_row_offset(capsys, table_id): + filter_snippets.filter_limit_cells_per_row_offset( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_limit_col_family_regex(capsys, table_id): + filter_snippets.filter_limit_col_family_regex(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_limit_col_qualifier_regex(capsys, table_id): + filter_snippets.filter_limit_col_qualifier_regex( + PROJECT, BIGTABLE_INSTANCE, table_id + ) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_limit_col_range(capsys, table_id): + filter_snippets.filter_limit_col_range(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_limit_value_range(capsys, table_id): + filter_snippets.filter_limit_value_range(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_limit_value_regex(capsys, table_id): + filter_snippets.filter_limit_value_regex(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_limit_timestamp_range(capsys, table_id): + filter_snippets.filter_limit_timestamp_range(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_limit_block_all(capsys, table_id): + filter_snippets.filter_limit_block_all(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_limit_pass_all(capsys, table_id): + filter_snippets.filter_limit_pass_all(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_modify_strip_value(capsys, table_id): + filter_snippets.filter_modify_strip_value(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_modify_apply_label(capsys, table_id): + filter_snippets.filter_modify_apply_label(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_composing_chain(capsys, table_id): + filter_snippets.filter_composing_chain(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_composing_interleave(capsys, table_id): + filter_snippets.filter_composing_interleave(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_filter_composing_condition(capsys, table_id): + filter_snippets.filter_composing_condition(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected diff --git a/packages/google-cloud-bigtable/samples/snippets/filters/noxfile.py b/packages/google-cloud-bigtable/samples/snippets/filters/noxfile.py new file mode 100644 index 000000000000..a169b5b5b464 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/filters/noxfile.py @@ -0,0 +1,292 @@ +# Copyright 2019 Google LLC +# +# 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 __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, Optional + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in ( + "True", + "true", +) + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8") + else: + session.install("flake8", "flake8-annotations") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# format = isort + black +# + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list.extend(glob.glob("**/tests", recursive=True)) + + if len(test_list) == 0: + print("No tests found, skipping directory.") + return + + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + concurrent_args = [] + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + with open("requirements.txt") as rfile: + packages = rfile.read() + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install( + "-r", "requirements-test.txt", "-c", "constraints-test.txt" + ) + else: + session.install("-r", "requirements-test.txt") + with open("requirements-test.txt") as rtfile: + packages += rtfile.read() + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + if "pytest-parallel" in packages: + concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + elif "pytest-xdist" in packages: + concurrent_args.extend(['-n', 'auto']) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs + concurrent_args), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/packages/google-cloud-bigtable/samples/snippets/filters/requirements-test.txt b/packages/google-cloud-bigtable/samples/snippets/filters/requirements-test.txt new file mode 100644 index 000000000000..ee4ba018603b --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/filters/requirements-test.txt @@ -0,0 +1,2 @@ +pytest +pytest-asyncio diff --git a/packages/google-cloud-bigtable/samples/snippets/filters/requirements.txt b/packages/google-cloud-bigtable/samples/snippets/filters/requirements.txt new file mode 100644 index 000000000000..730d25dec63f --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/filters/requirements.txt @@ -0,0 +1 @@ +google-cloud-bigtable==2.35.0 diff --git a/packages/google-cloud-bigtable/samples/snippets/filters/snapshots/__init__.py b/packages/google-cloud-bigtable/samples/snippets/filters/snapshots/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/samples/snippets/filters/snapshots/snap_filters_test.py b/packages/google-cloud-bigtable/samples/snippets/filters/snapshots/snap_filters_test.py new file mode 100644 index 000000000000..2331c93bc1b6 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/filters/snapshots/snap_filters_test.py @@ -0,0 +1,480 @@ +# -*- coding: utf-8 -*- +# this was previously implemented using the `snapshottest` package (https://goo.gl/zC4yUc), +# which is not compatible with Python 3.12. So we moved to a standard dictionary storing +# expected outputs for each test +from __future__ import unicode_literals + + +snapshots = {} + +snapshots['test_filter_limit_row_regex'] = '''Reading data for phone#4c410523#20190501: +Column Family cell_plan +\tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 +\tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190501: +Column Family cell_plan +\tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190401.002 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_filter_limit_cells_per_col'] = '''Reading data for phone#4c410523#20190501: +Column Family cell_plan +\tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 +\tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190505: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190501: +Column Family cell_plan +\tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190401.002 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190502: +Column Family cell_plan +\tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_filter_limit_cells_per_row'] = '''Reading data for phone#4c410523#20190501: +Column Family cell_plan +\tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 +\tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190505: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190501: +Column Family cell_plan +\tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190502: +Column Family cell_plan +\tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_filter_limit_cells_per_row_offset'] = '''Reading data for phone#4c410523#20190501: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family stats_summary +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190505: +Column Family stats_summary +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190501: +Column Family stats_summary +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190401.002 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190502: +Column Family stats_summary +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_filter_limit_col_family_regex'] = '''Reading data for phone#4c410523#20190501: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190505: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190501: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190401.002 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190502: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_filter_limit_col_qualifier_regex'] = '''Reading data for phone#4c410523#20190501: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190505: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190501: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190502: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_filter_limit_col_range'] = '''Reading data for phone#4c410523#20190501: +Column Family cell_plan +\tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 +\tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190505: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_filter_limit_value_range'] = '''Reading data for phone#4c410523#20190501: +Column Family stats_summary +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family stats_summary +\tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_filter_limit_value_regex'] = '''Reading data for phone#4c410523#20190501: +Column Family stats_summary +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family stats_summary +\tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190505: +Column Family stats_summary +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190501: +Column Family stats_summary +\tos_build: PQ2A.190401.002 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190502: +Column Family stats_summary +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_filter_limit_timestamp_range'] = '''Reading data for phone#4c410523#20190501: +Column Family cell_plan +\tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 + +''' + +snapshots['test_filter_limit_block_all'] = '' + +snapshots['test_filter_limit_pass_all'] = '''Reading data for phone#4c410523#20190501: +Column Family cell_plan +\tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 +\tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190505: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190501: +Column Family cell_plan +\tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190401.002 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190502: +Column Family cell_plan +\tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_filter_modify_strip_value'] = '''Reading data for phone#4c410523#20190501: +Column Family cell_plan +\tdata_plan_01gb: @2019-05-01 00:00:00+00:00 +\tdata_plan_01gb: @2019-04-30 23:00:00+00:00 +\tdata_plan_05gb: @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: @2019-05-01 00:00:00+00:00 +\tconnected_wifi: @2019-05-01 00:00:00+00:00 +\tos_build: @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family cell_plan +\tdata_plan_05gb: @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: @2019-05-01 00:00:00+00:00 +\tconnected_wifi: @2019-05-01 00:00:00+00:00 +\tos_build: @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190505: +Column Family cell_plan +\tdata_plan_05gb: @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: @2019-05-01 00:00:00+00:00 +\tconnected_wifi: @2019-05-01 00:00:00+00:00 +\tos_build: @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190501: +Column Family cell_plan +\tdata_plan_10gb: @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: @2019-05-01 00:00:00+00:00 +\tconnected_wifi: @2019-05-01 00:00:00+00:00 +\tos_build: @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190502: +Column Family cell_plan +\tdata_plan_10gb: @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tconnected_cell: @2019-05-01 00:00:00+00:00 +\tconnected_wifi: @2019-05-01 00:00:00+00:00 +\tos_build: @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_filter_modify_apply_label'] = '''Reading data for phone#4c410523#20190501: +Column Family cell_plan +\tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 [labelled] +\tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 [labelled] +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 [labelled] +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [labelled] +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [labelled] +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 [labelled] + +Reading data for phone#4c410523#20190502: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 [labelled] +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [labelled] +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [labelled] +\tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 [labelled] + +Reading data for phone#4c410523#20190505: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 [labelled] +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 [labelled] +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [labelled] +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 [labelled] + +Reading data for phone#5c10102#20190501: +Column Family cell_plan +\tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 [labelled] +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [labelled] +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [labelled] +\tos_build: PQ2A.190401.002 @2019-05-01 00:00:00+00:00 [labelled] + +Reading data for phone#5c10102#20190502: +Column Family cell_plan +\tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 [labelled] +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [labelled] +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 [labelled] +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 [labelled] + +''' + +snapshots['test_filter_composing_chain'] = '''Reading data for phone#4c410523#20190501: +Column Family cell_plan +\tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190505: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190501: +Column Family cell_plan +\tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190502: +Column Family cell_plan +\tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_filter_composing_interleave'] = '''Reading data for phone#4c410523#20190501: +Column Family cell_plan +\tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190505: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190501: +Column Family cell_plan +\tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tos_build: PQ2A.190401.002 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190502: +Column Family cell_plan +\tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 +Column Family stats_summary +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_filter_composing_condition'] = '''Reading data for phone#4c410523#20190501: +Column Family cell_plan +\tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 [filtered-out] +\tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 [filtered-out] +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 [filtered-out] +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [filtered-out] +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [filtered-out] +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 [filtered-out] + +Reading data for phone#4c410523#20190502: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 [filtered-out] +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [filtered-out] +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [filtered-out] +\tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 [filtered-out] + +Reading data for phone#4c410523#20190505: +Column Family cell_plan +\tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 [filtered-out] +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 [filtered-out] +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [filtered-out] +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 [filtered-out] + +Reading data for phone#5c10102#20190501: +Column Family cell_plan +\tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 [passed-filter] +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [passed-filter] +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [passed-filter] +\tos_build: PQ2A.190401.002 @2019-05-01 00:00:00+00:00 [passed-filter] + +Reading data for phone#5c10102#20190502: +Column Family cell_plan +\tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 [passed-filter] +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 [passed-filter] +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 [passed-filter] +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 [passed-filter] + +''' diff --git a/packages/google-cloud-bigtable/samples/snippets/reads/__init__.py b/packages/google-cloud-bigtable/samples/snippets/reads/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/samples/snippets/reads/noxfile.py b/packages/google-cloud-bigtable/samples/snippets/reads/noxfile.py new file mode 100644 index 000000000000..a169b5b5b464 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/reads/noxfile.py @@ -0,0 +1,292 @@ +# Copyright 2019 Google LLC +# +# 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 __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, Optional + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in ( + "True", + "true", +) + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8") + else: + session.install("flake8", "flake8-annotations") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# format = isort + black +# + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list.extend(glob.glob("**/tests", recursive=True)) + + if len(test_list) == 0: + print("No tests found, skipping directory.") + return + + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + concurrent_args = [] + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + with open("requirements.txt") as rfile: + packages = rfile.read() + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install( + "-r", "requirements-test.txt", "-c", "constraints-test.txt" + ) + else: + session.install("-r", "requirements-test.txt") + with open("requirements-test.txt") as rtfile: + packages += rtfile.read() + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + if "pytest-parallel" in packages: + concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + elif "pytest-xdist" in packages: + concurrent_args.extend(['-n', 'auto']) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs + concurrent_args), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/packages/google-cloud-bigtable/samples/snippets/reads/read_snippets.py b/packages/google-cloud-bigtable/samples/snippets/reads/read_snippets.py new file mode 100644 index 000000000000..210ca73a71bd --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/reads/read_snippets.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python + +# Copyright 2020, Google LLC +# 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 bigtable_reads_row] +def read_row(project_id, instance_id, table_id): + from google.cloud import bigtable + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + row_key = "phone#4c410523#20190501" + + row = table.read_row(row_key) + print_row(row) + + +# [END bigtable_reads_row] + +# [START bigtable_reads_row_partial] +def read_row_partial(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + row_key = "phone#4c410523#20190501" + col_filter = row_filters.ColumnQualifierRegexFilter(b"os_build") + + row = table.read_row(row_key, filter_=col_filter) + print_row(row) + + +# [END bigtable_reads_row_partial] +# [START bigtable_reads_rows] +def read_rows(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable.row_set import RowSet + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + row_set = RowSet() + row_set.add_row_key(b"phone#4c410523#20190501") + row_set.add_row_key(b"phone#4c410523#20190502") + + rows = table.read_rows(row_set=row_set) + for row in rows: + print_row(row) + + +# [END bigtable_reads_rows] +# [START bigtable_reads_row_range] +def read_row_range(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable.row_set import RowSet + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + row_set = RowSet() + row_set.add_row_range_from_keys( + start_key=b"phone#4c410523#20190501", end_key=b"phone#4c410523#201906201" + ) + + rows = table.read_rows(row_set=row_set) + for row in rows: + print_row(row) + + +# [END bigtable_reads_row_range] +# [START bigtable_reads_row_ranges] +def read_row_ranges(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable.row_set import RowSet + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + row_set = RowSet() + row_set.add_row_range_from_keys( + start_key=b"phone#4c410523#20190501", end_key=b"phone#4c410523#201906201" + ) + row_set.add_row_range_from_keys( + start_key=b"phone#5c10102#20190501", end_key=b"phone#5c10102#201906201" + ) + + rows = table.read_rows(row_set=row_set) + for row in rows: + print_row(row) + + +# [END bigtable_reads_row_ranges] +# [START bigtable_reads_prefix] +def read_prefix(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable.row_set import RowSet + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + prefix = "phone#" + end_key = prefix[:-1] + chr(ord(prefix[-1]) + 1) + + row_set = RowSet() + row_set.add_row_range_from_keys(prefix.encode("utf-8"), end_key.encode("utf-8")) + + rows = table.read_rows(row_set=row_set) + for row in rows: + print_row(row) + + +# [END bigtable_reads_prefix] +# [START bigtable_reads_filter] +def read_filter(project_id, instance_id, table_id): + from google.cloud import bigtable + from google.cloud.bigtable import row_filters + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + rows = table.read_rows(filter_=row_filters.ValueRegexFilter(b"PQ2A.*$")) + for row in rows: + print_row(row) + + +# [END bigtable_reads_filter] + +# [START bigtable_reads_print] +def print_row(row): + print("Reading data for {}:".format(row.row_key.decode("utf-8"))) + for cf, cols in sorted(row.cells.items()): + print("Column Family {}".format(cf)) + for col, cells in sorted(cols.items()): + for cell in cells: + labels = ( + " [{}]".format(",".join(cell.labels)) if len(cell.labels) else "" + ) + print( + "\t{}: {} @{}{}".format( + col.decode("utf-8"), + cell.value.decode("utf-8"), + cell.timestamp, + labels, + ) + ) + print("") + + +# [END bigtable_reads_print] diff --git a/packages/google-cloud-bigtable/samples/snippets/reads/reads_test.py b/packages/google-cloud-bigtable/samples/snippets/reads/reads_test.py new file mode 100644 index 000000000000..0078ce5981af --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/reads/reads_test.py @@ -0,0 +1,117 @@ +# Copyright 2020, Google LLC +# 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 datetime +import os +import inspect +import uuid + +import pytest + +from .snapshots.snap_reads_test import snapshots +from . import read_snippets +from ...utils import create_table_cm + + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] +TABLE_ID = f"mobile-time-series-reads-{str(uuid.uuid4())[:16]}" + + +@pytest.fixture(scope="module", autouse=True) +def table_id(): + with create_table_cm(PROJECT, BIGTABLE_INSTANCE, TABLE_ID, {"stats_summary": None}) as table: + timestamp = datetime.datetime(2019, 5, 1) + rows = [ + table.direct_row("phone#4c410523#20190501"), + table.direct_row("phone#4c410523#20190502"), + table.direct_row("phone#4c410523#20190505"), + table.direct_row("phone#5c10102#20190501"), + table.direct_row("phone#5c10102#20190502"), + ] + + rows[0].set_cell("stats_summary", "connected_cell", 1, timestamp) + rows[0].set_cell("stats_summary", "connected_wifi", 1, timestamp) + rows[0].set_cell("stats_summary", "os_build", "PQ2A.190405.003", timestamp) + rows[1].set_cell("stats_summary", "connected_cell", 1, timestamp) + rows[1].set_cell("stats_summary", "connected_wifi", 1, timestamp) + rows[1].set_cell("stats_summary", "os_build", "PQ2A.190405.004", timestamp) + rows[2].set_cell("stats_summary", "connected_cell", 0, timestamp) + rows[2].set_cell("stats_summary", "connected_wifi", 1, timestamp) + rows[2].set_cell("stats_summary", "os_build", "PQ2A.190406.000", timestamp) + rows[3].set_cell("stats_summary", "connected_cell", 1, timestamp) + rows[3].set_cell("stats_summary", "connected_wifi", 1, timestamp) + rows[3].set_cell("stats_summary", "os_build", "PQ2A.190401.002", timestamp) + rows[4].set_cell("stats_summary", "connected_cell", 1, timestamp) + rows[4].set_cell("stats_summary", "connected_wifi", 0, timestamp) + rows[4].set_cell("stats_summary", "os_build", "PQ2A.190406.000", timestamp) + + table.mutate_rows(rows) + + yield TABLE_ID + + +def test_read_row(capsys, table_id): + read_snippets.read_row(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_read_row_partial(capsys, table_id): + read_snippets.read_row_partial(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_read_rows(capsys, table_id): + read_snippets.read_rows(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_read_row_range(capsys, table_id): + read_snippets.read_row_range(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_read_row_ranges(capsys, table_id): + read_snippets.read_row_ranges(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_read_prefix(capsys, table_id): + read_snippets.read_prefix(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected + + +def test_read_filter(capsys, table_id): + read_snippets.read_filter(PROJECT, BIGTABLE_INSTANCE, table_id) + + out, _ = capsys.readouterr() + expected = snapshots[inspect.currentframe().f_code.co_name] + assert out == expected diff --git a/packages/google-cloud-bigtable/samples/snippets/reads/requirements-test.txt b/packages/google-cloud-bigtable/samples/snippets/reads/requirements-test.txt new file mode 100644 index 000000000000..e079f8a6038d --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/reads/requirements-test.txt @@ -0,0 +1 @@ +pytest diff --git a/packages/google-cloud-bigtable/samples/snippets/reads/requirements.txt b/packages/google-cloud-bigtable/samples/snippets/reads/requirements.txt new file mode 100644 index 000000000000..730d25dec63f --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/reads/requirements.txt @@ -0,0 +1 @@ +google-cloud-bigtable==2.35.0 diff --git a/packages/google-cloud-bigtable/samples/snippets/reads/snapshots/__init__.py b/packages/google-cloud-bigtable/samples/snippets/reads/snapshots/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/samples/snippets/reads/snapshots/snap_reads_test.py b/packages/google-cloud-bigtable/samples/snippets/reads/snapshots/snap_reads_test.py new file mode 100644 index 000000000000..564a4df7eecc --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/reads/snapshots/snap_reads_test.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +# this was previously implemented using the `snapshottest` package (https://goo.gl/zC4yUc), +# which is not compatible with Python 3.12. So we moved to a standard dictionary storing +# expected outputs for each test +from __future__ import unicode_literals + +snapshots = {} + +snapshots['test_read_row_partial'] = '''Reading data for phone#4c410523#20190501: +Column Family stats_summary +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_read_rows'] = '''Reading data for phone#4c410523#20190501: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_read_row_range'] = '''Reading data for phone#4c410523#20190501: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190505: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_read_row_ranges'] = '''Reading data for phone#4c410523#20190501: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190505: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190501: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190401.002 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190502: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_read_prefix'] = '''Reading data for phone#4c410523#20190501: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190505: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190501: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190401.002 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190502: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_read_filter'] = '''Reading data for phone#4c410523#20190501: +Column Family stats_summary +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190502: +Column Family stats_summary +\tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 + +Reading data for phone#4c410523#20190505: +Column Family stats_summary +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190501: +Column Family stats_summary +\tos_build: PQ2A.190401.002 @2019-05-01 00:00:00+00:00 + +Reading data for phone#5c10102#20190502: +Column Family stats_summary +\tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 + +''' + +snapshots['test_read_row'] = '''Reading data for phone#4c410523#20190501: +Column Family stats_summary +\tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 +\tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 + +''' diff --git a/packages/google-cloud-bigtable/samples/snippets/writes/__init__.py b/packages/google-cloud-bigtable/samples/snippets/writes/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/samples/snippets/writes/noxfile.py b/packages/google-cloud-bigtable/samples/snippets/writes/noxfile.py new file mode 100644 index 000000000000..a169b5b5b464 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/writes/noxfile.py @@ -0,0 +1,292 @@ +# Copyright 2019 Google LLC +# +# 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 __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, Optional + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in ( + "True", + "true", +) + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8") + else: + session.install("flake8", "flake8-annotations") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# format = isort + black +# + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list.extend(glob.glob("**/tests", recursive=True)) + + if len(test_list) == 0: + print("No tests found, skipping directory.") + return + + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + concurrent_args = [] + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + with open("requirements.txt") as rfile: + packages = rfile.read() + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install( + "-r", "requirements-test.txt", "-c", "constraints-test.txt" + ) + else: + session.install("-r", "requirements-test.txt") + with open("requirements-test.txt") as rtfile: + packages += rtfile.read() + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + if "pytest-parallel" in packages: + concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + elif "pytest-xdist" in packages: + concurrent_args.extend(['-n', 'auto']) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs + concurrent_args), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/packages/google-cloud-bigtable/samples/snippets/writes/requirements-test.txt b/packages/google-cloud-bigtable/samples/snippets/writes/requirements-test.txt new file mode 100644 index 000000000000..5e15eb26f589 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/writes/requirements-test.txt @@ -0,0 +1,2 @@ +backoff==2.2.1 +pytest diff --git a/packages/google-cloud-bigtable/samples/snippets/writes/requirements.txt b/packages/google-cloud-bigtable/samples/snippets/writes/requirements.txt new file mode 100644 index 000000000000..54c0c14a3c5b --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/writes/requirements.txt @@ -0,0 +1 @@ +google-cloud-bigtable==2.35.0 \ No newline at end of file diff --git a/packages/google-cloud-bigtable/samples/snippets/writes/write_batch.py b/packages/google-cloud-bigtable/samples/snippets/writes/write_batch.py new file mode 100644 index 000000000000..a583bb7134e1 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/writes/write_batch.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +# Copyright 2019, Google LLC +# 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 bigtable_writes_batch] +from datetime import datetime, timezone + +from google.cloud import bigtable +from google.cloud.bigtable.batcher import MutationsBatcher + + +def write_batch(project_id, instance_id, table_id): + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + with MutationsBatcher(table=table) as batcher: + timestamp = datetime.now(timezone.utc) + column_family_id = "stats_summary" + + rows = [ + table.direct_row("tablet#a0b81f74#20190501"), + table.direct_row("tablet#a0b81f74#20190502"), + ] + + rows[0].set_cell(column_family_id, "connected_wifi", 1, timestamp) + rows[0].set_cell(column_family_id, "os_build", "12155.0.0-rc1", timestamp) + rows[1].set_cell(column_family_id, "connected_wifi", 1, timestamp) + rows[1].set_cell(column_family_id, "os_build", "12145.0.0-rc6", timestamp) + + batcher.mutate_rows(rows) + + print("Successfully wrote 2 rows.") + + +# [END bigtable_writes_batch] diff --git a/packages/google-cloud-bigtable/samples/snippets/writes/write_conditionally.py b/packages/google-cloud-bigtable/samples/snippets/writes/write_conditionally.py new file mode 100644 index 000000000000..b6f05fba77f4 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/writes/write_conditionally.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +# Copyright 2019, Google LLC +# 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 bigtable_writes_conditional] +from datetime import datetime, timezone + +from google.cloud import bigtable +from google.cloud.bigtable import row_filters + + +def write_conditional(project_id, instance_id, table_id): + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + timestamp = datetime.now(timezone.utc) + column_family_id = "stats_summary" + + row_key = "phone#4c410523#20190501" + + row_filter = row_filters.RowFilterChain( + filters=[ + row_filters.FamilyNameRegexFilter(column_family_id), + row_filters.ColumnQualifierRegexFilter("os_build"), + row_filters.ValueRegexFilter("PQ2A\\..*"), + ] + ) + row = table.conditional_row(row_key, filter_=row_filter) + row.set_cell(column_family_id, "os_name", "android", timestamp) + row.commit() + + print("Successfully updated row's os_name.") + + +# [END bigtable_writes_conditional] diff --git a/packages/google-cloud-bigtable/samples/snippets/writes/write_increment.py b/packages/google-cloud-bigtable/samples/snippets/writes/write_increment.py new file mode 100644 index 000000000000..ac8e2d16af34 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/writes/write_increment.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +# Copyright 2019, Google LLC +# 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 bigtable_writes_increment] +from google.cloud import bigtable + + +def write_increment(project_id, instance_id, table_id): + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + column_family_id = "stats_summary" + + row_key = "phone#4c410523#20190501" + row = table.append_row(row_key) + + # Decrement the connected_wifi value by 1. + row.increment_cell_value(column_family_id, "connected_wifi", -1) + row.commit() + + print("Successfully updated row {}.".format(row_key)) + + +# [END bigtable_writes_increment] diff --git a/packages/google-cloud-bigtable/samples/snippets/writes/write_simple.py b/packages/google-cloud-bigtable/samples/snippets/writes/write_simple.py new file mode 100644 index 000000000000..fb7074bc526e --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/writes/write_simple.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +# Copyright 2019, Google LLC +# 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 bigtable_writes_simple] +from datetime import datetime, timezone + +from google.cloud import bigtable + + +def write_simple(project_id, instance_id, table_id): + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + timestamp = datetime.now(timezone.utc) + column_family_id = "stats_summary" + + row_key = "phone#4c410523#20190501" + + row = table.direct_row(row_key) + row.set_cell(column_family_id, "connected_cell", 1, timestamp) + row.set_cell(column_family_id, "connected_wifi", 1, timestamp) + row.set_cell(column_family_id, "os_build", "PQ2A.190405.003", timestamp) + + row.commit() + + print("Successfully wrote row {}.".format(row_key)) + + +# [END bigtable_writes_simple] diff --git a/packages/google-cloud-bigtable/samples/snippets/writes/writes_test.py b/packages/google-cloud-bigtable/samples/snippets/writes/writes_test.py new file mode 100644 index 000000000000..2c7a3d62b162 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/snippets/writes/writes_test.py @@ -0,0 +1,73 @@ +# Copyright 2018 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 os + +import backoff +from google.api_core.exceptions import DeadlineExceeded +import pytest +import uuid + +from .write_batch import write_batch +from .write_conditionally import write_conditional +from .write_increment import write_increment +from .write_simple import write_simple +from ...utils import create_table_cm + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] +TABLE_ID = f"mobile-time-series-writes-{str(uuid.uuid4())[:16]}" + + +@pytest.fixture +def table_id(): + with create_table_cm(PROJECT, BIGTABLE_INSTANCE, TABLE_ID, {"stats_summary": None}): + yield TABLE_ID + + +def test_writes(capsys, table_id): + + # `row.commit()` sometimes ends up with DeadlineExceeded, so now + # we put retries with a hard deadline. + @backoff.on_exception(backoff.expo, DeadlineExceeded, max_time=60) + def _write_simple(): + write_simple(PROJECT, BIGTABLE_INSTANCE, table_id) + + _write_simple() + out, _ = capsys.readouterr() + assert "Successfully wrote row" in out + + @backoff.on_exception(backoff.expo, DeadlineExceeded, max_time=60) + def _write_increment(): + write_increment(PROJECT, BIGTABLE_INSTANCE, table_id) + + _write_increment() + out, _ = capsys.readouterr() + assert "Successfully updated row" in out + + @backoff.on_exception(backoff.expo, DeadlineExceeded, max_time=60) + def _write_conditional(): + write_conditional(PROJECT, BIGTABLE_INSTANCE, table_id) + + _write_conditional() + out, _ = capsys.readouterr() + assert "Successfully updated row's os_name" in out + + @backoff.on_exception(backoff.expo, DeadlineExceeded, max_time=60) + def _write_batch(): + write_batch(PROJECT, BIGTABLE_INSTANCE, table_id) + + _write_batch() + out, _ = capsys.readouterr() + assert "Successfully wrote 2 rows" in out diff --git a/packages/google-cloud-bigtable/samples/tableadmin/README.md b/packages/google-cloud-bigtable/samples/tableadmin/README.md new file mode 100644 index 000000000000..b2f6a13af55a --- /dev/null +++ b/packages/google-cloud-bigtable/samples/tableadmin/README.md @@ -0,0 +1,52 @@ +[//]: # "This README.md file is auto-generated, all changes to this file will be lost." +[//]: # "To regenerate it, use `python -m synthtool`." + +## Python Samples for Cloud Bigtable + +This directory contains samples for Cloud Bigtable, which may be used as a refererence for how to use this product. +Samples, quickstarts, and other documentation are available at cloud.google.com. + + +### Table Admin + +Demonstrates how to connect to Cloud Bigtable and run some basic operations. + + +Open in Cloud Shell + + +To run this sample: + +1. If this is your first time working with GCP products, you will need to set up [the Cloud SDK][cloud_sdk] or utilize [Google Cloud Shell][gcloud_shell]. This sample may [require authetication][authentication] and you will need to [enable billing][enable_billing]. + +1. Make a fork of this repo and clone the branch locally, then navigate to the sample directory you want to use. + +1. Install the dependencies needed to run the samples. + + pip install -r requirements.txt + +1. Run the sample using + + python tableadmin.py + + + +
usage: tableadmin.py [-h] [run] [delete] [--table TABLE] project_id instance_id 


Demonstrates how to connect to Cloud Bigtable and run some basic operations.
Prerequisites: - Create a Cloud Bigtable cluster.
https://cloud.google.com/bigtable/docs/creating-cluster - Set your Google
Application Default Credentials.
https://developers.google.com/identity/protocols/application-default-
credentials


positional arguments:
  project_id     Your Cloud Platform project ID.
  instance_id    ID of the Cloud Bigtable instance to connect to.


optional arguments:
  -h, --help     show this help message and exit
  --table TABLE  Table to create and destroy. (default: Hello-Bigtable)
+ +## Additional Information + +You can read the documentation for more details on API usage and use GitHub +to browse the source and [report issues][issues]. + +### Contributing +View the [contributing guidelines][contrib_guide], the [Python style guide][py_style] for more information. + +[authentication]: https://cloud.google.com/docs/authentication/getting-started +[enable_billing]:https://cloud.google.com/apis/docs/getting-started#enabling_billing +[client_library_python]: https://googlecloudplatform.github.io/google-cloud-python/ +[issues]: https://github.com/GoogleCloudPlatform/google-cloud-python/issues +[contrib_guide]: https://github.com/googleapis/google-cloud-python/blob/main/CONTRIBUTING.rst +[py_style]: http://google.github.io/styleguide/pyguide.html +[cloud_sdk]: https://cloud.google.com/sdk/docs +[gcloud_shell]: https://cloud.google.com/shell/docs +[gcloud_shell]: https://cloud.google.com/shell/docs diff --git a/packages/google-cloud-bigtable/samples/tableadmin/__init__.py b/packages/google-cloud-bigtable/samples/tableadmin/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/google-cloud-bigtable/samples/tableadmin/noxfile.py b/packages/google-cloud-bigtable/samples/tableadmin/noxfile.py new file mode 100644 index 000000000000..a169b5b5b464 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/tableadmin/noxfile.py @@ -0,0 +1,292 @@ +# Copyright 2019 Google LLC +# +# 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 __future__ import print_function + +import glob +import os +from pathlib import Path +import sys +from typing import Callable, Dict, Optional + +import nox + + +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING +# DO NOT EDIT THIS FILE EVER! +# WARNING - WARNING - WARNING - WARNING - WARNING +# WARNING - WARNING - WARNING - WARNING - WARNING + +BLACK_VERSION = "black==22.3.0" +ISORT_VERSION = "isort==5.10.1" + +# Copy `noxfile_config.py` to your directory and modify it instead. + +# `TEST_CONFIG` dict is a configuration hook that allows users to +# modify the test configurations. The values here should be in sync +# with `noxfile_config.py`. Users will copy `noxfile_config.py` into +# their directory and modify it. + +TEST_CONFIG = { + # You can opt out from the test for specific Python versions. + "ignored_versions": [], + # Old samples are opted out of enforcing Python type hints + # All new samples should feature them + "enforce_type_hints": False, + # An envvar key for determining the project id to use. Change it + # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a + # build specific Cloud project. You can also use your own string + # to use your own Cloud project. + "gcloud_project_env": "GOOGLE_CLOUD_PROJECT", + # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT', + # If you need to use a specific version of pip, + # change pip_version_override to the string representation + # of the version number, for example, "20.2.4" + "pip_version_override": None, + # A dictionary you want to inject into your test. Don't put any + # secrets here. These values will override predefined values. + "envs": {}, +} + + +try: + # Ensure we can import noxfile_config in the project's directory. + sys.path.append(".") + from noxfile_config import TEST_CONFIG_OVERRIDE +except ImportError as e: + print("No user noxfile_config found: detail: {}".format(e)) + TEST_CONFIG_OVERRIDE = {} + +# Update the TEST_CONFIG with the user supplied values. +TEST_CONFIG.update(TEST_CONFIG_OVERRIDE) + + +def get_pytest_env_vars() -> Dict[str, str]: + """Returns a dict for pytest invocation.""" + ret = {} + + # Override the GCLOUD_PROJECT and the alias. + env_key = TEST_CONFIG["gcloud_project_env"] + # This should error out if not set. + ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key] + + # Apply user supplied envs. + ret.update(TEST_CONFIG["envs"]) + return ret + + +# DO NOT EDIT - automatically generated. +# All versions used to test samples. +ALL_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +# Any default versions that should be ignored. +IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"] + +TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS]) + +INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in ( + "True", + "true", +) + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + +# +# Style Checks +# + + +# Linting with flake8. +# +# We ignore the following rules: +# E203: whitespace before ‘:’ +# E266: too many leading ‘#’ for block comment +# E501: line too long +# I202: Additional newline in a section of imports +# +# We also need to specify the rules which are ignored by default: +# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121'] +FLAKE8_COMMON_ARGS = [ + "--show-source", + "--builtin=gettext", + "--max-complexity=20", + "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py", + "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202", + "--max-line-length=88", +] + + +@nox.session +def lint(session: nox.sessions.Session) -> None: + if not TEST_CONFIG["enforce_type_hints"]: + session.install("flake8") + else: + session.install("flake8", "flake8-annotations") + + args = FLAKE8_COMMON_ARGS + [ + ".", + ] + session.run("flake8", *args) + + +# +# Black +# + + +@nox.session +def blacken(session: nox.sessions.Session) -> None: + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + session.run("black", *python_files) + + +# +# format = isort + black +# + +@nox.session +def format(session: nox.sessions.Session) -> None: + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + python_files = [path for path in os.listdir(".") if path.endswith(".py")] + + # Use the --fss option to sort imports using strict alphabetical order. + # See https://pycqa.github.io/isort/docs/configuration/options.html#force-sort-within-sections + session.run("isort", "--fss", *python_files) + session.run("black", *python_files) + + +# +# Sample Tests +# + + +PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"] + + +def _session_tests( + session: nox.sessions.Session, post_install: Callable = None +) -> None: + # check for presence of tests + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list.extend(glob.glob("**/tests", recursive=True)) + + if len(test_list) == 0: + print("No tests found, skipping directory.") + return + + if TEST_CONFIG["pip_version_override"]: + pip_version = TEST_CONFIG["pip_version_override"] + session.install(f"pip=={pip_version}") + """Runs py.test for a particular project.""" + concurrent_args = [] + if os.path.exists("requirements.txt"): + if os.path.exists("constraints.txt"): + session.install("-r", "requirements.txt", "-c", "constraints.txt") + else: + session.install("-r", "requirements.txt") + with open("requirements.txt") as rfile: + packages = rfile.read() + + if os.path.exists("requirements-test.txt"): + if os.path.exists("constraints-test.txt"): + session.install( + "-r", "requirements-test.txt", "-c", "constraints-test.txt" + ) + else: + session.install("-r", "requirements-test.txt") + with open("requirements-test.txt") as rtfile: + packages += rtfile.read() + + if INSTALL_LIBRARY_FROM_SOURCE: + session.install("-e", _get_repo_root()) + + if post_install: + post_install(session) + + if "pytest-parallel" in packages: + concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + elif "pytest-xdist" in packages: + concurrent_args.extend(['-n', 'auto']) + + session.run( + "pytest", + *(PYTEST_COMMON_ARGS + session.posargs + concurrent_args), + # Pytest will return 5 when no tests are collected. This can happen + # on travis where slow and flaky tests are excluded. + # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html + success_codes=[0, 5], + env=get_pytest_env_vars(), + ) + + +@nox.session(python=ALL_VERSIONS) +def py(session: nox.sessions.Session) -> None: + """Runs py.test for a sample using the specified version of Python.""" + if session.python in TESTED_VERSIONS: + _session_tests(session) + else: + session.skip( + "SKIPPED: {} tests are disabled for this sample.".format(session.python) + ) + + +# +# Readmegen +# + + +def _get_repo_root() -> Optional[str]: + """ Returns the root folder of the project. """ + # Get root of this repository. Assume we don't have directories nested deeper than 10 items. + p = Path(os.getcwd()) + for i in range(10): + if p is None: + break + if Path(p / ".git").exists(): + return str(p) + # .git is not available in repos cloned via Cloud Build + # setup.py is always in the library's root, so use that instead + # https://github.com/googleapis/synthtool/issues/792 + if Path(p / "setup.py").exists(): + return str(p) + p = p.parent + raise Exception("Unable to detect repository root.") + + +GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")]) + + +@nox.session +@nox.parametrize("path", GENERATED_READMES) +def readmegen(session: nox.sessions.Session, path: str) -> None: + """(Re-)generates the readme for a sample.""" + session.install("jinja2", "pyyaml") + dir_ = os.path.dirname(path) + + if os.path.exists(os.path.join(dir_, "requirements.txt")): + session.install("-r", os.path.join(dir_, "requirements.txt")) + + in_file = os.path.join(dir_, "README.rst.in") + session.run( + "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file + ) diff --git a/packages/google-cloud-bigtable/samples/tableadmin/requirements-test.txt b/packages/google-cloud-bigtable/samples/tableadmin/requirements-test.txt new file mode 100644 index 000000000000..f01fd134c400 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/tableadmin/requirements-test.txt @@ -0,0 +1,2 @@ +pytest +google-cloud-testutils==1.7.0 diff --git a/packages/google-cloud-bigtable/samples/tableadmin/requirements.txt b/packages/google-cloud-bigtable/samples/tableadmin/requirements.txt new file mode 100644 index 000000000000..730d25dec63f --- /dev/null +++ b/packages/google-cloud-bigtable/samples/tableadmin/requirements.txt @@ -0,0 +1 @@ +google-cloud-bigtable==2.35.0 diff --git a/packages/google-cloud-bigtable/samples/tableadmin/tableadmin.py b/packages/google-cloud-bigtable/samples/tableadmin/tableadmin.py new file mode 100644 index 000000000000..ad00e57887c3 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/tableadmin/tableadmin.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python + +# Copyright 2018, Google LLC +# 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. + +"""Demonstrates how to connect to Cloud Bigtable and run some basic operations. +# http://www.apache.org/licenses/LICENSE-2.0 +Prerequisites: +- Create a Cloud Bigtable cluster. + https://cloud.google.com/bigtable/docs/creating-cluster +- Set your Google Application Default Credentials. + https://developers.google.com/identity/protocols/application-default-credentials + +Operations performed: +- Create a Cloud Bigtable table. +- List tables for a Cloud Bigtable instance. +- Print metadata of the newly created table. +- Create Column Families with different GC rules. + - GC Rules like: MaxAge, MaxVersions, Union, Intersection and Nested. +- Delete a Bigtable table. +""" + +import argparse +import datetime + +from google.cloud import bigtable +from google.cloud.bigtable import column_family +from ..utils import create_table_cm + + +def run_table_operations(project_id, instance_id, table_id): + """Create a Bigtable table and perform basic operations on it + + :type project_id: str + :param project_id: Project id of the client. + + :type instance_id: str + :param instance_id: Instance of the client. + + :type table_id: str + :param table_id: Table id to create table. + """ + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + with create_table_cm(project_id, instance_id, table_id, verbose=False) as table: + # [START bigtable_list_tables] + tables = instance.list_tables() + print("Listing tables in current project...") + if tables != []: + for tbl in tables: + print(tbl.table_id) + else: + print("No table exists in current project...") + # [END bigtable_list_tables] + + # [START bigtable_create_family_gc_max_age] + print("Creating column family cf1 with with MaxAge GC Rule...") + # Create a column family with GC policy : maximum age + # where age = current time minus cell timestamp + + # Define the GC rule to retain data with max age of 5 days + max_age_rule = column_family.MaxAgeGCRule(datetime.timedelta(days=5)) + + column_family1 = table.column_family("cf1", max_age_rule) + column_family1.create() + print("Created column family cf1 with MaxAge GC Rule.") + # [END bigtable_create_family_gc_max_age] + + # [START bigtable_create_family_gc_max_versions] + print("Creating column family cf2 with max versions GC rule...") + # Create a column family with GC policy : most recent N versions + # where 1 = most recent version + + # Define the GC policy to retain only the most recent 2 versions + max_versions_rule = column_family.MaxVersionsGCRule(2) + + column_family2 = table.column_family("cf2", max_versions_rule) + column_family2.create() + print("Created column family cf2 with Max Versions GC Rule.") + # [END bigtable_create_family_gc_max_versions] + + # [START bigtable_create_family_gc_union] + print("Creating column family cf3 with union GC rule...") + # Create a column family with GC policy to drop data that matches + # at least one condition. + # Define a GC rule to drop cells older than 5 days or not the + # most recent version + union_rule = column_family.GCRuleUnion( + [ + column_family.MaxAgeGCRule(datetime.timedelta(days=5)), + column_family.MaxVersionsGCRule(2), + ] + ) + + column_family3 = table.column_family("cf3", union_rule) + column_family3.create() + print("Created column family cf3 with Union GC rule") + # [END bigtable_create_family_gc_union] + + # [START bigtable_create_family_gc_intersection] + print("Creating column family cf4 with Intersection GC rule...") + # Create a column family with GC policy to drop data that matches + # all conditions + # GC rule: Drop cells older than 5 days AND older than the most + # recent 2 versions + intersection_rule = column_family.GCRuleIntersection( + [ + column_family.MaxAgeGCRule(datetime.timedelta(days=5)), + column_family.MaxVersionsGCRule(2), + ] + ) + + column_family4 = table.column_family("cf4", intersection_rule) + column_family4.create() + print("Created column family cf4 with Intersection GC rule.") + # [END bigtable_create_family_gc_intersection] + + # [START bigtable_create_family_gc_nested] + print("Creating column family cf5 with a Nested GC rule...") + # Create a column family with nested GC policies. + # Create a nested GC rule: + # Drop cells that are either older than the 10 recent versions + # OR + # Drop cells that are older than a month AND older than the + # 2 recent versions + rule1 = column_family.MaxVersionsGCRule(10) + rule2 = column_family.GCRuleIntersection( + [ + column_family.MaxAgeGCRule(datetime.timedelta(days=30)), + column_family.MaxVersionsGCRule(2), + ] + ) + + nested_rule = column_family.GCRuleUnion([rule1, rule2]) + + column_family5 = table.column_family("cf5", nested_rule) + column_family5.create() + print("Created column family cf5 with a Nested GC rule.") + # [END bigtable_create_family_gc_nested] + + # [START bigtable_list_column_families] + print("Printing Column Family and GC Rule for all column families...") + column_families = table.list_column_families() + for column_family_name, gc_rule in sorted(column_families.items()): + print("Column Family:", column_family_name) + print("GC Rule:") + print(gc_rule.to_pb()) + # Sample output: + # Column Family: cf4 + # GC Rule: + # gc_rule { + # intersection { + # rules { + # max_age { + # seconds: 432000 + # } + # } + # rules { + # max_num_versions: 2 + # } + # } + # } + # [END bigtable_list_column_families] + + print("Print column family cf1 GC rule before update...") + print("Column Family: cf1") + print(column_family1.to_pb()) + + # [START bigtable_update_gc_rule] + print("Updating column family cf1 GC rule...") + # Update the column family cf1 to update the GC rule + column_family1 = table.column_family("cf1", column_family.MaxVersionsGCRule(1)) + column_family1.update() + print("Updated column family cf1 GC rule\n") + # [END bigtable_update_gc_rule] + + print("Print column family cf1 GC rule after update...") + print("Column Family: cf1") + print(column_family1.to_pb()) + + # [START bigtable_delete_family] + print("Delete a column family cf2...") + # Delete a column family + column_family2.delete() + print("Column family cf2 deleted successfully.") + # [END bigtable_delete_family] + + print( + 'execute command "python tableadmin.py delete [project_id] \ + [instance_id] --table [tableName]" to delete the table.' + ) + + +def delete_table(project_id, instance_id, table_id): + """Delete bigtable. + + :type project_id: str + :param project_id: Project id of the client. + + :type instance_id: str + :param instance_id: Instance of the client. + + :type table_id: str + :param table_id: Table id to create table. + """ + + client = bigtable.Client(project=project_id, admin=True) + instance = client.instance(instance_id) + table = instance.table(table_id) + + # [START bigtable_delete_table] + # Delete the entire table + + print("Checking if table {} exists...".format(table_id)) + if table.exists(): + print("Table {} exists.".format(table_id)) + print("Deleting {} table.".format(table_id)) + table.delete() + print("Deleted {} table.".format(table_id)) + else: + print("Table {} does not exists.".format(table_id)) + # [END bigtable_delete_table] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "command", + help="run or delete. \ + Operation to perform on table.", + ) + parser.add_argument( + "--table", help="Cloud Bigtable Table name.", default="Hello-Bigtable" + ) + + parser.add_argument("project_id", help="Your Cloud Platform project ID.") + parser.add_argument( + "instance_id", help="ID of the Cloud Bigtable instance to connect to." + ) + + args = parser.parse_args() + + if args.command.lower() == "run": + run_table_operations(args.project_id, args.instance_id, args.table) + elif args.command.lower() == "delete": + delete_table(args.project_id, args.instance_id, args.table) + else: + print( + "Command should be either run or delete.\n Use argument -h,\ + --help to show help and exit." + ) diff --git a/packages/google-cloud-bigtable/samples/tableadmin/tableadmin_test.py b/packages/google-cloud-bigtable/samples/tableadmin/tableadmin_test.py new file mode 100755 index 000000000000..0ffdc75c9066 --- /dev/null +++ b/packages/google-cloud-bigtable/samples/tableadmin/tableadmin_test.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +# Copyright 2018, Google LLC +# 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 os +from test_utils.retry import RetryErrors +from google.api_core import exceptions +import uuid + +from .tableadmin import delete_table +from .tableadmin import run_table_operations +from ..utils import create_table_cm + +PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] +BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] +TABLE_ID = f"tableadmin-test-{str(uuid.uuid4())[:16]}" + +retry_429_503 = RetryErrors(exceptions.TooManyRequests, exceptions.ServiceUnavailable) + + +def test_run_table_operations(capsys): + retry_429_503(run_table_operations)(PROJECT, BIGTABLE_INSTANCE, TABLE_ID) + out, _ = capsys.readouterr() + + assert "Listing tables in current project." in out + assert "Creating column family cf1 with with MaxAge GC Rule" in out + assert "Created column family cf1 with MaxAge GC Rule." in out + assert "Created column family cf2 with Max Versions GC Rule." in out + assert "Created column family cf3 with Union GC rule" in out + assert "Created column family cf4 with Intersection GC rule." in out + assert "Created column family cf5 with a Nested GC rule." in out + assert "Printing Column Family and GC Rule for all column families." in out + assert "Updating column family cf1 GC rule..." in out + assert "Updated column family cf1 GC rule" in out + assert "Print column family cf1 GC rule after update..." in out + assert "Column Family: cf1" in out + assert "max_num_versions: 1" in out + assert "Delete a column family cf2..." in out + assert "Column family cf2 deleted successfully." in out + + +def test_delete_table(capsys): + table_id = f"table-admin-to-delete-{str(uuid.uuid4())[:16]}" + with create_table_cm(PROJECT, BIGTABLE_INSTANCE, table_id, verbose=False): + delete_table(PROJECT, BIGTABLE_INSTANCE, table_id) + out, _ = capsys.readouterr() + + assert "Table " + table_id + " exists." in out + assert "Deleting " + table_id + " table." in out + assert "Deleted " + table_id + " table." in out diff --git a/packages/google-cloud-bigtable/samples/testdata/README.md b/packages/google-cloud-bigtable/samples/testdata/README.md new file mode 100644 index 000000000000..57520179f2dc --- /dev/null +++ b/packages/google-cloud-bigtable/samples/testdata/README.md @@ -0,0 +1,5 @@ +#### To generate singer_pb2.py and descriptors.pb file from singer.proto using `protoc` +```shell +cd samples +protoc --proto_path=testdata/ --include_imports --descriptor_set_out=testdata/descriptors.pb --python_out=testdata/ testdata/singer.proto +``` \ No newline at end of file diff --git a/packages/google-cloud-bigtable/samples/testdata/descriptors.pb b/packages/google-cloud-bigtable/samples/testdata/descriptors.pb new file mode 100644 index 0000000000000000000000000000000000000000..bddf04de378263f791d1d7f558e97f934b281d2b GIT binary patch literal 182 zcmd5Zl#{BLTUwl%tQ5q>77SJ> zB*ev%mzbL>!KlEf!5IW*3z=}Srl;l=rAjaX1^JBR^l%uX=MGX81W~M|$HfZf3$b%C o2lxjFFbHvQv3NN~MF}v1SZ@A4-U3V@R*=85w*Yez8`zD;07g_Xr2qf` literal 0 HcmV?d00001 diff --git a/packages/google-cloud-bigtable/samples/testdata/singer.proto b/packages/google-cloud-bigtable/samples/testdata/singer.proto new file mode 100644 index 000000000000..d60e0dfb3b2a --- /dev/null +++ b/packages/google-cloud-bigtable/samples/testdata/singer.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package examples.bigtable.music; + +enum Genre { + POP = 0; + JAZZ = 1; + FOLK = 2; + ROCK = 3; +} + +message Singer { + string name = 1; + Genre genre = 2; +} diff --git a/packages/google-cloud-bigtable/samples/testdata/singer_pb2.py b/packages/google-cloud-bigtable/samples/testdata/singer_pb2.py new file mode 100644 index 000000000000..d2a328df0e9a --- /dev/null +++ b/packages/google-cloud-bigtable/samples/testdata/singer_pb2.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: singer.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0csinger.proto\x12\x17\x65xamples.bigtable.music\"E\n\x06Singer\x12\x0c\n\x04name\x18\x01 \x01(\t\x12-\n\x05genre\x18\x02 \x01(\x0e\x32\x1e.examples.bigtable.music.Genre*.\n\x05Genre\x12\x07\n\x03POP\x10\x00\x12\x08\n\x04JAZZ\x10\x01\x12\x08\n\x04\x46OLK\x10\x02\x12\x08\n\x04ROCK\x10\x03\x62\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'singer_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _GENRE._serialized_start=112 + _GENRE._serialized_end=158 + _SINGER._serialized_start=41 + _SINGER._serialized_end=110 +# @@protoc_insertion_point(module_scope) diff --git a/packages/google-cloud-bigtable/samples/utils.py b/packages/google-cloud-bigtable/samples/utils.py new file mode 100644 index 000000000000..f796aaedb79c --- /dev/null +++ b/packages/google-cloud-bigtable/samples/utils.py @@ -0,0 +1,99 @@ +# Copyright 2024, Google LLC +# 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. +""" +Provides helper logic used across samples +""" + + +from google.cloud import bigtable +from google.cloud.bigtable.column_family import ColumnFamily +from google.cloud.bigtable_admin_v2.types import ColumnFamily as ColumnFamily_pb +from google.api_core import exceptions +from google.api_core.retry import Retry +from google.api_core.retry import if_exception_type + +delete_retry = Retry(if_exception_type(exceptions.TooManyRequests, exceptions.ServiceUnavailable)) + +class create_table_cm: + """ + Create a new table using a context manager, to ensure that table.delete() is called to clean up + the table, even if an exception is thrown + """ + def __init__(self, *args, verbose=True, **kwargs): + self._args = args + self._kwargs = kwargs + self._verbose = verbose + + def __enter__(self): + self._table = create_table(*self._args, **self._kwargs) + if self._verbose: + print(f"created table: {self._table.table_id}") + return self._table + + def __exit__(self, *args): + if self._table.exists(): + if self._verbose: + print(f"deleting table: {self._table.table_id}") + delete_retry(self._table.delete()) + else: + if self._verbose: + print(f"table {self._table.table_id} not found") + + +def create_table(project, instance_id, table_id, column_families={}): + """ + Creates a new table, and blocks until it reaches a ready state + """ + client = bigtable.Client(project=project, admin=True) + instance = client.instance(instance_id) + + table = instance.table(table_id) + if table.exists(): + table.delete() + + # convert column families to pb if needed + pb_families = { + id: ColumnFamily(id, table, rule).to_pb() if not isinstance(rule, ColumnFamily_pb) else rule + for (id, rule) in column_families.items() + } + + # create table using gapic layer + instance._client.table_admin_client.create_table( + request={ + "parent": instance.name, + "table_id": table_id, + "table": {"column_families": pb_families}, + } + ) + + wait_for_table(table) + + return table + +@Retry( + on_error=if_exception_type( + exceptions.PreconditionFailed, + exceptions.FailedPrecondition, + exceptions.NotFound, + ), + timeout=120, +) +def wait_for_table(table): + """ + raises an exception if the table does not exist or is not ready to use + + Because this method is wrapped with an api_core.Retry decorator, it will + retry with backoff if the table is not ready + """ + if not table.exists(): + raise exceptions.NotFound \ No newline at end of file From 9dbb07b489f34bf9f2b802e0090578c5d04fe487 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 22 May 2026 13:23:19 -0700 Subject: [PATCH 3/5] reverted google-auth changes --- packages/google-auth/google/oauth2/id_token.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/google-auth/google/oauth2/id_token.py b/packages/google-auth/google/oauth2/id_token.py index 3587a76a28b9..d21be1a06c84 100644 --- a/packages/google-auth/google/oauth2/id_token.py +++ b/packages/google-auth/google/oauth2/id_token.py @@ -59,7 +59,7 @@ import http.client as http_client import json import os -from typing import Union +from typing import Any, Mapping, Union from google.auth import environment_vars from google.auth import exceptions @@ -113,7 +113,7 @@ def verify_token( audience: Union[str, list[str], None] = None, certs_url: str = _GOOGLE_OAUTH2_CERTS_URL, clock_skew_in_seconds: int = 0, -) -> dict: # Note: type annotation simplified to prevent issues in google3 +) -> Mapping[str, Any]: """Verifies an ID token and returns the decoded token. Args: @@ -130,7 +130,7 @@ def verify_token( validation. Returns: - dict[str, Any]: The decoded token. + Mapping[str, Any]: The decoded token. """ certs = _fetch_certs(request, certs_url) @@ -172,7 +172,7 @@ def verify_oauth2_token(id_token, request, audience=None, clock_skew_in_seconds= validation. Returns: - dict[str, Any]: The decoded token. + Mapping[str, Any]: The decoded token. Raises: exceptions.GoogleAuthError: If the issuer is invalid. @@ -210,7 +210,7 @@ def verify_firebase_token(id_token, request, audience=None, clock_skew_in_second validation. Returns: - dict[str, Any]: The decoded token. + Mapping[str, Any]: The decoded token. """ return verify_token( id_token, From adba325c4d5d448284ea39fbc0044b214ed1a8ca Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 22 May 2026 21:52:43 -0700 Subject: [PATCH 4/5] ran format --- .../samples/beam/hello_world_write.py | 16 ++-- .../samples/beam/hello_world_write_test.py | 2 +- .../samples/beam/noxfile.py | 18 ++-- .../samples/hello/async_main.py | 4 +- .../samples/hello/async_main_test.py | 2 +- .../samples/hello/main.py | 11 ++- .../samples/hello/noxfile.py | 18 ++-- .../samples/hello_happybase/main.py | 9 +- .../samples/hello_happybase/main_test.py | 3 +- .../samples/hello_happybase/noxfile.py | 18 ++-- .../samples/instanceadmin/noxfile.py | 18 ++-- .../instanceadmin/test_instanceadmin.py | 7 +- .../samples/metricscaler/metricscaler.py | 13 ++- .../samples/metricscaler/metricscaler_test.py | 16 +--- .../samples/metricscaler/noxfile.py | 18 ++-- .../samples/quickstart/main_async_test.py | 5 +- .../samples/quickstart/main_test.py | 9 +- .../samples/quickstart/noxfile.py | 18 ++-- .../samples/quickstart_happybase/main.py | 3 +- .../samples/quickstart_happybase/main_test.py | 7 +- .../samples/quickstart_happybase/noxfile.py | 18 ++-- .../data_client/data_client_snippets_async.py | 47 +++++----- .../data_client_snippets_async_test.py | 8 +- .../samples/snippets/data_client/noxfile.py | 18 ++-- .../snippets/deletes/deletes_async_test.py | 13 ++- .../snippets/deletes/deletes_snippets.py | 5 + .../deletes/deletes_snippets_async.py | 31 ++++--- .../samples/snippets/deletes/deletes_test.py | 10 +- .../samples/snippets/deletes/noxfile.py | 18 ++-- .../snippets/filters/filter_snippets.py | 4 +- .../filters/filter_snippets_async_test.py | 17 ++-- .../samples/snippets/filters/filters_test.py | 7 +- .../samples/snippets/filters/noxfile.py | 18 ++-- .../filters/snapshots/snap_filters_test.py | 91 ++++++++++++------- .../samples/snippets/reads/noxfile.py | 18 ++-- .../samples/snippets/reads/read_snippets.py | 2 + .../samples/snippets/reads/reads_test.py | 11 ++- .../reads/snapshots/snap_reads_test.py | 28 +++--- .../samples/snippets/writes/noxfile.py | 18 ++-- .../samples/snippets/writes/writes_test.py | 7 +- .../samples/tableadmin/noxfile.py | 18 ++-- .../samples/tableadmin/tableadmin.py | 1 + .../samples/tableadmin/tableadmin_test.py | 8 +- .../samples/testdata/singer_pb2.py | 23 ++--- .../google-cloud-bigtable/samples/utils.py | 20 ++-- 45 files changed, 371 insertions(+), 303 deletions(-) diff --git a/packages/google-cloud-bigtable/samples/beam/hello_world_write.py b/packages/google-cloud-bigtable/samples/beam/hello_world_write.py index 89f541d0d190..06c9505f2f29 100644 --- a/packages/google-cloud-bigtable/samples/beam/hello_world_write.py +++ b/packages/google-cloud-bigtable/samples/beam/hello_world_write.py @@ -16,6 +16,7 @@ import apache_beam as beam from apache_beam.io.gcp.bigtableio import WriteToBigTable from apache_beam.options.pipeline_options import PipelineOptions + from google.cloud.bigtable import row @@ -53,12 +54,15 @@ def run(argv=None): """Build and run the pipeline.""" options = BigtableOptions(argv) with beam.Pipeline(options=options) as p: - p | beam.Create( - ["phone#4c410523#20190501", "phone#4c410523#20190502"] - ) | beam.ParDo(CreateRowFn()) | WriteToBigTable( - project_id=options.bigtable_project, - instance_id=options.bigtable_instance, - table_id=options.bigtable_table, + ( + p + | beam.Create(["phone#4c410523#20190501", "phone#4c410523#20190502"]) + | beam.ParDo(CreateRowFn()) + | WriteToBigTable( + project_id=options.bigtable_project, + instance_id=options.bigtable_instance, + table_id=options.bigtable_table, + ) ) diff --git a/packages/google-cloud-bigtable/samples/beam/hello_world_write_test.py b/packages/google-cloud-bigtable/samples/beam/hello_world_write_test.py index ba0e980964bb..82490ec7855e 100644 --- a/packages/google-cloud-bigtable/samples/beam/hello_world_write_test.py +++ b/packages/google-cloud-bigtable/samples/beam/hello_world_write_test.py @@ -16,8 +16,8 @@ import pytest -from . import hello_world_write from ..utils import create_table_cm +from . import hello_world_write PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] diff --git a/packages/google-cloud-bigtable/samples/beam/noxfile.py b/packages/google-cloud-bigtable/samples/beam/noxfile.py index d0b343a9167c..1b8f66b398c9 100644 --- a/packages/google-cloud-bigtable/samples/beam/noxfile.py +++ b/packages/google-cloud-bigtable/samples/beam/noxfile.py @@ -16,13 +16,12 @@ import glob import os -from pathlib import Path import sys +from pathlib import Path from typing import Callable, Dict, Optional import nox - # WARNING - WARNING - WARNING - WARNING - WARNING # WARNING - WARNING - WARNING - WARNING - WARNING # DO NOT EDIT THIS FILE EVER! @@ -158,6 +157,7 @@ def blacken(session: nox.sessions.Session) -> None: # format = isort + black # + @nox.session def format(session: nox.sessions.Session) -> None: """ @@ -185,7 +185,9 @@ def _session_tests( session: nox.sessions.Session, post_install: Callable = None ) -> None: # check for presence of tests - test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) test_list.extend(glob.glob("**/tests", recursive=True)) if len(test_list) == 0: @@ -207,9 +209,7 @@ def _session_tests( if os.path.exists("requirements-test.txt"): if os.path.exists("constraints-test.txt"): - session.install( - "-r", "requirements-test.txt", "-c", "constraints-test.txt" - ) + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") else: session.install("-r", "requirements-test.txt") with open("requirements-test.txt") as rtfile: @@ -222,9 +222,9 @@ def _session_tests( post_install(session) if "pytest-parallel" in packages: - concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) elif "pytest-xdist" in packages: - concurrent_args.extend(['-n', 'auto']) + concurrent_args.extend(["-n", "auto"]) session.run( "pytest", @@ -254,7 +254,7 @@ def py(session: nox.sessions.Session) -> None: def _get_repo_root() -> Optional[str]: - """ Returns the root folder of the project. """ + """Returns the root folder of the project.""" # Get root of this repository. Assume we don't have directories nested deeper than 10 items. p = Path(os.getcwd()) for i in range(10): diff --git a/packages/google-cloud-bigtable/samples/hello/async_main.py b/packages/google-cloud-bigtable/samples/hello/async_main.py index e134e28d0cc3..c26a74faeead 100644 --- a/packages/google-cloud-bigtable/samples/hello/async_main.py +++ b/packages/google-cloud-bigtable/samples/hello/async_main.py @@ -26,11 +26,13 @@ import argparse import asyncio -from ..utils import wait_for_table # [START bigtable_async_hw_imports] from google.cloud import bigtable from google.cloud.bigtable.data import row_filters + +from ..utils import wait_for_table + # [END bigtable_async_hw_imports] # use to ignore warnings diff --git a/packages/google-cloud-bigtable/samples/hello/async_main_test.py b/packages/google-cloud-bigtable/samples/hello/async_main_test.py index aa65a86523f4..4f09d01e5630 100644 --- a/packages/google-cloud-bigtable/samples/hello/async_main_test.py +++ b/packages/google-cloud-bigtable/samples/hello/async_main_test.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os import asyncio +import os import uuid from .async_main import main diff --git a/packages/google-cloud-bigtable/samples/hello/main.py b/packages/google-cloud-bigtable/samples/hello/main.py index 2c0d83f98fb8..13899a87425b 100644 --- a/packages/google-cloud-bigtable/samples/hello/main.py +++ b/packages/google-cloud-bigtable/samples/hello/main.py @@ -25,14 +25,14 @@ """ import argparse -from ..utils import wait_for_table # [START bigtable_hw_imports] from datetime import datetime, timezone from google.cloud import bigtable -from google.cloud.bigtable import column_family -from google.cloud.bigtable import row_filters +from google.cloud.bigtable import column_family, row_filters + +from ..utils import wait_for_table # [END bigtable_hw_imports] @@ -91,7 +91,10 @@ def main(project_id, instance_id, table_id): row_key = f"greeting{i}".encode() row = table.direct_row(row_key) row.set_cell( - column_family_id, column, value, timestamp=datetime.now(timezone.utc), + column_family_id, + column, + value, + timestamp=datetime.now(timezone.utc), ) rows.append(row) table.mutate_rows(rows) diff --git a/packages/google-cloud-bigtable/samples/hello/noxfile.py b/packages/google-cloud-bigtable/samples/hello/noxfile.py index a169b5b5b464..c0e60097b353 100644 --- a/packages/google-cloud-bigtable/samples/hello/noxfile.py +++ b/packages/google-cloud-bigtable/samples/hello/noxfile.py @@ -16,13 +16,12 @@ import glob import os -from pathlib import Path import sys +from pathlib import Path from typing import Callable, Dict, Optional import nox - # WARNING - WARNING - WARNING - WARNING - WARNING # WARNING - WARNING - WARNING - WARNING - WARNING # DO NOT EDIT THIS FILE EVER! @@ -160,6 +159,7 @@ def blacken(session: nox.sessions.Session) -> None: # format = isort + black # + @nox.session def format(session: nox.sessions.Session) -> None: """ @@ -187,7 +187,9 @@ def _session_tests( session: nox.sessions.Session, post_install: Callable = None ) -> None: # check for presence of tests - test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) test_list.extend(glob.glob("**/tests", recursive=True)) if len(test_list) == 0: @@ -209,9 +211,7 @@ def _session_tests( if os.path.exists("requirements-test.txt"): if os.path.exists("constraints-test.txt"): - session.install( - "-r", "requirements-test.txt", "-c", "constraints-test.txt" - ) + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") else: session.install("-r", "requirements-test.txt") with open("requirements-test.txt") as rtfile: @@ -224,9 +224,9 @@ def _session_tests( post_install(session) if "pytest-parallel" in packages: - concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) elif "pytest-xdist" in packages: - concurrent_args.extend(['-n', 'auto']) + concurrent_args.extend(["-n", "auto"]) session.run( "pytest", @@ -256,7 +256,7 @@ def py(session: nox.sessions.Session) -> None: def _get_repo_root() -> Optional[str]: - """ Returns the root folder of the project. """ + """Returns the root folder of the project.""" # Get root of this repository. Assume we don't have directories nested deeper than 10 items. p = Path(os.getcwd()) for i in range(10): diff --git a/packages/google-cloud-bigtable/samples/hello_happybase/main.py b/packages/google-cloud-bigtable/samples/hello_happybase/main.py index 50820febde8b..54099a1fa630 100644 --- a/packages/google-cloud-bigtable/samples/hello_happybase/main.py +++ b/packages/google-cloud-bigtable/samples/hello_happybase/main.py @@ -25,11 +25,11 @@ """ import argparse -from ..utils import wait_for_table # [START bigtable_hw_imports_happybase] -from google.cloud import bigtable -from google.cloud import happybase +from google.cloud import bigtable, happybase + +from ..utils import wait_for_table # [END bigtable_hw_imports_happybase] @@ -48,7 +48,8 @@ def main(project_id, instance_id, table_name): print("Creating the {} table.".format(table_name)) column_family_name = "cf1" connection.create_table( - table_name, {column_family_name: dict()} # Use default options. + table_name, + {column_family_name: dict()}, # Use default options. ) # [END bigtable_hw_create_table_happybase] diff --git a/packages/google-cloud-bigtable/samples/hello_happybase/main_test.py b/packages/google-cloud-bigtable/samples/hello_happybase/main_test.py index 252f4ccaf9e7..b7c5ceea8ad9 100644 --- a/packages/google-cloud-bigtable/samples/hello_happybase/main_test.py +++ b/packages/google-cloud-bigtable/samples/hello_happybase/main_test.py @@ -15,9 +15,10 @@ import os import uuid -from .main import main from google.cloud import bigtable +from .main import main + PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] TABLE_ID = f"hello-world-hb-test-{str(uuid.uuid4())[:16]}" diff --git a/packages/google-cloud-bigtable/samples/hello_happybase/noxfile.py b/packages/google-cloud-bigtable/samples/hello_happybase/noxfile.py index a169b5b5b464..c0e60097b353 100644 --- a/packages/google-cloud-bigtable/samples/hello_happybase/noxfile.py +++ b/packages/google-cloud-bigtable/samples/hello_happybase/noxfile.py @@ -16,13 +16,12 @@ import glob import os -from pathlib import Path import sys +from pathlib import Path from typing import Callable, Dict, Optional import nox - # WARNING - WARNING - WARNING - WARNING - WARNING # WARNING - WARNING - WARNING - WARNING - WARNING # DO NOT EDIT THIS FILE EVER! @@ -160,6 +159,7 @@ def blacken(session: nox.sessions.Session) -> None: # format = isort + black # + @nox.session def format(session: nox.sessions.Session) -> None: """ @@ -187,7 +187,9 @@ def _session_tests( session: nox.sessions.Session, post_install: Callable = None ) -> None: # check for presence of tests - test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) test_list.extend(glob.glob("**/tests", recursive=True)) if len(test_list) == 0: @@ -209,9 +211,7 @@ def _session_tests( if os.path.exists("requirements-test.txt"): if os.path.exists("constraints-test.txt"): - session.install( - "-r", "requirements-test.txt", "-c", "constraints-test.txt" - ) + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") else: session.install("-r", "requirements-test.txt") with open("requirements-test.txt") as rtfile: @@ -224,9 +224,9 @@ def _session_tests( post_install(session) if "pytest-parallel" in packages: - concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) elif "pytest-xdist" in packages: - concurrent_args.extend(['-n', 'auto']) + concurrent_args.extend(["-n", "auto"]) session.run( "pytest", @@ -256,7 +256,7 @@ def py(session: nox.sessions.Session) -> None: def _get_repo_root() -> Optional[str]: - """ Returns the root folder of the project. """ + """Returns the root folder of the project.""" # Get root of this repository. Assume we don't have directories nested deeper than 10 items. p = Path(os.getcwd()) for i in range(10): diff --git a/packages/google-cloud-bigtable/samples/instanceadmin/noxfile.py b/packages/google-cloud-bigtable/samples/instanceadmin/noxfile.py index a169b5b5b464..c0e60097b353 100644 --- a/packages/google-cloud-bigtable/samples/instanceadmin/noxfile.py +++ b/packages/google-cloud-bigtable/samples/instanceadmin/noxfile.py @@ -16,13 +16,12 @@ import glob import os -from pathlib import Path import sys +from pathlib import Path from typing import Callable, Dict, Optional import nox - # WARNING - WARNING - WARNING - WARNING - WARNING # WARNING - WARNING - WARNING - WARNING - WARNING # DO NOT EDIT THIS FILE EVER! @@ -160,6 +159,7 @@ def blacken(session: nox.sessions.Session) -> None: # format = isort + black # + @nox.session def format(session: nox.sessions.Session) -> None: """ @@ -187,7 +187,9 @@ def _session_tests( session: nox.sessions.Session, post_install: Callable = None ) -> None: # check for presence of tests - test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) test_list.extend(glob.glob("**/tests", recursive=True)) if len(test_list) == 0: @@ -209,9 +211,7 @@ def _session_tests( if os.path.exists("requirements-test.txt"): if os.path.exists("constraints-test.txt"): - session.install( - "-r", "requirements-test.txt", "-c", "constraints-test.txt" - ) + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") else: session.install("-r", "requirements-test.txt") with open("requirements-test.txt") as rtfile: @@ -224,9 +224,9 @@ def _session_tests( post_install(session) if "pytest-parallel" in packages: - concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) elif "pytest-xdist" in packages: - concurrent_args.extend(['-n', 'auto']) + concurrent_args.extend(["-n", "auto"]) session.run( "pytest", @@ -256,7 +256,7 @@ def py(session: nox.sessions.Session) -> None: def _get_repo_root() -> Optional[str]: - """ Returns the root folder of the project. """ + """Returns the root folder of the project.""" # Get root of this repository. Assume we don't have directories nested deeper than 10 items. p = Path(os.getcwd()) for i in range(10): diff --git a/packages/google-cloud-bigtable/samples/instanceadmin/test_instanceadmin.py b/packages/google-cloud-bigtable/samples/instanceadmin/test_instanceadmin.py index b0041294bf1c..5d1378fcd946 100644 --- a/packages/google-cloud-bigtable/samples/instanceadmin/test_instanceadmin.py +++ b/packages/google-cloud-bigtable/samples/instanceadmin/test_instanceadmin.py @@ -18,12 +18,11 @@ import warnings import backoff -from google.api_core import exceptions -from google.cloud import bigtable -import pytest - import instanceadmin +import pytest +from google.api_core import exceptions +from google.cloud import bigtable PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] INSTANCE_ID_FORMAT = "instanceadmin-{:03}-{}" diff --git a/packages/google-cloud-bigtable/samples/metricscaler/metricscaler.py b/packages/google-cloud-bigtable/samples/metricscaler/metricscaler.py index f1fe80523dd8..1f89e6aacc15 100644 --- a/packages/google-cloud-bigtable/samples/metricscaler/metricscaler.py +++ b/packages/google-cloud-bigtable/samples/metricscaler/metricscaler.py @@ -20,11 +20,11 @@ import os import time -from google.cloud import bigtable -from google.cloud import monitoring_v3 -from google.cloud.bigtable import enums from google.cloud.monitoring_v3 import query +from google.cloud import bigtable, monitoring_v3 +from google.cloud.bigtable import enums + PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] logger = logging.getLogger("bigtable.metricscaler") @@ -43,7 +43,7 @@ def get_cpu_load(bigtable_instance, bigtable_cluster): cpu_query = query.Query( client, project=PROJECT, - metric_type="bigtable.googleapis.com/" "cluster/cpu_load", + metric_type="bigtable.googleapis.com/cluster/cpu_load", minutes=5, ) cpu_query = cpu_query.select_resources( @@ -65,7 +65,7 @@ def get_storage_utilization(bigtable_instance, bigtable_cluster): utilization_query = query.Query( client, project=PROJECT, - metric_type="bigtable.googleapis.com/" "cluster/storage_utilization", + metric_type="bigtable.googleapis.com/cluster/storage_utilization", minutes=5, ) utilization_query = utilization_query.select_resources( @@ -205,8 +205,7 @@ def main( ) parser.add_argument( "--high_storage_threshold", - help="If Cloud Bigtable storage utilization is above this threshold, " - "scale up", + help="If Cloud Bigtable storage utilization is above this threshold, scale up", default=0.6, ) parser.add_argument( diff --git a/packages/google-cloud-bigtable/samples/metricscaler/metricscaler_test.py b/packages/google-cloud-bigtable/samples/metricscaler/metricscaler_test.py index 47be38187f30..f769ce05e11f 100644 --- a/packages/google-cloud-bigtable/samples/metricscaler/metricscaler_test.py +++ b/packages/google-cloud-bigtable/samples/metricscaler/metricscaler_test.py @@ -17,19 +17,13 @@ import os import uuid -from google.cloud import bigtable -from google.cloud.bigtable import enums -from mock import Mock, patch - import pytest -from test_utils.retry import RetryInstanceState -from test_utils.retry import RetryResult - -from metricscaler import get_cpu_load -from metricscaler import get_storage_utilization -from metricscaler import main -from metricscaler import scale_bigtable +from metricscaler import get_cpu_load, get_storage_utilization, main, scale_bigtable +from mock import Mock, patch +from test_utils.retry import RetryInstanceState, RetryResult +from google.cloud import bigtable +from google.cloud.bigtable import enums PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] BIGTABLE_ZONE = os.environ["BIGTABLE_ZONE"] diff --git a/packages/google-cloud-bigtable/samples/metricscaler/noxfile.py b/packages/google-cloud-bigtable/samples/metricscaler/noxfile.py index a169b5b5b464..c0e60097b353 100644 --- a/packages/google-cloud-bigtable/samples/metricscaler/noxfile.py +++ b/packages/google-cloud-bigtable/samples/metricscaler/noxfile.py @@ -16,13 +16,12 @@ import glob import os -from pathlib import Path import sys +from pathlib import Path from typing import Callable, Dict, Optional import nox - # WARNING - WARNING - WARNING - WARNING - WARNING # WARNING - WARNING - WARNING - WARNING - WARNING # DO NOT EDIT THIS FILE EVER! @@ -160,6 +159,7 @@ def blacken(session: nox.sessions.Session) -> None: # format = isort + black # + @nox.session def format(session: nox.sessions.Session) -> None: """ @@ -187,7 +187,9 @@ def _session_tests( session: nox.sessions.Session, post_install: Callable = None ) -> None: # check for presence of tests - test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) test_list.extend(glob.glob("**/tests", recursive=True)) if len(test_list) == 0: @@ -209,9 +211,7 @@ def _session_tests( if os.path.exists("requirements-test.txt"): if os.path.exists("constraints-test.txt"): - session.install( - "-r", "requirements-test.txt", "-c", "constraints-test.txt" - ) + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") else: session.install("-r", "requirements-test.txt") with open("requirements-test.txt") as rtfile: @@ -224,9 +224,9 @@ def _session_tests( post_install(session) if "pytest-parallel" in packages: - concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) elif "pytest-xdist" in packages: - concurrent_args.extend(['-n', 'auto']) + concurrent_args.extend(["-n", "auto"]) session.run( "pytest", @@ -256,7 +256,7 @@ def py(session: nox.sessions.Session) -> None: def _get_repo_root() -> Optional[str]: - """ Returns the root folder of the project. """ + """Returns the root folder of the project.""" # Get root of this repository. Assume we don't have directories nested deeper than 10 items. p = Path(os.getcwd()) for i in range(10): diff --git a/packages/google-cloud-bigtable/samples/quickstart/main_async_test.py b/packages/google-cloud-bigtable/samples/quickstart/main_async_test.py index 0749cbd316a3..a67c0d095ba0 100644 --- a/packages/google-cloud-bigtable/samples/quickstart/main_async_test.py +++ b/packages/google-cloud-bigtable/samples/quickstart/main_async_test.py @@ -16,12 +16,13 @@ import uuid from typing import AsyncGenerator -from google.cloud.bigtable.data import BigtableDataClientAsync, SetCell import pytest import pytest_asyncio -from .main_async import main +from google.cloud.bigtable.data import BigtableDataClientAsync, SetCell + from ..utils import create_table_cm +from .main_async import main PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] diff --git a/packages/google-cloud-bigtable/samples/quickstart/main_test.py b/packages/google-cloud-bigtable/samples/quickstart/main_test.py index f58161f231b1..88419abd7ec4 100644 --- a/packages/google-cloud-bigtable/samples/quickstart/main_test.py +++ b/packages/google-cloud-bigtable/samples/quickstart/main_test.py @@ -14,12 +14,11 @@ import os import uuid -import pytest -from .main import main +import pytest from ..utils import create_table_cm - +from .main import main PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] @@ -30,7 +29,9 @@ def table(): column_family_id = "cf1" column_families = {column_family_id: None} - with create_table_cm(PROJECT, BIGTABLE_INSTANCE, TABLE_ID, column_families) as table: + with create_table_cm( + PROJECT, BIGTABLE_INSTANCE, TABLE_ID, column_families + ) as table: row = table.direct_row("r1") row.set_cell(column_family_id, "c1", "test-value") row.commit() diff --git a/packages/google-cloud-bigtable/samples/quickstart/noxfile.py b/packages/google-cloud-bigtable/samples/quickstart/noxfile.py index a169b5b5b464..c0e60097b353 100644 --- a/packages/google-cloud-bigtable/samples/quickstart/noxfile.py +++ b/packages/google-cloud-bigtable/samples/quickstart/noxfile.py @@ -16,13 +16,12 @@ import glob import os -from pathlib import Path import sys +from pathlib import Path from typing import Callable, Dict, Optional import nox - # WARNING - WARNING - WARNING - WARNING - WARNING # WARNING - WARNING - WARNING - WARNING - WARNING # DO NOT EDIT THIS FILE EVER! @@ -160,6 +159,7 @@ def blacken(session: nox.sessions.Session) -> None: # format = isort + black # + @nox.session def format(session: nox.sessions.Session) -> None: """ @@ -187,7 +187,9 @@ def _session_tests( session: nox.sessions.Session, post_install: Callable = None ) -> None: # check for presence of tests - test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) test_list.extend(glob.glob("**/tests", recursive=True)) if len(test_list) == 0: @@ -209,9 +211,7 @@ def _session_tests( if os.path.exists("requirements-test.txt"): if os.path.exists("constraints-test.txt"): - session.install( - "-r", "requirements-test.txt", "-c", "constraints-test.txt" - ) + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") else: session.install("-r", "requirements-test.txt") with open("requirements-test.txt") as rtfile: @@ -224,9 +224,9 @@ def _session_tests( post_install(session) if "pytest-parallel" in packages: - concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) elif "pytest-xdist" in packages: - concurrent_args.extend(['-n', 'auto']) + concurrent_args.extend(["-n", "auto"]) session.run( "pytest", @@ -256,7 +256,7 @@ def py(session: nox.sessions.Session) -> None: def _get_repo_root() -> Optional[str]: - """ Returns the root folder of the project. """ + """Returns the root folder of the project.""" # Get root of this repository. Assume we don't have directories nested deeper than 10 items. p = Path(os.getcwd()) for i in range(10): diff --git a/packages/google-cloud-bigtable/samples/quickstart_happybase/main.py b/packages/google-cloud-bigtable/samples/quickstart_happybase/main.py index 6a05c4cbd46b..6e474d141201 100644 --- a/packages/google-cloud-bigtable/samples/quickstart_happybase/main.py +++ b/packages/google-cloud-bigtable/samples/quickstart_happybase/main.py @@ -16,8 +16,7 @@ # [START bigtable_quickstart_happybase] import argparse -from google.cloud import bigtable -from google.cloud import happybase +from google.cloud import bigtable, happybase def main(project_id="project-id", instance_id="instance-id", table_id="my-table"): diff --git a/packages/google-cloud-bigtable/samples/quickstart_happybase/main_test.py b/packages/google-cloud-bigtable/samples/quickstart_happybase/main_test.py index 343ec800a96d..0f0d1ecf5f5f 100644 --- a/packages/google-cloud-bigtable/samples/quickstart_happybase/main_test.py +++ b/packages/google-cloud-bigtable/samples/quickstart_happybase/main_test.py @@ -14,10 +14,11 @@ import os import uuid + import pytest -from .main import main from ..utils import create_table_cm +from .main import main PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] @@ -28,7 +29,9 @@ def table(): column_family_id = "cf1" column_families = {column_family_id: None} - with create_table_cm(PROJECT, BIGTABLE_INSTANCE, TABLE_ID, column_families) as table: + with create_table_cm( + PROJECT, BIGTABLE_INSTANCE, TABLE_ID, column_families + ) as table: row = table.direct_row("r1") row.set_cell(column_family_id, "c1", "test-value") row.commit() diff --git a/packages/google-cloud-bigtable/samples/quickstart_happybase/noxfile.py b/packages/google-cloud-bigtable/samples/quickstart_happybase/noxfile.py index a169b5b5b464..c0e60097b353 100644 --- a/packages/google-cloud-bigtable/samples/quickstart_happybase/noxfile.py +++ b/packages/google-cloud-bigtable/samples/quickstart_happybase/noxfile.py @@ -16,13 +16,12 @@ import glob import os -from pathlib import Path import sys +from pathlib import Path from typing import Callable, Dict, Optional import nox - # WARNING - WARNING - WARNING - WARNING - WARNING # WARNING - WARNING - WARNING - WARNING - WARNING # DO NOT EDIT THIS FILE EVER! @@ -160,6 +159,7 @@ def blacken(session: nox.sessions.Session) -> None: # format = isort + black # + @nox.session def format(session: nox.sessions.Session) -> None: """ @@ -187,7 +187,9 @@ def _session_tests( session: nox.sessions.Session, post_install: Callable = None ) -> None: # check for presence of tests - test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) test_list.extend(glob.glob("**/tests", recursive=True)) if len(test_list) == 0: @@ -209,9 +211,7 @@ def _session_tests( if os.path.exists("requirements-test.txt"): if os.path.exists("constraints-test.txt"): - session.install( - "-r", "requirements-test.txt", "-c", "constraints-test.txt" - ) + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") else: session.install("-r", "requirements-test.txt") with open("requirements-test.txt") as rtfile: @@ -224,9 +224,9 @@ def _session_tests( post_install(session) if "pytest-parallel" in packages: - concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) elif "pytest-xdist" in packages: - concurrent_args.extend(['-n', 'auto']) + concurrent_args.extend(["-n", "auto"]) session.run( "pytest", @@ -256,7 +256,7 @@ def py(session: nox.sessions.Session) -> None: def _get_repo_root() -> Optional[str]: - """ Returns the root folder of the project. """ + """Returns the root folder of the project.""" # Get root of this repository. Assume we don't have directories nested deeper than 10 items. p = Path(os.getcwd()) for i in range(10): diff --git a/packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async.py b/packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async.py index 332dbd56fb23..2d5a7e39521a 100644 --- a/packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async.py +++ b/packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async.py @@ -16,8 +16,7 @@ async def write_simple(table): # [START bigtable_async_write_simple] - from google.cloud.bigtable.data import BigtableDataClientAsync - from google.cloud.bigtable.data import SetCell + from google.cloud.bigtable.data import BigtableDataClientAsync, SetCell async def write_simple(project_id, instance_id, table_id): async with BigtableDataClientAsync(project=project_id) as client: @@ -40,9 +39,8 @@ async def write_simple(project_id, instance_id, table_id): async def write_batch(table): # [START bigtable_async_writes_batch] from google.cloud.bigtable.data import BigtableDataClientAsync - from google.cloud.bigtable.data.mutations import SetCell - from google.cloud.bigtable.data.mutations import RowMutationEntry from google.cloud.bigtable.data.exceptions import MutationsExceptionGroup + from google.cloud.bigtable.data.mutations import RowMutationEntry, SetCell async def write_batch(project_id, instance_id, table_id): async with BigtableDataClientAsync(project=project_id) as client: @@ -104,9 +102,7 @@ async def write_increment(project_id, instance_id, table_id): async def write_conditional(table): # [START bigtable_async_writes_conditional] - from google.cloud.bigtable.data import BigtableDataClientAsync - from google.cloud.bigtable.data import row_filters - from google.cloud.bigtable.data import SetCell + from google.cloud.bigtable.data import BigtableDataClientAsync, SetCell, row_filters async def write_conditional(project_id, instance_id, table_id): async with BigtableDataClientAsync(project=project_id) as client: @@ -139,9 +135,10 @@ async def write_conditional(project_id, instance_id, table_id): async def write_aggregate(table): # [START bigtable_async_write_aggregate] import time + from google.cloud.bigtable.data import BigtableDataClientAsync - from google.cloud.bigtable.data.mutations import AddToCell, RowMutationEntry from google.cloud.bigtable.data.exceptions import MutationsExceptionGroup + from google.cloud.bigtable.data.mutations import AddToCell, RowMutationEntry async def write_aggregate(project_id, instance_id, table_id): """Increments a value in a Bigtable table using AddToCell mutation.""" @@ -194,8 +191,7 @@ async def read_row(project_id, instance_id, table_id): async def read_row_partial(table): # [START bigtable_async_reads_row_partial] - from google.cloud.bigtable.data import BigtableDataClientAsync - from google.cloud.bigtable.data import row_filters + from google.cloud.bigtable.data import BigtableDataClientAsync, row_filters async def read_row_partial(project_id, instance_id, table_id): async with BigtableDataClientAsync(project=project_id) as client: @@ -212,13 +208,11 @@ async def read_row_partial(project_id, instance_id, table_id): async def read_rows_multiple(table): # [START bigtable_async_reads_rows] - from google.cloud.bigtable.data import BigtableDataClientAsync - from google.cloud.bigtable.data import ReadRowsQuery + from google.cloud.bigtable.data import BigtableDataClientAsync, ReadRowsQuery async def read_rows(project_id, instance_id, table_id): async with BigtableDataClientAsync(project=project_id) as client: async with client.get_table(instance_id, table_id) as table: - query = ReadRowsQuery( row_keys=[b"phone#4c410523#20190501", b"phone#4c410523#20190502"] ) @@ -231,14 +225,15 @@ async def read_rows(project_id, instance_id, table_id): async def read_row_range(table): # [START bigtable_async_reads_row_range] - from google.cloud.bigtable.data import BigtableDataClientAsync - from google.cloud.bigtable.data import ReadRowsQuery - from google.cloud.bigtable.data import RowRange + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + RowRange, + ) async def read_row_range(project_id, instance_id, table_id): async with BigtableDataClientAsync(project=project_id) as client: async with client.get_table(instance_id, table_id) as table: - row_range = RowRange( start_key=b"phone#4c410523#20190501", end_key=b"phone#4c410523#201906201", @@ -254,14 +249,15 @@ async def read_row_range(project_id, instance_id, table_id): async def read_with_prefix(table): # [START bigtable_async_reads_prefix] - from google.cloud.bigtable.data import BigtableDataClientAsync - from google.cloud.bigtable.data import ReadRowsQuery - from google.cloud.bigtable.data import RowRange + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + RowRange, + ) async def read_prefix(project_id, instance_id, table_id): async with BigtableDataClientAsync(project=project_id) as client: async with client.get_table(instance_id, table_id) as table: - prefix = "phone#" end_key = prefix[:-1] + chr(ord(prefix[-1]) + 1) prefix_range = RowRange(start_key=prefix, end_key=end_key) @@ -276,14 +272,15 @@ async def read_prefix(project_id, instance_id, table_id): async def read_with_filter(table): # [START bigtable_async_reads_filter] - from google.cloud.bigtable.data import BigtableDataClientAsync - from google.cloud.bigtable.data import ReadRowsQuery - from google.cloud.bigtable.data import row_filters + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + ReadRowsQuery, + row_filters, + ) async def read_with_filter(project_id, instance_id, table_id): async with BigtableDataClientAsync(project=project_id) as client: async with client.get_table(instance_id, table_id) as table: - row_filter = row_filters.ValueRegexFilter(b"PQ2A.*$") query = ReadRowsQuery(row_filter=row_filter) diff --git a/packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async_test.py b/packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async_test.py index 2761bd487114..6742d2260a83 100644 --- a/packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async_test.py +++ b/packages/google-cloud-bigtable/samples/snippets/data_client/data_client_snippets_async_test.py @@ -10,14 +10,14 @@ # 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 pytest -import pytest_asyncio import os import uuid -from . import data_client_snippets_async as data_snippets -from ...utils import create_table_cm +import pytest +import pytest_asyncio +from ...utils import create_table_cm +from . import data_client_snippets_async as data_snippets PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] diff --git a/packages/google-cloud-bigtable/samples/snippets/data_client/noxfile.py b/packages/google-cloud-bigtable/samples/snippets/data_client/noxfile.py index a169b5b5b464..c0e60097b353 100644 --- a/packages/google-cloud-bigtable/samples/snippets/data_client/noxfile.py +++ b/packages/google-cloud-bigtable/samples/snippets/data_client/noxfile.py @@ -16,13 +16,12 @@ import glob import os -from pathlib import Path import sys +from pathlib import Path from typing import Callable, Dict, Optional import nox - # WARNING - WARNING - WARNING - WARNING - WARNING # WARNING - WARNING - WARNING - WARNING - WARNING # DO NOT EDIT THIS FILE EVER! @@ -160,6 +159,7 @@ def blacken(session: nox.sessions.Session) -> None: # format = isort + black # + @nox.session def format(session: nox.sessions.Session) -> None: """ @@ -187,7 +187,9 @@ def _session_tests( session: nox.sessions.Session, post_install: Callable = None ) -> None: # check for presence of tests - test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) test_list.extend(glob.glob("**/tests", recursive=True)) if len(test_list) == 0: @@ -209,9 +211,7 @@ def _session_tests( if os.path.exists("requirements-test.txt"): if os.path.exists("constraints-test.txt"): - session.install( - "-r", "requirements-test.txt", "-c", "constraints-test.txt" - ) + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") else: session.install("-r", "requirements-test.txt") with open("requirements-test.txt") as rtfile: @@ -224,9 +224,9 @@ def _session_tests( post_install(session) if "pytest-parallel" in packages: - concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) elif "pytest-xdist" in packages: - concurrent_args.extend(['-n', 'auto']) + concurrent_args.extend(["-n", "auto"]) session.run( "pytest", @@ -256,7 +256,7 @@ def py(session: nox.sessions.Session) -> None: def _get_repo_root() -> Optional[str]: - """ Returns the root folder of the project. """ + """Returns the root folder of the project.""" # Get root of this repository. Assume we don't have directories nested deeper than 10 items. p = Path(os.getcwd()) for i in range(10): diff --git a/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_async_test.py b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_async_test.py index 4fb4898e5270..f5e93995cff9 100644 --- a/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_async_test.py +++ b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_async_test.py @@ -18,12 +18,12 @@ import uuid from typing import AsyncGenerator -from google.cloud._helpers import _microseconds_from_datetime import pytest import pytest_asyncio +from google.cloud._helpers import _microseconds_from_datetime -from . import deletes_snippets_async from ...utils import create_table_cm +from . import deletes_snippets_async PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] @@ -33,6 +33,7 @@ @pytest.fixture(scope="module") def event_loop(): import asyncio + loop = asyncio.get_event_loop_policy().new_event_loop() yield loop loop.close() @@ -40,7 +41,13 @@ def event_loop(): @pytest_asyncio.fixture(scope="module", autouse=True) async def table_id() -> AsyncGenerator[str, None]: - with create_table_cm(PROJECT, BIGTABLE_INSTANCE, TABLE_ID, {"stats_summary": None, "cell_plan": None}, verbose=False): + with create_table_cm( + PROJECT, + BIGTABLE_INSTANCE, + TABLE_ID, + {"stats_summary": None, "cell_plan": None}, + verbose=False, + ): await _populate_table(TABLE_ID) yield TABLE_ID diff --git a/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets.py b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets.py index 6cdbf33a69db..09f467577732 100644 --- a/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets.py +++ b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets.py @@ -28,6 +28,7 @@ def delete_from_column(project_id, instance_id, table_id): # [END bigtable_delete_from_column] + # [START bigtable_delete_from_column_family] def delete_from_column_family(project_id, instance_id, table_id): from google.cloud.bigtable import Client @@ -57,6 +58,7 @@ def delete_from_row(project_id, instance_id, table_id): # [END bigtable_delete_from_row] + # [START bigtable_streaming_and_batching] def streaming_and_batching(project_id, instance_id, table_id): from google.cloud.bigtable import Client @@ -75,6 +77,7 @@ def streaming_and_batching(project_id, instance_id, table_id): # [END bigtable_streaming_and_batching] + # [START bigtable_check_and_mutate] def check_and_mutate(project_id, instance_id, table_id): from google.cloud.bigtable import Client @@ -104,6 +107,7 @@ def drop_row_range(project_id, instance_id, table_id): # [END bigtable_drop_row_range] + # [START bigtable_delete_column_family] def delete_column_family(project_id, instance_id, table_id): from google.cloud.bigtable import Client @@ -118,6 +122,7 @@ def delete_column_family(project_id, instance_id, table_id): # [END bigtable_delete_column_family] + # [START bigtable_delete_table] def delete_table(project_id, instance_id, table_id): from google.cloud.bigtable import Client diff --git a/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets_async.py b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets_async.py index 2241fab4a71b..b70d557e7610 100644 --- a/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets_async.py +++ b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_snippets_async.py @@ -16,8 +16,10 @@ # [START bigtable_delete_from_column_asyncio] async def delete_from_column(project_id, instance_id, table_id): - from google.cloud.bigtable.data import BigtableDataClientAsync - from google.cloud.bigtable.data import DeleteRangeFromColumn + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + DeleteRangeFromColumn, + ) client = BigtableDataClientAsync(project=project_id) table = client.get_table(instance_id, table_id) @@ -33,10 +35,10 @@ async def delete_from_column(project_id, instance_id, table_id): # [END bigtable_delete_from_column_asyncio] + # [START bigtable_delete_from_column_family_asyncio] async def delete_from_column_family(project_id, instance_id, table_id): - from google.cloud.bigtable.data import BigtableDataClientAsync - from google.cloud.bigtable.data import DeleteAllFromFamily + from google.cloud.bigtable.data import BigtableDataClientAsync, DeleteAllFromFamily client = BigtableDataClientAsync(project=project_id) table = client.get_table(instance_id, table_id) @@ -52,8 +54,7 @@ async def delete_from_column_family(project_id, instance_id, table_id): # [START bigtable_delete_from_row_asyncio] async def delete_from_row(project_id, instance_id, table_id): - from google.cloud.bigtable.data import BigtableDataClientAsync - from google.cloud.bigtable.data import DeleteAllFromRow + from google.cloud.bigtable.data import BigtableDataClientAsync, DeleteAllFromRow client = BigtableDataClientAsync(project=project_id) table = client.get_table(instance_id, table_id) @@ -66,12 +67,15 @@ async def delete_from_row(project_id, instance_id, table_id): # [END bigtable_delete_from_row_asyncio] + # [START bigtable_streaming_and_batching_asyncio] async def streaming_and_batching(project_id, instance_id, table_id): - from google.cloud.bigtable.data import BigtableDataClientAsync - from google.cloud.bigtable.data import DeleteRangeFromColumn - from google.cloud.bigtable.data import RowMutationEntry - from google.cloud.bigtable.data import ReadRowsQuery + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + DeleteRangeFromColumn, + ReadRowsQuery, + RowMutationEntry, + ) client = BigtableDataClientAsync(project=project_id) table = client.get_table(instance_id, table_id) @@ -93,10 +97,13 @@ async def streaming_and_batching(project_id, instance_id, table_id): # [END bigtable_streaming_and_batching_asyncio] + # [START bigtable_check_and_mutate_asyncio] async def check_and_mutate(project_id, instance_id, table_id): - from google.cloud.bigtable.data import BigtableDataClientAsync - from google.cloud.bigtable.data import DeleteRangeFromColumn + from google.cloud.bigtable.data import ( + BigtableDataClientAsync, + DeleteRangeFromColumn, + ) from google.cloud.bigtable.data.row_filters import LiteralValueFilter client = BigtableDataClientAsync(project=project_id) diff --git a/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_test.py b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_test.py index 3284c37da739..a683df541309 100644 --- a/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_test.py +++ b/packages/google-cloud-bigtable/samples/snippets/deletes/deletes_test.py @@ -20,8 +20,8 @@ import pytest -from . import deletes_snippets from ...utils import create_table_cm +from . import deletes_snippets PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] @@ -32,7 +32,13 @@ def table_id(): from google.cloud.bigtable.row_set import RowSet - with create_table_cm(PROJECT, BIGTABLE_INSTANCE, TABLE_ID, {"stats_summary": None, "cell_plan": None}, verbose=False) as table: + with create_table_cm( + PROJECT, + BIGTABLE_INSTANCE, + TABLE_ID, + {"stats_summary": None, "cell_plan": None}, + verbose=False, + ) as table: timestamp = datetime.datetime(2019, 5, 1) timestamp_minus_hr = datetime.datetime(2019, 5, 1) - datetime.timedelta(hours=1) diff --git a/packages/google-cloud-bigtable/samples/snippets/deletes/noxfile.py b/packages/google-cloud-bigtable/samples/snippets/deletes/noxfile.py index a169b5b5b464..c0e60097b353 100644 --- a/packages/google-cloud-bigtable/samples/snippets/deletes/noxfile.py +++ b/packages/google-cloud-bigtable/samples/snippets/deletes/noxfile.py @@ -16,13 +16,12 @@ import glob import os -from pathlib import Path import sys +from pathlib import Path from typing import Callable, Dict, Optional import nox - # WARNING - WARNING - WARNING - WARNING - WARNING # WARNING - WARNING - WARNING - WARNING - WARNING # DO NOT EDIT THIS FILE EVER! @@ -160,6 +159,7 @@ def blacken(session: nox.sessions.Session) -> None: # format = isort + black # + @nox.session def format(session: nox.sessions.Session) -> None: """ @@ -187,7 +187,9 @@ def _session_tests( session: nox.sessions.Session, post_install: Callable = None ) -> None: # check for presence of tests - test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) test_list.extend(glob.glob("**/tests", recursive=True)) if len(test_list) == 0: @@ -209,9 +211,7 @@ def _session_tests( if os.path.exists("requirements-test.txt"): if os.path.exists("constraints-test.txt"): - session.install( - "-r", "requirements-test.txt", "-c", "constraints-test.txt" - ) + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") else: session.install("-r", "requirements-test.txt") with open("requirements-test.txt") as rtfile: @@ -224,9 +224,9 @@ def _session_tests( post_install(session) if "pytest-parallel" in packages: - concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) elif "pytest-xdist" in packages: - concurrent_args.extend(['-n', 'auto']) + concurrent_args.extend(["-n", "auto"]) session.run( "pytest", @@ -256,7 +256,7 @@ def py(session: nox.sessions.Session) -> None: def _get_repo_root() -> Optional[str]: - """ Returns the root folder of the project. """ + """Returns the root folder of the project.""" # Get root of this repository. Assume we don't have directories nested deeper than 10 items. p = Path(os.getcwd()) for i in range(10): diff --git a/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets.py b/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets.py index d17c773a4730..f2a1a0fd0a06 100644 --- a/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets.py +++ b/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets.py @@ -183,9 +183,10 @@ def filter_limit_value_regex(project_id, instance_id, table_id): # [END bigtable_filters_limit_value_regex] # [START bigtable_filters_limit_timestamp_range] def filter_limit_timestamp_range(project_id, instance_id, table_id): + import datetime + from google.cloud import bigtable from google.cloud.bigtable import row_filters - import datetime client = bigtable.Client(project=project_id, admin=True) instance = client.instance(instance_id) @@ -332,6 +333,7 @@ def filter_composing_condition(project_id, instance_id, table_id): # [END bigtable_filters_composing_condition] + # [START bigtable_filters_print] def print_row(row): print("Reading data for {}:".format(row.row_key.decode("utf-8"))) diff --git a/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets_async_test.py b/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets_async_test.py index a3f83a6f2404..3c961a27b752 100644 --- a/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets_async_test.py +++ b/packages/google-cloud-bigtable/samples/snippets/filters/filter_snippets_async_test.py @@ -13,22 +13,21 @@ import datetime +import inspect import os import uuid - -import inspect from typing import AsyncGenerator import pytest import pytest_asyncio -from .snapshots.snap_filters_test import snapshots - -from . import filter_snippets_async -from ...utils import create_table_cm from google.cloud._helpers import ( _microseconds_from_datetime, ) +from ...utils import create_table_cm +from . import filter_snippets_async +from .snapshots.snap_filters_test import snapshots + PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] TABLE_ID = f"mobile-time-series-filters-async-{str(uuid.uuid4())[:16]}" @@ -37,6 +36,7 @@ @pytest.fixture(scope="module") def event_loop(): import asyncio + loop = asyncio.get_event_loop_policy().new_event_loop() yield loop loop.close() @@ -44,7 +44,9 @@ def event_loop(): @pytest_asyncio.fixture(scope="module", autouse=True) async def table_id() -> AsyncGenerator[str, None]: - with create_table_cm(PROJECT, BIGTABLE_INSTANCE, TABLE_ID, {"stats_summary": None, "cell_plan": None}): + with create_table_cm( + PROJECT, BIGTABLE_INSTANCE, TABLE_ID, {"stats_summary": None, "cell_plan": None} + ): await _populate_table(TABLE_ID) yield TABLE_ID @@ -241,6 +243,7 @@ def _datetime_to_micros(value: datetime.datetime) -> int: """Uses the same conversion rules as the old client in""" import calendar import datetime as dt + if not value.tzinfo: value = value.replace(tzinfo=datetime.timezone.utc) # Regardless of what timezone is on the value, convert it to UTC. diff --git a/packages/google-cloud-bigtable/samples/snippets/filters/filters_test.py b/packages/google-cloud-bigtable/samples/snippets/filters/filters_test.py index fe99886bdb0c..c5d780c90e80 100644 --- a/packages/google-cloud-bigtable/samples/snippets/filters/filters_test.py +++ b/packages/google-cloud-bigtable/samples/snippets/filters/filters_test.py @@ -20,9 +20,9 @@ import pytest +from ...utils import create_table_cm from . import filter_snippets from .snapshots.snap_filters_test import snapshots -from ...utils import create_table_cm PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] @@ -34,8 +34,9 @@ def table_id(): from google.cloud.bigtable.row_set import RowSet table_id = TABLE_ID - with create_table_cm(PROJECT, BIGTABLE_INSTANCE, table_id, {"stats_summary": None, "cell_plan": None}) as table: - + with create_table_cm( + PROJECT, BIGTABLE_INSTANCE, table_id, {"stats_summary": None, "cell_plan": None} + ) as table: timestamp = datetime.datetime(2019, 5, 1) timestamp_minus_hr = datetime.datetime(2019, 5, 1) - datetime.timedelta(hours=1) diff --git a/packages/google-cloud-bigtable/samples/snippets/filters/noxfile.py b/packages/google-cloud-bigtable/samples/snippets/filters/noxfile.py index a169b5b5b464..c0e60097b353 100644 --- a/packages/google-cloud-bigtable/samples/snippets/filters/noxfile.py +++ b/packages/google-cloud-bigtable/samples/snippets/filters/noxfile.py @@ -16,13 +16,12 @@ import glob import os -from pathlib import Path import sys +from pathlib import Path from typing import Callable, Dict, Optional import nox - # WARNING - WARNING - WARNING - WARNING - WARNING # WARNING - WARNING - WARNING - WARNING - WARNING # DO NOT EDIT THIS FILE EVER! @@ -160,6 +159,7 @@ def blacken(session: nox.sessions.Session) -> None: # format = isort + black # + @nox.session def format(session: nox.sessions.Session) -> None: """ @@ -187,7 +187,9 @@ def _session_tests( session: nox.sessions.Session, post_install: Callable = None ) -> None: # check for presence of tests - test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) test_list.extend(glob.glob("**/tests", recursive=True)) if len(test_list) == 0: @@ -209,9 +211,7 @@ def _session_tests( if os.path.exists("requirements-test.txt"): if os.path.exists("constraints-test.txt"): - session.install( - "-r", "requirements-test.txt", "-c", "constraints-test.txt" - ) + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") else: session.install("-r", "requirements-test.txt") with open("requirements-test.txt") as rtfile: @@ -224,9 +224,9 @@ def _session_tests( post_install(session) if "pytest-parallel" in packages: - concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) elif "pytest-xdist" in packages: - concurrent_args.extend(['-n', 'auto']) + concurrent_args.extend(["-n", "auto"]) session.run( "pytest", @@ -256,7 +256,7 @@ def py(session: nox.sessions.Session) -> None: def _get_repo_root() -> Optional[str]: - """ Returns the root folder of the project. """ + """Returns the root folder of the project.""" # Get root of this repository. Assume we don't have directories nested deeper than 10 items. p = Path(os.getcwd()) for i in range(10): diff --git a/packages/google-cloud-bigtable/samples/snippets/filters/snapshots/snap_filters_test.py b/packages/google-cloud-bigtable/samples/snippets/filters/snapshots/snap_filters_test.py index 2331c93bc1b6..0547ddddd858 100644 --- a/packages/google-cloud-bigtable/samples/snippets/filters/snapshots/snap_filters_test.py +++ b/packages/google-cloud-bigtable/samples/snippets/filters/snapshots/snap_filters_test.py @@ -4,10 +4,9 @@ # expected outputs for each test from __future__ import unicode_literals - snapshots = {} -snapshots['test_filter_limit_row_regex'] = '''Reading data for phone#4c410523#20190501: +snapshots["test_filter_limit_row_regex"] = """Reading data for phone#4c410523#20190501: Column Family cell_plan \tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 \tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 @@ -25,9 +24,11 @@ \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 \tos_build: PQ2A.190401.002 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_filter_limit_cells_per_col'] = '''Reading data for phone#4c410523#20190501: +snapshots[ + "test_filter_limit_cells_per_col" +] = """Reading data for phone#4c410523#20190501: Column Family cell_plan \tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 \tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 @@ -69,9 +70,11 @@ \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 \tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_filter_limit_cells_per_row'] = '''Reading data for phone#4c410523#20190501: +snapshots[ + "test_filter_limit_cells_per_row" +] = """Reading data for phone#4c410523#20190501: Column Family cell_plan \tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 \tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 @@ -100,9 +103,11 @@ Column Family stats_summary \tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_filter_limit_cells_per_row_offset'] = '''Reading data for phone#4c410523#20190501: +snapshots[ + "test_filter_limit_cells_per_row_offset" +] = """Reading data for phone#4c410523#20190501: Column Family cell_plan \tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 Column Family stats_summary @@ -130,9 +135,11 @@ \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 \tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_filter_limit_col_family_regex'] = '''Reading data for phone#4c410523#20190501: +snapshots[ + "test_filter_limit_col_family_regex" +] = """Reading data for phone#4c410523#20190501: Column Family stats_summary \tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 @@ -162,9 +169,11 @@ \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 \tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_filter_limit_col_qualifier_regex'] = '''Reading data for phone#4c410523#20190501: +snapshots[ + "test_filter_limit_col_qualifier_regex" +] = """Reading data for phone#4c410523#20190501: Column Family stats_summary \tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 @@ -189,9 +198,9 @@ \tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_filter_limit_col_range'] = '''Reading data for phone#4c410523#20190501: +snapshots["test_filter_limit_col_range"] = """Reading data for phone#4c410523#20190501: Column Family cell_plan \tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 \tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 @@ -205,9 +214,11 @@ Column Family cell_plan \tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_filter_limit_value_range'] = '''Reading data for phone#4c410523#20190501: +snapshots[ + "test_filter_limit_value_range" +] = """Reading data for phone#4c410523#20190501: Column Family stats_summary \tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 @@ -215,9 +226,11 @@ Column Family stats_summary \tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_filter_limit_value_regex'] = '''Reading data for phone#4c410523#20190501: +snapshots[ + "test_filter_limit_value_regex" +] = """Reading data for phone#4c410523#20190501: Column Family stats_summary \tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 @@ -237,17 +250,19 @@ Column Family stats_summary \tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_filter_limit_timestamp_range'] = '''Reading data for phone#4c410523#20190501: +snapshots[ + "test_filter_limit_timestamp_range" +] = """Reading data for phone#4c410523#20190501: Column Family cell_plan \tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 -''' +""" -snapshots['test_filter_limit_block_all'] = '' +snapshots["test_filter_limit_block_all"] = "" -snapshots['test_filter_limit_pass_all'] = '''Reading data for phone#4c410523#20190501: +snapshots["test_filter_limit_pass_all"] = """Reading data for phone#4c410523#20190501: Column Family cell_plan \tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 \tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 @@ -289,9 +304,11 @@ \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 \tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_filter_modify_strip_value'] = '''Reading data for phone#4c410523#20190501: +snapshots[ + "test_filter_modify_strip_value" +] = """Reading data for phone#4c410523#20190501: Column Family cell_plan \tdata_plan_01gb: @2019-05-01 00:00:00+00:00 \tdata_plan_01gb: @2019-04-30 23:00:00+00:00 @@ -333,9 +350,11 @@ \tconnected_wifi: @2019-05-01 00:00:00+00:00 \tos_build: @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_filter_modify_apply_label'] = '''Reading data for phone#4c410523#20190501: +snapshots[ + "test_filter_modify_apply_label" +] = """Reading data for phone#4c410523#20190501: Column Family cell_plan \tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 [labelled] \tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 [labelled] @@ -377,9 +396,9 @@ \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 [labelled] \tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 [labelled] -''' +""" -snapshots['test_filter_composing_chain'] = '''Reading data for phone#4c410523#20190501: +snapshots["test_filter_composing_chain"] = """Reading data for phone#4c410523#20190501: Column Family cell_plan \tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 \tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 @@ -400,9 +419,11 @@ Column Family cell_plan \tdata_plan_10gb: true @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_filter_composing_interleave'] = '''Reading data for phone#4c410523#20190501: +snapshots[ + "test_filter_composing_interleave" +] = """Reading data for phone#4c410523#20190501: Column Family cell_plan \tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 \tdata_plan_05gb: true @2019-05-01 00:00:00+00:00 @@ -433,9 +454,11 @@ Column Family stats_summary \tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_filter_composing_condition'] = '''Reading data for phone#4c410523#20190501: +snapshots[ + "test_filter_composing_condition" +] = """Reading data for phone#4c410523#20190501: Column Family cell_plan \tdata_plan_01gb: false @2019-05-01 00:00:00+00:00 [filtered-out] \tdata_plan_01gb: true @2019-04-30 23:00:00+00:00 [filtered-out] @@ -477,4 +500,4 @@ \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 [passed-filter] \tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 [passed-filter] -''' +""" diff --git a/packages/google-cloud-bigtable/samples/snippets/reads/noxfile.py b/packages/google-cloud-bigtable/samples/snippets/reads/noxfile.py index a169b5b5b464..c0e60097b353 100644 --- a/packages/google-cloud-bigtable/samples/snippets/reads/noxfile.py +++ b/packages/google-cloud-bigtable/samples/snippets/reads/noxfile.py @@ -16,13 +16,12 @@ import glob import os -from pathlib import Path import sys +from pathlib import Path from typing import Callable, Dict, Optional import nox - # WARNING - WARNING - WARNING - WARNING - WARNING # WARNING - WARNING - WARNING - WARNING - WARNING # DO NOT EDIT THIS FILE EVER! @@ -160,6 +159,7 @@ def blacken(session: nox.sessions.Session) -> None: # format = isort + black # + @nox.session def format(session: nox.sessions.Session) -> None: """ @@ -187,7 +187,9 @@ def _session_tests( session: nox.sessions.Session, post_install: Callable = None ) -> None: # check for presence of tests - test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) test_list.extend(glob.glob("**/tests", recursive=True)) if len(test_list) == 0: @@ -209,9 +211,7 @@ def _session_tests( if os.path.exists("requirements-test.txt"): if os.path.exists("constraints-test.txt"): - session.install( - "-r", "requirements-test.txt", "-c", "constraints-test.txt" - ) + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") else: session.install("-r", "requirements-test.txt") with open("requirements-test.txt") as rtfile: @@ -224,9 +224,9 @@ def _session_tests( post_install(session) if "pytest-parallel" in packages: - concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) elif "pytest-xdist" in packages: - concurrent_args.extend(['-n', 'auto']) + concurrent_args.extend(["-n", "auto"]) session.run( "pytest", @@ -256,7 +256,7 @@ def py(session: nox.sessions.Session) -> None: def _get_repo_root() -> Optional[str]: - """ Returns the root folder of the project. """ + """Returns the root folder of the project.""" # Get root of this repository. Assume we don't have directories nested deeper than 10 items. p = Path(os.getcwd()) for i in range(10): diff --git a/packages/google-cloud-bigtable/samples/snippets/reads/read_snippets.py b/packages/google-cloud-bigtable/samples/snippets/reads/read_snippets.py index 210ca73a71bd..7bdf01c7c890 100644 --- a/packages/google-cloud-bigtable/samples/snippets/reads/read_snippets.py +++ b/packages/google-cloud-bigtable/samples/snippets/reads/read_snippets.py @@ -29,6 +29,7 @@ def read_row(project_id, instance_id, table_id): # [END bigtable_reads_row] + # [START bigtable_reads_row_partial] def read_row_partial(project_id, instance_id, table_id): from google.cloud import bigtable @@ -144,6 +145,7 @@ def read_filter(project_id, instance_id, table_id): # [END bigtable_reads_filter] + # [START bigtable_reads_print] def print_row(row): print("Reading data for {}:".format(row.row_key.decode("utf-8"))) diff --git a/packages/google-cloud-bigtable/samples/snippets/reads/reads_test.py b/packages/google-cloud-bigtable/samples/snippets/reads/reads_test.py index 0078ce5981af..251141954955 100644 --- a/packages/google-cloud-bigtable/samples/snippets/reads/reads_test.py +++ b/packages/google-cloud-bigtable/samples/snippets/reads/reads_test.py @@ -12,16 +12,15 @@ # limitations under the License. import datetime -import os import inspect +import os import uuid import pytest -from .snapshots.snap_reads_test import snapshots -from . import read_snippets from ...utils import create_table_cm - +from . import read_snippets +from .snapshots.snap_reads_test import snapshots PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] @@ -30,7 +29,9 @@ @pytest.fixture(scope="module", autouse=True) def table_id(): - with create_table_cm(PROJECT, BIGTABLE_INSTANCE, TABLE_ID, {"stats_summary": None}) as table: + with create_table_cm( + PROJECT, BIGTABLE_INSTANCE, TABLE_ID, {"stats_summary": None} + ) as table: timestamp = datetime.datetime(2019, 5, 1) rows = [ table.direct_row("phone#4c410523#20190501"), diff --git a/packages/google-cloud-bigtable/samples/snippets/reads/snapshots/snap_reads_test.py b/packages/google-cloud-bigtable/samples/snippets/reads/snapshots/snap_reads_test.py index 564a4df7eecc..c2449d123a38 100644 --- a/packages/google-cloud-bigtable/samples/snippets/reads/snapshots/snap_reads_test.py +++ b/packages/google-cloud-bigtable/samples/snippets/reads/snapshots/snap_reads_test.py @@ -6,13 +6,13 @@ snapshots = {} -snapshots['test_read_row_partial'] = '''Reading data for phone#4c410523#20190501: +snapshots["test_read_row_partial"] = """Reading data for phone#4c410523#20190501: Column Family stats_summary \tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_read_rows'] = '''Reading data for phone#4c410523#20190501: +snapshots["test_read_rows"] = """Reading data for phone#4c410523#20190501: Column Family stats_summary \tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 @@ -24,9 +24,9 @@ \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 \tos_build: PQ2A.190405.004 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_read_row_range'] = '''Reading data for phone#4c410523#20190501: +snapshots["test_read_row_range"] = """Reading data for phone#4c410523#20190501: Column Family stats_summary \tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 @@ -44,9 +44,9 @@ \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 \tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_read_row_ranges'] = '''Reading data for phone#4c410523#20190501: +snapshots["test_read_row_ranges"] = """Reading data for phone#4c410523#20190501: Column Family stats_summary \tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 @@ -76,9 +76,9 @@ \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 \tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_read_prefix'] = '''Reading data for phone#4c410523#20190501: +snapshots["test_read_prefix"] = """Reading data for phone#4c410523#20190501: Column Family stats_summary \tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 @@ -108,9 +108,9 @@ \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x00 @2019-05-01 00:00:00+00:00 \tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_read_filter'] = '''Reading data for phone#4c410523#20190501: +snapshots["test_read_filter"] = """Reading data for phone#4c410523#20190501: Column Family stats_summary \tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 @@ -130,12 +130,12 @@ Column Family stats_summary \tos_build: PQ2A.190406.000 @2019-05-01 00:00:00+00:00 -''' +""" -snapshots['test_read_row'] = '''Reading data for phone#4c410523#20190501: +snapshots["test_read_row"] = """Reading data for phone#4c410523#20190501: Column Family stats_summary \tconnected_cell: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 \tconnected_wifi: \x00\x00\x00\x00\x00\x00\x00\x01 @2019-05-01 00:00:00+00:00 \tos_build: PQ2A.190405.003 @2019-05-01 00:00:00+00:00 -''' +""" diff --git a/packages/google-cloud-bigtable/samples/snippets/writes/noxfile.py b/packages/google-cloud-bigtable/samples/snippets/writes/noxfile.py index a169b5b5b464..c0e60097b353 100644 --- a/packages/google-cloud-bigtable/samples/snippets/writes/noxfile.py +++ b/packages/google-cloud-bigtable/samples/snippets/writes/noxfile.py @@ -16,13 +16,12 @@ import glob import os -from pathlib import Path import sys +from pathlib import Path from typing import Callable, Dict, Optional import nox - # WARNING - WARNING - WARNING - WARNING - WARNING # WARNING - WARNING - WARNING - WARNING - WARNING # DO NOT EDIT THIS FILE EVER! @@ -160,6 +159,7 @@ def blacken(session: nox.sessions.Session) -> None: # format = isort + black # + @nox.session def format(session: nox.sessions.Session) -> None: """ @@ -187,7 +187,9 @@ def _session_tests( session: nox.sessions.Session, post_install: Callable = None ) -> None: # check for presence of tests - test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) test_list.extend(glob.glob("**/tests", recursive=True)) if len(test_list) == 0: @@ -209,9 +211,7 @@ def _session_tests( if os.path.exists("requirements-test.txt"): if os.path.exists("constraints-test.txt"): - session.install( - "-r", "requirements-test.txt", "-c", "constraints-test.txt" - ) + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") else: session.install("-r", "requirements-test.txt") with open("requirements-test.txt") as rtfile: @@ -224,9 +224,9 @@ def _session_tests( post_install(session) if "pytest-parallel" in packages: - concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) elif "pytest-xdist" in packages: - concurrent_args.extend(['-n', 'auto']) + concurrent_args.extend(["-n", "auto"]) session.run( "pytest", @@ -256,7 +256,7 @@ def py(session: nox.sessions.Session) -> None: def _get_repo_root() -> Optional[str]: - """ Returns the root folder of the project. """ + """Returns the root folder of the project.""" # Get root of this repository. Assume we don't have directories nested deeper than 10 items. p = Path(os.getcwd()) for i in range(10): diff --git a/packages/google-cloud-bigtable/samples/snippets/writes/writes_test.py b/packages/google-cloud-bigtable/samples/snippets/writes/writes_test.py index 2c7a3d62b162..663122d3e783 100644 --- a/packages/google-cloud-bigtable/samples/snippets/writes/writes_test.py +++ b/packages/google-cloud-bigtable/samples/snippets/writes/writes_test.py @@ -13,17 +13,17 @@ # limitations under the License. import os +import uuid import backoff -from google.api_core.exceptions import DeadlineExceeded import pytest -import uuid +from google.api_core.exceptions import DeadlineExceeded +from ...utils import create_table_cm from .write_batch import write_batch from .write_conditionally import write_conditional from .write_increment import write_increment from .write_simple import write_simple -from ...utils import create_table_cm PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] @@ -37,7 +37,6 @@ def table_id(): def test_writes(capsys, table_id): - # `row.commit()` sometimes ends up with DeadlineExceeded, so now # we put retries with a hard deadline. @backoff.on_exception(backoff.expo, DeadlineExceeded, max_time=60) diff --git a/packages/google-cloud-bigtable/samples/tableadmin/noxfile.py b/packages/google-cloud-bigtable/samples/tableadmin/noxfile.py index a169b5b5b464..c0e60097b353 100644 --- a/packages/google-cloud-bigtable/samples/tableadmin/noxfile.py +++ b/packages/google-cloud-bigtable/samples/tableadmin/noxfile.py @@ -16,13 +16,12 @@ import glob import os -from pathlib import Path import sys +from pathlib import Path from typing import Callable, Dict, Optional import nox - # WARNING - WARNING - WARNING - WARNING - WARNING # WARNING - WARNING - WARNING - WARNING - WARNING # DO NOT EDIT THIS FILE EVER! @@ -160,6 +159,7 @@ def blacken(session: nox.sessions.Session) -> None: # format = isort + black # + @nox.session def format(session: nox.sessions.Session) -> None: """ @@ -187,7 +187,9 @@ def _session_tests( session: nox.sessions.Session, post_install: Callable = None ) -> None: # check for presence of tests - test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob("**/test_*.py", recursive=True) + test_list = glob.glob("**/*_test.py", recursive=True) + glob.glob( + "**/test_*.py", recursive=True + ) test_list.extend(glob.glob("**/tests", recursive=True)) if len(test_list) == 0: @@ -209,9 +211,7 @@ def _session_tests( if os.path.exists("requirements-test.txt"): if os.path.exists("constraints-test.txt"): - session.install( - "-r", "requirements-test.txt", "-c", "constraints-test.txt" - ) + session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt") else: session.install("-r", "requirements-test.txt") with open("requirements-test.txt") as rtfile: @@ -224,9 +224,9 @@ def _session_tests( post_install(session) if "pytest-parallel" in packages: - concurrent_args.extend(['--workers', 'auto', '--tests-per-worker', 'auto']) + concurrent_args.extend(["--workers", "auto", "--tests-per-worker", "auto"]) elif "pytest-xdist" in packages: - concurrent_args.extend(['-n', 'auto']) + concurrent_args.extend(["-n", "auto"]) session.run( "pytest", @@ -256,7 +256,7 @@ def py(session: nox.sessions.Session) -> None: def _get_repo_root() -> Optional[str]: - """ Returns the root folder of the project. """ + """Returns the root folder of the project.""" # Get root of this repository. Assume we don't have directories nested deeper than 10 items. p = Path(os.getcwd()) for i in range(10): diff --git a/packages/google-cloud-bigtable/samples/tableadmin/tableadmin.py b/packages/google-cloud-bigtable/samples/tableadmin/tableadmin.py index ad00e57887c3..d62cfa3328b0 100644 --- a/packages/google-cloud-bigtable/samples/tableadmin/tableadmin.py +++ b/packages/google-cloud-bigtable/samples/tableadmin/tableadmin.py @@ -35,6 +35,7 @@ from google.cloud import bigtable from google.cloud.bigtable import column_family + from ..utils import create_table_cm diff --git a/packages/google-cloud-bigtable/samples/tableadmin/tableadmin_test.py b/packages/google-cloud-bigtable/samples/tableadmin/tableadmin_test.py index 0ffdc75c9066..1c4cc41a1964 100755 --- a/packages/google-cloud-bigtable/samples/tableadmin/tableadmin_test.py +++ b/packages/google-cloud-bigtable/samples/tableadmin/tableadmin_test.py @@ -14,13 +14,13 @@ # limitations under the License. import os -from test_utils.retry import RetryErrors -from google.api_core import exceptions import uuid -from .tableadmin import delete_table -from .tableadmin import run_table_operations +from google.api_core import exceptions +from test_utils.retry import RetryErrors + from ..utils import create_table_cm +from .tableadmin import delete_table, run_table_operations PROJECT = os.environ["GOOGLE_CLOUD_PROJECT"] BIGTABLE_INSTANCE = os.environ["BIGTABLE_INSTANCE"] diff --git a/packages/google-cloud-bigtable/samples/testdata/singer_pb2.py b/packages/google-cloud-bigtable/samples/testdata/singer_pb2.py index d2a328df0e9a..f5da249d4811 100644 --- a/packages/google-cloud-bigtable/samples/testdata/singer_pb2.py +++ b/packages/google-cloud-bigtable/samples/testdata/singer_pb2.py @@ -2,26 +2,27 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # source: singer.proto """Generated protocol buffer code.""" -from google.protobuf.internal import builder as _builder + from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0csinger.proto\x12\x17\x65xamples.bigtable.music\"E\n\x06Singer\x12\x0c\n\x04name\x18\x01 \x01(\t\x12-\n\x05genre\x18\x02 \x01(\x0e\x32\x1e.examples.bigtable.music.Genre*.\n\x05Genre\x12\x07\n\x03POP\x10\x00\x12\x08\n\x04JAZZ\x10\x01\x12\x08\n\x04\x46OLK\x10\x02\x12\x08\n\x04ROCK\x10\x03\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x0csinger.proto\x12\x17\x65xamples.bigtable.music"E\n\x06Singer\x12\x0c\n\x04name\x18\x01 \x01(\t\x12-\n\x05genre\x18\x02 \x01(\x0e\x32\x1e.examples.bigtable.music.Genre*.\n\x05Genre\x12\x07\n\x03POP\x10\x00\x12\x08\n\x04JAZZ\x10\x01\x12\x08\n\x04\x46OLK\x10\x02\x12\x08\n\x04ROCK\x10\x03\x62\x06proto3' +) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'singer_pb2', globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "singer_pb2", globals()) if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - _GENRE._serialized_start=112 - _GENRE._serialized_end=158 - _SINGER._serialized_start=41 - _SINGER._serialized_end=110 + DESCRIPTOR._options = None + _GENRE._serialized_start = 112 + _GENRE._serialized_end = 158 + _SINGER._serialized_start = 41 + _SINGER._serialized_end = 110 # @@protoc_insertion_point(module_scope) diff --git a/packages/google-cloud-bigtable/samples/utils.py b/packages/google-cloud-bigtable/samples/utils.py index f796aaedb79c..d093d0427cbf 100644 --- a/packages/google-cloud-bigtable/samples/utils.py +++ b/packages/google-cloud-bigtable/samples/utils.py @@ -14,21 +14,24 @@ Provides helper logic used across samples """ +from google.api_core import exceptions +from google.api_core.retry import Retry, if_exception_type from google.cloud import bigtable from google.cloud.bigtable.column_family import ColumnFamily from google.cloud.bigtable_admin_v2.types import ColumnFamily as ColumnFamily_pb -from google.api_core import exceptions -from google.api_core.retry import Retry -from google.api_core.retry import if_exception_type -delete_retry = Retry(if_exception_type(exceptions.TooManyRequests, exceptions.ServiceUnavailable)) +delete_retry = Retry( + if_exception_type(exceptions.TooManyRequests, exceptions.ServiceUnavailable) +) + class create_table_cm: """ Create a new table using a context manager, to ensure that table.delete() is called to clean up - the table, even if an exception is thrown + the table, even if an exception is thrown """ + def __init__(self, *args, verbose=True, **kwargs): self._args = args self._kwargs = kwargs @@ -63,7 +66,9 @@ def create_table(project, instance_id, table_id, column_families={}): # convert column families to pb if needed pb_families = { - id: ColumnFamily(id, table, rule).to_pb() if not isinstance(rule, ColumnFamily_pb) else rule + id: ColumnFamily(id, table, rule).to_pb() + if not isinstance(rule, ColumnFamily_pb) + else rule for (id, rule) in column_families.items() } @@ -80,6 +85,7 @@ def create_table(project, instance_id, table_id, column_families={}): return table + @Retry( on_error=if_exception_type( exceptions.PreconditionFailed, @@ -96,4 +102,4 @@ def wait_for_table(table): retry with backoff if the table is not ready """ if not table.exists(): - raise exceptions.NotFound \ No newline at end of file + raise exceptions.NotFound From 1e11b656a9549883e7bd4d59c5c2afd09ebda924 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 22 May 2026 21:53:52 -0700 Subject: [PATCH 5/5] removed accidental file --- .../samples/quickstart/diff | 3534 ----------------- 1 file changed, 3534 deletions(-) delete mode 100644 packages/google-cloud-bigtable/samples/quickstart/diff diff --git a/packages/google-cloud-bigtable/samples/quickstart/diff b/packages/google-cloud-bigtable/samples/quickstart/diff deleted file mode 100644 index 7d87fc52547e..000000000000 --- a/packages/google-cloud-bigtable/samples/quickstart/diff +++ /dev/null @@ -1,3534 +0,0 @@ -diff --git a/docs/classic_client/snippets.py b/docs/classic_client/snippets.py -index fa3aa362..f0558854 100644 ---- a/docs/classic_client/snippets.py -+++ b/docs/classic_client/snippets.py -@@ -57,8 +57,8 @@ SERVER_NODES = 3 - STORAGE_TYPE = enums.StorageType.SSD - LABEL_KEY = "python-snippet" - LABEL_STAMP = ( -- datetime.datetime.utcnow() -- .replace(microsecond=0, tzinfo=UTC) -+ datetime.datetime.now(datetime.timezone.utc) -+ .replace(microsecond=0) - .strftime("%Y-%m-%dt%H-%M-%S") - ) - LABELS = {LABEL_KEY: str(LABEL_STAMP)} -diff --git a/docs/classic_client/snippets_table.py b/docs/classic_client/snippets_table.py -index 89313527..0217c530 100644 ---- a/docs/classic_client/snippets_table.py -+++ b/docs/classic_client/snippets_table.py -@@ -37,7 +37,6 @@ from google.api_core.exceptions import ServiceUnavailable - from test_utils.system import unique_resource_id - from test_utils.retry import RetryErrors - --from google.cloud._helpers import UTC - from google.cloud.bigtable import Client - from google.cloud.bigtable import enums - from google.cloud.bigtable import column_family -@@ -54,8 +53,8 @@ SERVER_NODES = 3 - STORAGE_TYPE = enums.StorageType.SSD - LABEL_KEY = "python-snippet" - LABEL_STAMP = ( -- datetime.datetime.utcnow() -- .replace(microsecond=0, tzinfo=UTC) -+ datetime.datetime.now(datetime.timezone.utc) -+ .replace(microsecond=0) - .strftime("%Y-%m-%dt%H-%M-%S") - ) - LABELS = {LABEL_KEY: str(LABEL_STAMP)} -@@ -179,7 +178,7 @@ def test_bigtable_write_read_drop_truncate(): - value = "value_{}".format(i).encode() - row = table.row(row_key) - row.set_cell( -- COLUMN_FAMILY_ID, col_name, value, timestamp=datetime.datetime.utcnow() -+ COLUMN_FAMILY_ID, col_name, value, timestamp=datetime.datetime.now(datetime.timezone.utc) - ) - rows.append(row) - response = table.mutate_rows(rows) -@@ -270,7 +269,7 @@ def test_bigtable_mutations_batcher(): - row_key = row_keys[0] - row = table.row(row_key) - row.set_cell( -- COLUMN_FAMILY_ID, column_name, "value-0", timestamp=datetime.datetime.utcnow() -+ COLUMN_FAMILY_ID, column_name, "value-0", timestamp=datetime.datetime.now(datetime.timezone.utc) - ) - batcher.mutate(row) - # Add a collections of rows -@@ -279,7 +278,7 @@ def test_bigtable_mutations_batcher(): - row = table.row(row_keys[i]) - value = "value_{}".format(i).encode() - row.set_cell( -- COLUMN_FAMILY_ID, column_name, value, timestamp=datetime.datetime.utcnow() -+ COLUMN_FAMILY_ID, column_name, value, timestamp=datetime.datetime.now(datetime.timezone.utc) - ) - rows.append(row) - batcher.mutate_rows(rows) -@@ -759,7 +758,7 @@ def test_bigtable_batcher_mutate_flush_mutate_rows(): - row_key = b"row_key_1" - row = table.row(row_key) - row.set_cell( -- COLUMN_FAMILY_ID, COL_NAME1, "value-0", timestamp=datetime.datetime.utcnow() -+ COLUMN_FAMILY_ID, COL_NAME1, "value-0", timestamp=datetime.datetime.now(datetime.timezone.utc) - ) - - # In batcher, mutate will flush current batch if it -@@ -967,12 +966,12 @@ def test_bigtable_row_data_cells_cell_value_cell_values(): - value = b"value_in_col1" - row = Config.TABLE.row(b"row_key_1") - row.set_cell( -- COLUMN_FAMILY_ID, COL_NAME1, value, timestamp=datetime.datetime.utcnow() -+ COLUMN_FAMILY_ID, COL_NAME1, value, timestamp=datetime.datetime.now(datetime.timezone.utc) - ) - row.commit() - - row.set_cell( -- COLUMN_FAMILY_ID, COL_NAME1, value, timestamp=datetime.datetime.utcnow() -+ COLUMN_FAMILY_ID, COL_NAME1, value, timestamp=datetime.datetime.now(datetime.timezone.utc) - ) - row.commit() - -@@ -1050,7 +1049,7 @@ def test_bigtable_row_setcell_rowkey(): - - cell_val = b"cell-val" - row.set_cell( -- COLUMN_FAMILY_ID, COL_NAME1, cell_val, timestamp=datetime.datetime.utcnow() -+ COLUMN_FAMILY_ID, COL_NAME1, cell_val, timestamp=datetime.datetime.now(datetime.timezone.utc) - ) - # [END bigtable_api_row_set_cell] - -diff --git a/google/cloud/bigtable/cluster.py b/google/cloud/bigtable/cluster.py -index 967ec707..11fb5492 100644 ---- a/google/cloud/bigtable/cluster.py -+++ b/google/cloud/bigtable/cluster.py -@@ -511,11 +511,9 @@ class Cluster(object): - def _to_pb(self): - """Create cluster proto buff message for API calls""" - client = self._instance._client -- location = None -- if self.location_id: -- location = client.instance_admin_client.common_location_path( -- client.project, self.location_id -- ) -+ location = client.instance_admin_client.common_location_path( -+ client.project, self.location_id -+ ) - - cluster_pb = instance.Cluster( - location=location, -diff --git a/google/cloud/bigtable/data/_async/client.py b/google/cloud/bigtable/data/_async/client.py -index f86c886f..54a41036 100644 ---- a/google/cloud/bigtable/data/_async/client.py -+++ b/google/cloud/bigtable/data/_async/client.py -@@ -88,7 +88,6 @@ from google.cloud.bigtable.data.row_filters import RowFilter - from google.cloud.bigtable.data.row_filters import StripValueTransformerFilter - from google.cloud.bigtable.data.row_filters import CellsRowLimitFilter - from google.cloud.bigtable.data.row_filters import RowFilterChain --from google.cloud.bigtable.data._metrics import BigtableClientSideMetricsController - - from google.cloud.bigtable.data._cross_sync import CrossSync - -@@ -1040,8 +1039,6 @@ class _DataApiTargetAsync(abc.ABC): - default_retryable_errors or () - ) - -- self._metrics = BigtableClientSideMetricsController() -- - try: - self._register_instance_future = CrossSync.create_task( - self.client._register_instance, -@@ -1756,7 +1753,6 @@ class _DataApiTargetAsync(abc.ABC): - """ - Called to close the Table instance and release any resources held by it. - """ -- self._metrics.close() - if self._register_instance_future: - self._register_instance_future.cancel() - self.client._remove_instance_registration( -diff --git a/google/cloud/bigtable/data/_async/metrics_interceptor.py b/google/cloud/bigtable/data/_async/metrics_interceptor.py -index 249dcdcc..a154c008 100644 ---- a/google/cloud/bigtable/data/_async/metrics_interceptor.py -+++ b/google/cloud/bigtable/data/_async/metrics_interceptor.py -@@ -13,21 +13,11 @@ - # limitations under the License. - from __future__ import annotations - --from typing import Sequence -- --import time --from functools import wraps -- --from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric --from google.cloud.bigtable.data._metrics.data_model import OperationState --from google.cloud.bigtable.data._metrics.data_model import OperationType -- - from google.cloud.bigtable.data._cross_sync import CrossSync - - if CrossSync.is_async: - from grpc.aio import UnaryUnaryClientInterceptor - from grpc.aio import UnaryStreamClientInterceptor -- from grpc.aio import AioRpcError - else: - from grpc import UnaryUnaryClientInterceptor - from grpc import UnaryStreamClientInterceptor -@@ -36,57 +26,6 @@ else: - __CROSS_SYNC_OUTPUT__ = "google.cloud.bigtable.data._sync_autogen.metrics_interceptor" - - --def _with_active_operation(func): -- """ -- Decorator for interceptor methods to extract the active operation associated with the -- in-scope contextvars, and pass it to the decorated function. -- """ -- -- @wraps(func) -- def wrapper(self, continuation, client_call_details, request): -- operation: ActiveOperationMetric | None = ActiveOperationMetric.from_context() -- -- if operation: -- # start a new attempt if not started -- if ( -- operation.state == OperationState.CREATED -- or operation.state == OperationState.BETWEEN_ATTEMPTS -- ): -- operation.start_attempt() -- # wrap continuation in logic to process the operation -- return func(self, operation, continuation, client_call_details, request) -- else: -- # if operation not found, return unwrapped continuation -- return continuation(client_call_details, request) -- -- return wrapper -- -- --@CrossSync.convert --async def _get_metadata(source) -> dict[str, str | bytes] | None: -- """Helper to extract metadata from a call or RpcError""" -- try: -- metadata: Sequence[tuple[str, str | bytes]] -- if CrossSync.is_async: -- # grpc.aio returns metadata in Metadata objects -- if isinstance(source, AioRpcError): -- metadata = list(source.trailing_metadata()) + list( -- source.initial_metadata() -- ) -- else: -- metadata = list(await source.trailing_metadata()) + list( -- await source.initial_metadata() -- ) -- else: -- # sync grpc returns metadata as a sequence of tuples -- metadata = source.trailing_metadata() + source.initial_metadata() -- # convert metadata to dict format -- return {k: v for (k, v) in metadata} -- except Exception: -- # ignore errors while fetching metadata -- return None -- -- - @CrossSync.convert_class(sync_name="BigtableMetricsInterceptor") - class AsyncBigtableMetricsInterceptor( - UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor -@@ -96,33 +35,21 @@ class AsyncBigtableMetricsInterceptor( - """ - - @CrossSync.convert -- @_with_active_operation -- async def intercept_unary_unary( -- self, operation, continuation, client_call_details, request -- ): -+ async def intercept_unary_unary(self, continuation, client_call_details, request): - """ - Interceptor for unary rpcs: - - MutateRow - - CheckAndMutateRow - - ReadModifyWriteRow - """ -- metadata = None - try: - call = await continuation(client_call_details, request) -- metadata = await _get_metadata(call) - return call - except Exception as rpc_error: -- metadata = await _get_metadata(rpc_error) - raise rpc_error -- finally: -- if metadata is not None: -- operation.add_response_metadata(metadata) - - @CrossSync.convert -- @_with_active_operation -- async def intercept_unary_stream( -- self, operation, continuation, client_call_details, request -- ): -+ async def intercept_unary_stream(self, continuation, client_call_details, request): - """ - Interceptor for streaming rpcs: - - ReadRows -@@ -131,42 +58,21 @@ class AsyncBigtableMetricsInterceptor( - """ - try: - return self._streaming_generator_wrapper( -- operation, await continuation(client_call_details, request) -+ await continuation(client_call_details, request) - ) - except Exception as rpc_error: - # handle errors while intializing stream -- metadata = await _get_metadata(rpc_error) -- if metadata is not None: -- operation.add_response_metadata(metadata) - raise rpc_error - - @staticmethod - @CrossSync.convert -- async def _streaming_generator_wrapper(operation, call): -+ async def _streaming_generator_wrapper(call): - """ - Wrapped generator to be returned by intercept_unary_stream. - """ -- # only track has_first response for READ_ROWS -- has_first_response = ( -- operation.first_response_latency_ns is not None -- or operation.op_type != OperationType.READ_ROWS -- ) -- encountered_exc = None - try: - async for response in call: -- # record time to first response. Currently only used for READ_ROWs -- if not has_first_response: -- operation.first_response_latency_ns = ( -- time.monotonic_ns() - operation.start_time_ns -- ) -- has_first_response = True - yield response - except Exception as e: - # handle errors while processing stream -- encountered_exc = e -- raise -- finally: -- if call is not None: -- metadata = await _get_metadata(encountered_exc or call) -- if metadata is not None: -- operation.add_response_metadata(metadata) -+ raise e -diff --git a/google/cloud/bigtable/data/_helpers.py b/google/cloud/bigtable/data/_helpers.py -index e848ebc6..424a3448 100644 ---- a/google/cloud/bigtable/data/_helpers.py -+++ b/google/cloud/bigtable/data/_helpers.py -@@ -23,7 +23,6 @@ from collections import namedtuple - from google.cloud.bigtable.data.read_rows_query import ReadRowsQuery - - from google.api_core import exceptions as core_exceptions --from google.api_core.retry import exponential_sleep_generator - from google.api_core.retry import RetryFailureReason - from google.cloud.bigtable.data.exceptions import RetryExceptionGroup - -@@ -249,61 +248,3 @@ def _get_retryable_errors( - call_codes = table.default_mutate_rows_retryable_errors - - return [_get_error_type(e) for e in call_codes] -- -- --class TrackedBackoffGenerator: -- """ -- Generator class for exponential backoff sleep times. -- This implementation builds on top of api_core.retries.exponential_sleep_generator, -- adding the ability to retrieve previous values using get_attempt_backoff(idx). -- This is used by the Metrics class to track the sleep times used for each attempt. -- """ -- -- def __init__(self, initial=0.01, maximum=60, multiplier=2): -- self.history = [] -- self.subgenerator = exponential_sleep_generator( -- initial=initial, maximum=maximum, multiplier=multiplier -- ) -- self._next_override: float | None = None -- -- def __iter__(self): -- return self -- -- def set_next(self, next_value: float): -- """ -- Set the next backoff value, instead of generating one from subgenerator. -- After the value is yielded, it will go back to using self.subgenerator. -- -- If set_next is called twice before the next() is called, only the latest -- value will be used and others discarded -- -- Args: -- next_value: the upcomming value to yield when next() is called -- Raises: -- ValueError: if next_value is negative -- """ -- if next_value < 0: -- raise ValueError("backoff value cannot be less than 0") -- self._next_override = next_value -- -- def __next__(self) -> float: -- if self._next_override is not None: -- next_backoff = self._next_override -- self._next_override = None -- else: -- next_backoff = next(self.subgenerator) -- self.history.append(next_backoff) -- return next_backoff -- -- def get_attempt_backoff(self, attempt_idx) -> float: -- """ -- returns the backoff time for a specific attempt index, starting at 0. -- -- Args: -- attempt_idx: the index of the attempt to return backoff for -- Raises: -- IndexError: if attempt_idx is negative, or not in history -- """ -- if attempt_idx < 0: -- raise IndexError("received negative attempt number") -- return self.history[attempt_idx] -diff --git a/google/cloud/bigtable/data/_metrics/__init__.py b/google/cloud/bigtable/data/_metrics/__init__.py -deleted file mode 100644 -index 26cfc132..00000000 ---- a/google/cloud/bigtable/data/_metrics/__init__.py -+++ /dev/null -@@ -1,35 +0,0 @@ --# Copyright 2023 Google LLC --# --# 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.bigtable.data._metrics.metrics_controller import ( -- BigtableClientSideMetricsController, --) -- --from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric --from google.cloud.bigtable.data._metrics.data_model import ActiveAttemptMetric --from google.cloud.bigtable.data._metrics.data_model import CompletedOperationMetric --from google.cloud.bigtable.data._metrics.data_model import CompletedAttemptMetric --from google.cloud.bigtable.data._metrics.data_model import OperationState --from google.cloud.bigtable.data._metrics.data_model import OperationType --from google.cloud.bigtable.data._metrics.tracked_retry import tracked_retry -- --__all__ = ( -- "BigtableClientSideMetricsController", -- "OperationType", -- "OperationState", -- "ActiveOperationMetric", -- "ActiveAttemptMetric", -- "CompletedOperationMetric", -- "CompletedAttemptMetric", -- "tracked_retry", --) -diff --git a/google/cloud/bigtable/data/_metrics/data_model.py b/google/cloud/bigtable/data/_metrics/data_model.py -deleted file mode 100644 -index 64dd63bf..00000000 ---- a/google/cloud/bigtable/data/_metrics/data_model.py -+++ /dev/null -@@ -1,469 +0,0 @@ --# Copyright 2023 Google LLC --# --# 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 __future__ import annotations -- --from typing import ClassVar, Tuple, cast, TYPE_CHECKING -- --import time --import re --import logging --import contextvars -- --from enum import Enum --from functools import lru_cache --from dataclasses import dataclass --from dataclasses import field --from grpc import StatusCode --from grpc import RpcError --from grpc.aio import AioRpcError -- --import google.cloud.bigtable.data.exceptions as bt_exceptions --from google.cloud.bigtable_v2.types.response_params import ResponseParams --from google.cloud.bigtable.data._helpers import TrackedBackoffGenerator --from google.protobuf.message import DecodeError -- --if TYPE_CHECKING: -- from google.cloud.bigtable.data._metrics.handlers._base import MetricsHandler -- -- --LOGGER = logging.getLogger(__name__) -- --# default values for zone and cluster data, if not captured --DEFAULT_ZONE = "global" --DEFAULT_CLUSTER_ID = "" -- --# keys for parsing metadata blobs --BIGTABLE_LOCATION_METADATA_KEY = "x-goog-ext-425905942-bin" --SERVER_TIMING_METADATA_KEY = "server-timing" --SERVER_TIMING_REGEX = re.compile(r".*gfet4t7;\s*dur=(\d+\.?\d*).*") -- --INVALID_STATE_ERROR = "Invalid state for {}: {}" -- -- --class OperationType(Enum): -- """Enum for the type of operation being performed.""" -- -- READ_ROWS = "ReadRows" -- SAMPLE_ROW_KEYS = "SampleRowKeys" -- BULK_MUTATE_ROWS = "MutateRows" -- MUTATE_ROW = "MutateRow" -- CHECK_AND_MUTATE = "CheckAndMutateRow" -- READ_MODIFY_WRITE = "ReadModifyWriteRow" -- -- --class OperationState(Enum): -- """Enum for the state of the active operation. -- -- ┌───────────┐ -- │ CREATED │────────┐ -- └─────┬─────┘ │ -- │ │ -- ▼ │ -- ┌▶ ACTIVE_ATTEMPT ───┐│ -- │ │ ││ -- │ ▼ ││ -- └─ BETWEEN_ATTEMPTS ││ -- │ ││ -- ▼ ││ -- ┌───────────┐ ││ -- │ COMPLETED │ ◀─────┘│ -- └───────────┘ ◀──────┘ -- """ -- -- CREATED = 0 -- ACTIVE_ATTEMPT = 1 -- BETWEEN_ATTEMPTS = 2 -- COMPLETED = 3 -- -- --@dataclass(frozen=True) --class CompletedAttemptMetric: -- """ -- An immutable dataclass representing the data associated with a -- completed rpc attempt. -- -- Operation-level fields (eg. type, cluster, zone) are stored on the -- corresponding CompletedOperationMetric or ActiveOperationMetric object. -- """ -- -- duration_ns: int -- end_status: StatusCode -- gfe_latency_ns: int | None = None -- application_blocking_time_ns: int = 0 -- backoff_before_attempt_ns: int = 0 -- -- --@dataclass(frozen=True) --class CompletedOperationMetric: -- """ -- An immutable dataclass representing the data associated with a -- completed rpc operation. -- -- Attempt-level fields (eg. duration, latencies, etc) are stored on the -- corresponding CompletedAttemptMetric object. -- """ -- -- op_type: OperationType -- duration_ns: int -- completed_attempts: list[CompletedAttemptMetric] -- final_status: StatusCode -- cluster_id: str -- zone: str -- is_streaming: bool -- first_response_latency_ns: int | None = None -- flow_throttling_time_ns: int = 0 -- -- --@dataclass --class ActiveAttemptMetric: -- """ -- A dataclass representing the data associated with an rpc attempt that is -- currently in progress. Fields are mutable and may be optional. -- """ -- -- # keep monotonic timestamps for active attempts -- start_time_ns: int = field(default_factory=lambda: time.monotonic_ns()) -- # the time taken by the backend, in nanoseconds. Taken from response header -- gfe_latency_ns: int | None = None -- # time waiting on user to process the response, in nanoseconds -- # currently only relevant for ReadRows -- application_blocking_time_ns: int = 0 -- # backoff time is added to application_blocking_time_ns -- backoff_before_attempt_ns: int = 0 -- -- --@dataclass --class ActiveOperationMetric: -- """ -- A dataclass representing the data associated with an rpc operation that is -- currently in progress. Fields are mutable and may be optional. -- """ -- -- op_type: OperationType -- state: OperationState = OperationState.CREATED -- # create a default backoff generator, initialized with standard default backoff values -- backoff_generator: TrackedBackoffGenerator = field( -- default_factory=lambda: TrackedBackoffGenerator( -- initial=0.01, maximum=60, multiplier=2 -- ) -- ) -- # keep monotonic timestamps for active operations -- start_time_ns: int = field(default_factory=lambda: time.monotonic_ns()) -- active_attempt: ActiveAttemptMetric | None = None -- cluster_id: str | None = None -- zone: str | None = None -- completed_attempts: list[CompletedAttemptMetric] = field(default_factory=list) -- is_streaming: bool = False # only True for read_rows operations -- handlers: list[MetricsHandler] = field(default_factory=list) -- # the time it takes to recieve the first response from the server, in nanoseconds -- # attached by interceptor -- # currently only tracked for ReadRows -- first_response_latency_ns: int | None = None -- # time waiting on flow control, in nanoseconds -- flow_throttling_time_ns: int = 0 -- -- _active_operation_context: ClassVar[ -- contextvars.ContextVar[ActiveOperationMetric] -- ] = contextvars.ContextVar("active_operation_context") -- -- @classmethod -- def from_context(cls) -> ActiveOperationMetric | None: -- """Retrieves the active operation from the current execution context. -- -- Because execution within a context is sequential, this guarantees -- retrieval of the single, unique operation, isolated from other -- concurrent RPCs. -- -- Note: -- This is intended to be called by gRPC interceptors at the start -- of an RPC. -- -- Returns: -- ActiveOperationMetric: The current active operation. -- None: If no operation is set, or if the current operation is -- already in the `COMPLETED` state. -- """ -- op = cls._active_operation_context.get(None) -- if op and op.state == OperationState.COMPLETED: -- return None -- return op -- -- def __post_init__(self): -- """ -- Save new instances to contextvars on init -- """ -- self._active_operation_context.set(self) -- -- def start(self) -> None: -- """ -- Optionally called to mark the start of the operation. If not called, -- the operation will be started at initialization. -- -- StartState: CREATED -- EndState: CREATED -- """ -- if self.state != OperationState.CREATED: -- return self._handle_error(INVALID_STATE_ERROR.format("start", self.state)) -- self.start_time_ns = time.monotonic_ns() -- # set as active operation in contextvars -- self._active_operation_context.set(self) -- -- def start_attempt(self) -> ActiveAttemptMetric | None: -- """ -- Called to initiate a new attempt for the operation. -- -- StartState: CREATED | BETWEEN_ATTEMPTS -- EndState: ACTIVE_ATTEMPT -- """ -- if ( -- self.state != OperationState.BETWEEN_ATTEMPTS -- and self.state != OperationState.CREATED -- ): -- return self._handle_error( -- INVALID_STATE_ERROR.format("start_attempt", self.state) -- ) -- # set as active operation in contextvars -- self._active_operation_context.set(self) -- -- try: -- # find backoff value before this attempt -- prev_attempt_idx = len(self.completed_attempts) - 1 -- backoff = self.backoff_generator.get_attempt_backoff(prev_attempt_idx) -- # generator will return the backoff time in seconds, so convert to nanoseconds -- backoff_ns = int(backoff * 1e9) -- except IndexError: -- # backoff value not found -- backoff_ns = 0 -- -- self.active_attempt = ActiveAttemptMetric(backoff_before_attempt_ns=backoff_ns) -- self.state = OperationState.ACTIVE_ATTEMPT -- return self.active_attempt -- -- def add_response_metadata(self, metadata: dict[str, bytes | str]) -> None: -- """ -- Attach trailing metadata to the active attempt. -- -- If not called, default values for the metadata will be used. -- -- StartState: ACTIVE_ATTEMPT -- EndState: ACTIVE_ATTEMPT -- -- Args: -- - metadata: the metadata as extracted from the grpc call -- """ -- if self.state != OperationState.ACTIVE_ATTEMPT: -- return self._handle_error( -- INVALID_STATE_ERROR.format("add_response_metadata", self.state) -- ) -- if self.cluster_id is None or self.zone is None: -- # BIGTABLE_LOCATION_METADATA_KEY should give a binary-encoded ResponseParams proto -- blob = cast(bytes, metadata.get(BIGTABLE_LOCATION_METADATA_KEY)) -- if blob: -- parse_result = self._parse_response_metadata_blob(blob) -- if parse_result is not None: -- cluster, zone = parse_result -- if cluster: -- self.cluster_id = cluster -- if zone: -- self.zone = zone -- else: -- self._handle_error( -- f"Failed to decode {BIGTABLE_LOCATION_METADATA_KEY} metadata: {blob!r}" -- ) -- # SERVER_TIMING_METADATA_KEY should give a string with the server-latency headers -- timing_header = cast(str, metadata.get(SERVER_TIMING_METADATA_KEY)) -- if timing_header: -- timing_data = SERVER_TIMING_REGEX.match(timing_header) -- if timing_data and self.active_attempt: -- gfe_latency_ms = float(timing_data.group(1)) -- self.active_attempt.gfe_latency_ns = int(gfe_latency_ms * 1e6) -- -- @staticmethod -- @lru_cache(maxsize=32) -- def _parse_response_metadata_blob(blob: bytes) -> Tuple[str, str] | None: -- """ -- Parse the response metadata blob and return a tuple of cluster and zone. -- -- Function is cached to avoid parsing the same blob multiple times. -- -- Args: -- - blob: the metadata blob as extracted from the grpc call -- Returns: -- - a tuple of cluster_id and zone, or None if parsing failed -- """ -- try: -- proto = ResponseParams.pb().FromString(blob) -- return proto.cluster_id, proto.zone_id -- except (DecodeError, TypeError): -- # failed to parse metadata -- return None -- -- def end_attempt_with_status(self, status: StatusCode | BaseException) -> None: -- """ -- Called to mark the end of an attempt for the operation. -- -- Typically, this is used to mark a retryable error. If a retry will not -- be attempted, `end_with_status` or `end_with_success` should be used -- to finalize the operation along with the attempt. -- -- StartState: ACTIVE_ATTEMPT -- EndState: BETWEEN_ATTEMPTS -- -- Args: -- - status: The status of the attempt. -- """ -- if self.state != OperationState.ACTIVE_ATTEMPT or self.active_attempt is None: -- return self._handle_error( -- INVALID_STATE_ERROR.format("end_attempt_with_status", self.state) -- ) -- if isinstance(status, BaseException): -- status = self._exc_to_status(status) -- duration_ns = self._ensure_positive( -- time.monotonic_ns() - self.active_attempt.start_time_ns, "duration" -- ) -- complete_attempt = CompletedAttemptMetric( -- duration_ns=duration_ns, -- end_status=status, -- gfe_latency_ns=self.active_attempt.gfe_latency_ns, -- application_blocking_time_ns=self.active_attempt.application_blocking_time_ns, -- backoff_before_attempt_ns=self.active_attempt.backoff_before_attempt_ns, -- ) -- self.completed_attempts.append(complete_attempt) -- self.active_attempt = None -- self.state = OperationState.BETWEEN_ATTEMPTS -- for handler in self.handlers: -- handler.on_attempt_complete(complete_attempt, self) -- -- def end_with_status(self, status: StatusCode | BaseException) -> None: -- """ -- Called to mark the end of the operation. If there is an active attempt, -- end_attempt_with_status will be called with the same status. -- -- StartState: CREATED | ACTIVE_ATTEMPT | BETWEEN_ATTEMPTS -- EndState: COMPLETED -- -- Causes on_operation_completed to be called for each registered handler. -- -- Args: -- - status: The status of the operation. -- """ -- if self.state == OperationState.COMPLETED: -- return self._handle_error( -- INVALID_STATE_ERROR.format("end_with_status", self.state) -- ) -- final_status = ( -- self._exc_to_status(status) if isinstance(status, BaseException) else status -- ) -- if self.state == OperationState.ACTIVE_ATTEMPT: -- self.end_attempt_with_status(final_status) -- duration_ns = self._ensure_positive( -- time.monotonic_ns() - self.start_time_ns, "duration" -- ) -- finalized = CompletedOperationMetric( -- op_type=self.op_type, -- completed_attempts=self.completed_attempts, -- duration_ns=duration_ns, -- final_status=final_status, -- cluster_id=self.cluster_id or DEFAULT_CLUSTER_ID, -- zone=self.zone or DEFAULT_ZONE, -- is_streaming=self.is_streaming, -- first_response_latency_ns=self.first_response_latency_ns, -- flow_throttling_time_ns=self.flow_throttling_time_ns, -- ) -- self.state = OperationState.COMPLETED -- for handler in self.handlers: -- handler.on_operation_complete(finalized) -- -- def end_with_success(self): -- """ -- Called to mark the end of the operation with a successful status. -- -- StartState: CREATED | ACTIVE_ATTEMPT | BETWEEN_ATTEMPTS -- EndState: COMPLETED -- -- Causes on_operation_completed to be called for each registered handler. -- """ -- return self.end_with_status(StatusCode.OK) -- -- @staticmethod -- def _exc_to_status(exc: BaseException) -> StatusCode: -- """ -- Extracts the grpc status code from an exception. -- -- Exception groups and wrappers will be parsed to find the underlying -- grpc Exception. -- -- If the exception is not a grpc exception, will return StatusCode.UNKNOWN. -- -- Args: -- - exc: The exception to extract the status code from. -- """ -- if isinstance(exc, bt_exceptions._BigtableExceptionGroup): -- exc = exc.exceptions[-1] -- if hasattr(exc, "grpc_status_code") and exc.grpc_status_code is not None: -- return exc.grpc_status_code -- if ( -- exc.__cause__ -- and hasattr(exc.__cause__, "grpc_status_code") -- and exc.__cause__.grpc_status_code is not None -- ): -- return exc.__cause__.grpc_status_code -- if isinstance(exc, AioRpcError) or isinstance(exc, RpcError): -- return exc.code() -- return StatusCode.UNKNOWN -- -- @staticmethod -- def _handle_error(message: str) -> None: -- """ -- log error metric system error messages -- -- Args: -- - message: The message to include in the exception or warning. -- """ -- full_message = f"Error in Bigtable Metrics: {message}" -- LOGGER.warning(full_message) -- -- def _ensure_positive(self, value: int, field_name: str) -> int: -- """ -- Helper to replace negative value with 0, and record an error -- """ -- if value < 0: -- self._handle_error(f"received negative value for {field_name}: {value}") -- return 0 -- return value -- -- def __enter__(self): -- """ -- Implements the async manager protocol -- -- Using the operation's context manager provides assurances that the operation -- is always closed when complete, with the proper status code automaticallty -- detected when an exception is raised. -- """ -- return self -- -- def __exit__(self, exc_type, exc_val, exc_tb): -- """ -- Implements the context manager protocol -- -- The operation is automatically ended on exit, with the status determined -- by the exception type and value. -- -- If operation was already ended manually, do nothing. -- """ -- if not self.state == OperationState.COMPLETED: -- if exc_val is None: -- self.end_with_success() -- else: -- self.end_with_status(exc_val) -diff --git a/google/cloud/bigtable/data/_metrics/handlers/_base.py b/google/cloud/bigtable/data/_metrics/handlers/_base.py -deleted file mode 100644 -index 884091fd..00000000 ---- a/google/cloud/bigtable/data/_metrics/handlers/_base.py -+++ /dev/null -@@ -1,38 +0,0 @@ --# Copyright 2023 Google LLC --# --# 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.bigtable.data._metrics.data_model import ActiveOperationMetric --from google.cloud.bigtable.data._metrics.data_model import CompletedAttemptMetric --from google.cloud.bigtable.data._metrics.data_model import CompletedOperationMetric -- -- --class MetricsHandler: -- """ -- Base class for all metrics handlers. Metrics handlers will receive callbacks -- when operations and attempts are completed, and can use this information to -- update some external metrics system. -- """ -- -- def __init__(self, **kwargs): -- pass -- -- def on_operation_complete(self, op: CompletedOperationMetric) -> None: -- pass -- -- def on_attempt_complete( -- self, attempt: CompletedAttemptMetric, op: ActiveOperationMetric -- ) -> None: -- pass -- -- def close(self): -- pass -diff --git a/google/cloud/bigtable/data/_metrics/metrics_controller.py b/google/cloud/bigtable/data/_metrics/metrics_controller.py -deleted file mode 100644 -index e9815f20..00000000 ---- a/google/cloud/bigtable/data/_metrics/metrics_controller.py -+++ /dev/null -@@ -1,63 +0,0 @@ --# Copyright 2023 Google LLC --# --# 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 __future__ import annotations -- --from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric --from google.cloud.bigtable.data._metrics.handlers._base import MetricsHandler --from google.cloud.bigtable.data._metrics.data_model import OperationType -- -- --class BigtableClientSideMetricsController: -- """ -- BigtableClientSideMetricsController is responsible for managing the -- lifecycle of the metrics system. The Bigtable client library will -- use this class to create new operations. Each operation will be -- registered with the handlers associated with this controller. -- """ -- -- def __init__( -- self, -- handlers: list[MetricsHandler] | None = None, -- ): -- """ -- Initializes the metrics controller. -- -- Args: -- - handlers: A list of MetricsHandler objects to subscribe to metrics events. -- """ -- self.handlers: list[MetricsHandler] = handlers or [] -- -- def add_handler(self, handler: MetricsHandler) -> None: -- """ -- Add a new handler to the list of handlers. -- -- Args: -- - handler: A MetricsHandler object to add to the list of subscribed handlers. -- """ -- self.handlers.append(handler) -- -- def create_operation( -- self, op_type: OperationType, **kwargs -- ) -> ActiveOperationMetric: -- """ -- Creates a new operation and registers it with the subscribed handlers. -- """ -- return ActiveOperationMetric(op_type, **kwargs, handlers=self.handlers) -- -- def close(self): -- """ -- Close all handlers. -- """ -- for handler in self.handlers: -- handler.close() -diff --git a/google/cloud/bigtable/data/_metrics/tracked_retry.py b/google/cloud/bigtable/data/_metrics/tracked_retry.py -deleted file mode 100644 -index 94d2e5dc..00000000 ---- a/google/cloud/bigtable/data/_metrics/tracked_retry.py -+++ /dev/null -@@ -1,133 +0,0 @@ --# Copyright 2025 Google LLC --# --# 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. --""" --Methods for instrumenting an google.api_core.retry.retry_target or --google.api_core.retry.retry_target_stream method -- --`tracked_retry` will intercept `on_error` and `exception_factory` --methods to update the associated ActiveOperationMetric when exceptions --are encountered through the retryable rpc. --""" --from __future__ import annotations -- --from typing import Callable, List, Optional, Tuple, TypeVar -- --from grpc import StatusCode --from google.api_core.exceptions import GoogleAPICallError --from google.api_core.retry import RetryFailureReason --from google.cloud.bigtable.data.exceptions import _MutateRowsIncomplete --from google.cloud.bigtable.data._helpers import _retry_exception_factory --from google.cloud.bigtable.data._metrics import ActiveOperationMetric --from google.cloud.bigtable.data._metrics import OperationState -- -- --T = TypeVar("T") -- -- --ExceptionFactoryType = Callable[ -- [List[Exception], RetryFailureReason, Optional[float]], -- Tuple[Exception, Optional[Exception]], --] -- -- --def _track_retryable_error( -- operation: ActiveOperationMetric, --) -> Callable[[Exception], None]: -- """ -- Used as input to api_core.Retry classes, to track when retryable errors are encountered -- -- Should be passed as on_error callback -- """ -- -- def wrapper(exc: Exception) -> None: -- try: -- # record metadata from failed rpc -- if isinstance(exc, GoogleAPICallError) and exc.errors: -- rpc_error = exc.errors[-1] -- metadata = list(rpc_error.trailing_metadata()) + list( -- rpc_error.initial_metadata() -- ) -- operation.add_response_metadata({k: v for k, v in metadata}) -- except Exception: -- # ignore errors in metadata collection -- pass -- if isinstance(exc, _MutateRowsIncomplete): -- # _MutateRowsIncomplete represents a successful rpc with some failed mutations -- # mark the attempt as successful -- operation.end_attempt_with_status(StatusCode.OK) -- else: -- operation.end_attempt_with_status(exc) -- -- return wrapper -- -- --def _track_terminal_error( -- operation: ActiveOperationMetric, exception_factory: ExceptionFactoryType --) -> ExceptionFactoryType: -- """ -- Used as input to api_core.Retry classes, to track when terminal errors are encountered -- -- Should be used as a wrapper over an exception_factory callback -- """ -- -- def wrapper( -- exc_list: List[Exception], -- reason: RetryFailureReason, -- timeout_val: float | None, -- ) -> tuple[Exception, Exception | None]: -- source_exc, cause_exc = exception_factory(exc_list, reason, timeout_val) -- try: -- # record metadata from failed rpc -- if isinstance(source_exc, GoogleAPICallError) and source_exc.errors: -- rpc_error = source_exc.errors[-1] -- metadata = list(rpc_error.trailing_metadata()) + list( -- rpc_error.initial_metadata() -- ) -- operation.add_response_metadata({k: v for k, v in metadata}) -- except Exception: -- # ignore errors in metadata collection -- pass -- if ( -- reason == RetryFailureReason.TIMEOUT -- and operation.state == OperationState.ACTIVE_ATTEMPT -- and exc_list -- ): -- # record ending attempt for timeout failures -- attempt_exc = exc_list[-1] -- _track_retryable_error(operation)(attempt_exc) -- operation.end_with_status(source_exc) -- return source_exc, cause_exc -- -- return wrapper -- -- --def tracked_retry( -- *, -- retry_fn: Callable[..., T], -- operation: ActiveOperationMetric, -- **kwargs, --) -> T: -- """ -- Wrapper for retry_rarget or retry_target_stream, which injects methods to -- track the lifecycle of the retry using the provided ActiveOperationMetric -- """ -- in_exception_factory = kwargs.pop("exception_factory", _retry_exception_factory) -- kwargs.pop("on_error", None) -- kwargs.pop("sleep_generator", None) -- return retry_fn( -- sleep_generator=operation.backoff_generator, -- on_error=_track_retryable_error(operation), -- exception_factory=_track_terminal_error(operation, in_exception_factory), -- **kwargs, -- ) -diff --git a/google/cloud/bigtable/data/_sync_autogen/client.py b/google/cloud/bigtable/data/_sync_autogen/client.py -index 62200276..6a4da007 100644 ---- a/google/cloud/bigtable/data/_sync_autogen/client.py -+++ b/google/cloud/bigtable/data/_sync_autogen/client.py -@@ -75,7 +75,6 @@ from google.cloud.bigtable.data.row_filters import RowFilter - from google.cloud.bigtable.data.row_filters import StripValueTransformerFilter - from google.cloud.bigtable.data.row_filters import CellsRowLimitFilter - from google.cloud.bigtable.data.row_filters import RowFilterChain --from google.cloud.bigtable.data._metrics import BigtableClientSideMetricsController - from google.cloud.bigtable.data._cross_sync import CrossSync - from typing import Iterable - from grpc import insecure_channel -@@ -825,7 +824,6 @@ class _DataApiTarget(abc.ABC): - self.default_retryable_errors: Sequence[type[Exception]] = ( - default_retryable_errors or () - ) -- self._metrics = BigtableClientSideMetricsController() - try: - self._register_instance_future = CrossSync._Sync_Impl.create_task( - self.client._register_instance, -@@ -1483,7 +1481,6 @@ class _DataApiTarget(abc.ABC): - - def close(self): - """Called to close the Table instance and release any resources held by it.""" -- self._metrics.close() - if self._register_instance_future: - self._register_instance_future.cancel() - self.client._remove_instance_registration( -diff --git a/google/cloud/bigtable/data/_sync_autogen/metrics_interceptor.py b/google/cloud/bigtable/data/_sync_autogen/metrics_interceptor.py -index c5a59787..9e47313b 100644 ---- a/google/cloud/bigtable/data/_sync_autogen/metrics_interceptor.py -+++ b/google/cloud/bigtable/data/_sync_autogen/metrics_interceptor.py -@@ -15,46 +15,10 @@ - # This file is automatically generated by CrossSync. Do not edit manually. - - from __future__ import annotations --from typing import Sequence --import time --from functools import wraps --from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric --from google.cloud.bigtable.data._metrics.data_model import OperationState --from google.cloud.bigtable.data._metrics.data_model import OperationType - from grpc import UnaryUnaryClientInterceptor - from grpc import UnaryStreamClientInterceptor - - --def _with_active_operation(func): -- """Decorator for interceptor methods to extract the active operation associated with the -- in-scope contextvars, and pass it to the decorated function.""" -- -- @wraps(func) -- def wrapper(self, continuation, client_call_details, request): -- operation: ActiveOperationMetric | None = ActiveOperationMetric.from_context() -- if operation: -- if ( -- operation.state == OperationState.CREATED -- or operation.state == OperationState.BETWEEN_ATTEMPTS -- ): -- operation.start_attempt() -- return func(self, operation, continuation, client_call_details, request) -- else: -- return continuation(client_call_details, request) -- -- return wrapper -- -- --def _get_metadata(source) -> dict[str, str | bytes] | None: -- """Helper to extract metadata from a call or RpcError""" -- try: -- metadata: Sequence[tuple[str, str | bytes]] -- metadata = source.trailing_metadata() + source.initial_metadata() -- return {k: v for (k, v) in metadata} -- except Exception: -- return None -- -- - class BigtableMetricsInterceptor( - UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor - ): -@@ -62,65 +26,34 @@ class BigtableMetricsInterceptor( - An async gRPC interceptor to add client metadata and print server metadata. - """ - -- @_with_active_operation -- def intercept_unary_unary( -- self, operation, continuation, client_call_details, request -- ): -+ def intercept_unary_unary(self, continuation, client_call_details, request): - """Interceptor for unary rpcs: - - MutateRow - - CheckAndMutateRow - - ReadModifyWriteRow""" -- metadata = None - try: - call = continuation(client_call_details, request) -- metadata = _get_metadata(call) - return call - except Exception as rpc_error: -- metadata = _get_metadata(rpc_error) - raise rpc_error -- finally: -- if metadata is not None: -- operation.add_response_metadata(metadata) - -- @_with_active_operation -- def intercept_unary_stream( -- self, operation, continuation, client_call_details, request -- ): -+ def intercept_unary_stream(self, continuation, client_call_details, request): - """Interceptor for streaming rpcs: - - ReadRows - - MutateRows - - SampleRowKeys""" - try: - return self._streaming_generator_wrapper( -- operation, continuation(client_call_details, request) -+ continuation(client_call_details, request) - ) - except Exception as rpc_error: -- metadata = _get_metadata(rpc_error) -- if metadata is not None: -- operation.add_response_metadata(metadata) - raise rpc_error - - @staticmethod -- def _streaming_generator_wrapper(operation, call): -+ def _streaming_generator_wrapper(call): - """Wrapped generator to be returned by intercept_unary_stream.""" -- has_first_response = ( -- operation.first_response_latency_ns is not None -- or operation.op_type != OperationType.READ_ROWS -- ) -- encountered_exc = None - try: - for response in call: -- if not has_first_response: -- operation.first_response_latency_ns = ( -- time.monotonic_ns() - operation.start_time_ns -- ) -- has_first_response = True - yield response - except Exception as e: -- encountered_exc = e -- raise -- finally: -- if call is not None: -- metadata = _get_metadata(encountered_exc or call) -- if metadata is not None: -- operation.add_response_metadata(metadata) -+ raise e -diff --git a/noxfile.py b/noxfile.py -index 77f59b3c..29de5901 100644 ---- a/noxfile.py -+++ b/noxfile.py -@@ -517,7 +517,6 @@ def prerelease_deps(session, protobuf_implementation): - # Remaining dependencies - other_deps = [ - "requests", -- "cryptography", - ] - session.install(*other_deps) - -diff --git a/samples/hello/async_main.py b/samples/hello/async_main.py -index e134e28d..af95898e 100644 ---- a/samples/hello/async_main.py -+++ b/samples/hello/async_main.py -@@ -80,9 +80,6 @@ async def main(project_id, instance_id, table_id): - # sequential keys can result in poor distribution of operations - # across nodes. - # -- # We recommend that you use bytestrings directly for row keys -- # where possible, rather than encoding strings. -- # - # For more information about how to design a Bigtable schema for - # the best performance, see the documentation: - # -diff --git a/samples/hello/main.py b/samples/hello/main.py -index d3cf91ba..42fe7509 100644 ---- a/samples/hello/main.py -+++ b/samples/hello/main.py -@@ -81,9 +81,6 @@ def main(project_id, instance_id, table_id): - # sequential keys can result in poor distribution of operations - # across nodes. - # -- # We recommend that you use bytestrings directly for row keys -- # where possible, rather than encoding strings. -- # - # For more information about how to design a Bigtable schema for - # the best performance, see the documentation: - # -@@ -91,7 +88,7 @@ def main(project_id, instance_id, table_id): - row_key = f"greeting{i}".encode() - row = table.direct_row(row_key) - row.set_cell( -- column_family_id, column, value, timestamp=datetime.datetime.utcnow(), -+ column_family_id, column, value, timestamp=datetime.datetime.now(datetime.timezone.utc), - ) - rows.append(row) - table.mutate_rows(rows) -diff --git a/samples/snippets/writes/write_batch.py b/samples/snippets/writes/write_batch.py -index 8ad4b07a..d0e8d196 100644 ---- a/samples/snippets/writes/write_batch.py -+++ b/samples/snippets/writes/write_batch.py -@@ -25,7 +25,7 @@ def write_batch(project_id, instance_id, table_id): - table = instance.table(table_id) - - with MutationsBatcher(table=table) as batcher: -- timestamp = datetime.datetime.utcnow() -+ timestamp = datetime.datetime.now(datetime.timezone.utc) - column_family_id = "stats_summary" - - rows = [ -diff --git a/samples/snippets/writes/write_conditionally.py b/samples/snippets/writes/write_conditionally.py -index 7fb640aa..791dd0c3 100644 ---- a/samples/snippets/writes/write_conditionally.py -+++ b/samples/snippets/writes/write_conditionally.py -@@ -24,7 +24,7 @@ def write_conditional(project_id, instance_id, table_id): - instance = client.instance(instance_id) - table = instance.table(table_id) - -- timestamp = datetime.datetime.utcnow() -+ timestamp = datetime.datetime.now(datetime.timezone.utc) - column_family_id = "stats_summary" - - row_key = "phone#4c410523#20190501" -diff --git a/samples/snippets/writes/write_simple.py b/samples/snippets/writes/write_simple.py -index 1aa5a810..4ca56f8f 100644 ---- a/samples/snippets/writes/write_simple.py -+++ b/samples/snippets/writes/write_simple.py -@@ -24,7 +24,7 @@ def write_simple(project_id, instance_id, table_id): - instance = client.instance(instance_id) - table = instance.table(table_id) - -- timestamp = datetime.datetime.utcnow() -+ timestamp = datetime.datetime.now(datetime.timezone.utc) - column_family_id = "stats_summary" - - row_key = "phone#4c410523#20190501" -diff --git a/tests/system/v2_client/_helpers.py b/tests/system/v2_client/_helpers.py -index 95261879..e6fe1034 100644 ---- a/tests/system/v2_client/_helpers.py -+++ b/tests/system/v2_client/_helpers.py -@@ -17,7 +17,6 @@ import datetime - import grpc - from google.api_core import exceptions - from google.cloud import exceptions as core_exceptions --from google.cloud._helpers import UTC - from test_utils import retry - - -@@ -41,7 +40,7 @@ retry_grpc_unavailable = retry.RetryErrors( - - def label_stamp(): - return ( -- datetime.datetime.utcnow() -- .replace(microsecond=0, tzinfo=UTC) -+ datetime.datetime.now(datetime.timezone.utc) -+ .replace(microsecond=0) - .strftime("%Y-%m-%dt%H-%M-%S") - ) -diff --git a/tests/system/v2_client/test_data_api.py b/tests/system/v2_client/test_data_api.py -index 579837e3..b1563da1 100644 ---- a/tests/system/v2_client/test_data_api.py -+++ b/tests/system/v2_client/test_data_api.py -@@ -233,10 +233,9 @@ def test_table_read_row_large_cell(data_table, rows_to_delete, skip_on_emulator) - def _write_to_row(row1, row2, row3, row4): - from google.cloud._helpers import _datetime_from_microseconds - from google.cloud._helpers import _microseconds_from_datetime -- from google.cloud._helpers import UTC - from google.cloud.bigtable.row_data import Cell - -- timestamp1 = datetime.datetime.utcnow().replace(tzinfo=UTC) -+ timestamp1 = datetime.datetime.now(datetime.timezone.utc) - timestamp1_micros = _microseconds_from_datetime(timestamp1) - # Truncate to millisecond granularity. - timestamp1_micros -= timestamp1_micros % 1000 -diff --git a/tests/unit/data/_async/test_client.py b/tests/unit/data/_async/test_client.py -index 9f65d120..72b3ae73 100644 ---- a/tests/unit/data/_async/test_client.py -+++ b/tests/unit/data/_async/test_client.py -@@ -55,26 +55,18 @@ if CrossSync.is_async: - from google.cloud.bigtable.data._async._swappable_channel import ( - AsyncSwappableChannel, - ) -- from google.cloud.bigtable.data._async.metrics_interceptor import ( -- AsyncBigtableMetricsInterceptor, -- ) - - CrossSync.add_mapping("grpc_helpers", grpc_helpers_async) - CrossSync.add_mapping("SwappableChannel", AsyncSwappableChannel) -- CrossSync.add_mapping("MetricsInterceptor", AsyncBigtableMetricsInterceptor) - else: - from google.api_core import grpc_helpers - from google.cloud.bigtable.data._sync_autogen.client import Table # noqa: F401 - from google.cloud.bigtable.data._sync_autogen._swappable_channel import ( - SwappableChannel, - ) -- from google.cloud.bigtable.data._sync_autogen.metrics_interceptor import ( -- BigtableMetricsInterceptor, -- ) - - CrossSync.add_mapping("grpc_helpers", grpc_helpers) - CrossSync.add_mapping("SwappableChannel", SwappableChannel) -- CrossSync.add_mapping("MetricsInterceptor", BigtableMetricsInterceptor) - - __CROSS_SYNC_OUTPUT__ = "tests.unit.data._sync_autogen.test_client" - -@@ -122,7 +114,6 @@ class TestBigtableDataClientAsync: - assert not client._active_instances - assert client._channel_refresh_task is not None - assert client.transport._credentials == expected_credentials -- assert isinstance(client._metrics_interceptor, CrossSync.MetricsInterceptor) - await client.close() - - @CrossSync.pytest -@@ -1162,9 +1153,6 @@ class TestTableAsync: - @CrossSync.pytest - async def test_ctor(self): - from google.cloud.bigtable.data._helpers import _WarmedInstanceKey -- from google.cloud.bigtable.data._metrics import ( -- BigtableClientSideMetricsController, -- ) - - expected_table_id = "table-id" - expected_instance_id = "instance-id" -@@ -1206,7 +1194,6 @@ class TestTableAsync: - instance_key = _WarmedInstanceKey(table.instance_name, table.app_profile_id) - assert instance_key in client._active_instances - assert client._instance_owners[instance_key] == {id(table)} -- assert isinstance(table._metrics, BigtableClientSideMetricsController) - assert table.default_operation_timeout == expected_operation_timeout - assert table.default_attempt_timeout == expected_attempt_timeout - assert ( -@@ -1467,22 +1454,6 @@ class TestTableAsync: - # empty app_profile_id should send empty string - assert "app_profile_id=" in routing_str - -- @CrossSync.pytest -- async def test_close(self): -- client = self._make_client() -- table = self._make_one(client) -- with mock.patch.object( -- table._metrics, "close", mock.Mock() -- ) as metric_close_mock: -- with mock.patch.object( -- client, "_remove_instance_registration" -- ) as remove_mock: -- await table.close() -- remove_mock.assert_called_once_with( -- table.instance_id, table.app_profile_id, id(table) -- ) -- metric_close_mock.assert_called_once() -- - - @CrossSync.convert_class( - "TestAuthorizedView", add_mapping_for_name="TestAuthorizedView" -@@ -1513,9 +1484,6 @@ class TestAuthorizedViewsAsync(CrossSync.TestTable): - @CrossSync.pytest - async def test_ctor(self): - from google.cloud.bigtable.data._helpers import _WarmedInstanceKey -- from google.cloud.bigtable.data._metrics import ( -- BigtableClientSideMetricsController, -- ) - - expected_table_id = "table-id" - expected_instance_id = "instance-id" -@@ -1564,7 +1532,6 @@ class TestAuthorizedViewsAsync(CrossSync.TestTable): - instance_key = _WarmedInstanceKey(view.instance_name, view.app_profile_id) - assert instance_key in client._active_instances - assert client._instance_owners[instance_key] == {id(view)} -- assert isinstance(view._metrics, BigtableClientSideMetricsController) - assert view.default_operation_timeout == expected_operation_timeout - assert view.default_attempt_timeout == expected_attempt_timeout - assert ( -@@ -1778,8 +1745,9 @@ class TestReadRowsAsync: - @pytest.mark.parametrize( - "per_request_t, operation_t, expected_num", - [ -- (0.1, 0.19, 2), -- (0.1, 0.29, 3), -+ (0.05, 0.08, 2), -+ (0.05, 0.14, 3), -+ (0.05, 0.24, 5), - ], - ) - @CrossSync.pytest -diff --git a/tests/unit/data/_async/test_metrics_interceptor.py b/tests/unit/data/_async/test_metrics_interceptor.py -index 1593b8c9..6ea95835 100644 ---- a/tests/unit/data/_async/test_metrics_interceptor.py -+++ b/tests/unit/data/_async/test_metrics_interceptor.py -@@ -14,10 +14,7 @@ - - import pytest - from grpc import RpcError --from grpc import ClientCallDetails - --from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric --from google.cloud.bigtable.data._metrics.data_model import OperationState - from google.cloud.bigtable.data._cross_sync import CrossSync - - # try/except added for compatibility with python < 3.8 -@@ -70,267 +67,102 @@ class TestMetricsInterceptorAsync: - def _make_one(self, *args, **kwargs): - return self._get_target_class()(*args, **kwargs) - -- @CrossSync.pytest -- async def test_unary_unary_interceptor_op_not_found(self): -- """Test that interceptor call continuation if op is not found""" -- instance = self._make_one() -- continuation = CrossSync.Mock() -- details = ClientCallDetails() -- details.metadata = [] -- request = mock.Mock() -- await instance.intercept_unary_unary(continuation, details, request) -- continuation.assert_called_once_with(details, request) -- - @CrossSync.pytest - async def test_unary_unary_interceptor_success(self): - """Test that interceptor handles successful unary-unary calls""" - instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- ActiveOperationMetric._active_operation_context.set(op) - continuation = CrossSync.Mock() - call = continuation.return_value -- call.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")]) -- call.initial_metadata = CrossSync.Mock(return_value=[("c", "d")]) -- details = ClientCallDetails() -+ details = mock.Mock() - request = mock.Mock() - result = await instance.intercept_unary_unary(continuation, details, request) - assert result == call - continuation.assert_called_once_with(details, request) -- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) -- op.end_attempt_with_status.assert_not_called() - - @CrossSync.pytest - async def test_unary_unary_interceptor_failure(self): -- """test a failed RpcError with metadata""" -- instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- ActiveOperationMetric._active_operation_context.set(op) -- exc = RpcError("test") -- exc.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")]) -- exc.initial_metadata = CrossSync.Mock(return_value=[("c", "d")]) -- continuation = CrossSync.Mock(side_effect=exc) -- details = ClientCallDetails() -- request = mock.Mock() -- with pytest.raises(RpcError) as e: -- await instance.intercept_unary_unary(continuation, details, request) -- assert e.value == exc -- continuation.assert_called_once_with(details, request) -- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) -+ """Test a failed RpcError with metadata""" - -- @CrossSync.pytest -- async def test_unary_unary_interceptor_failure_no_metadata(self): -- """test with RpcError without without metadata attached""" - instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- ActiveOperationMetric._active_operation_context.set(op) - exc = RpcError("test") - continuation = CrossSync.Mock(side_effect=exc) -- call = continuation.return_value -- call.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")]) -- call.initial_metadata = CrossSync.Mock(return_value=[("c", "d")]) -- details = ClientCallDetails() -+ details = mock.Mock() - request = mock.Mock() - with pytest.raises(RpcError) as e: - await instance.intercept_unary_unary(continuation, details, request) - assert e.value == exc - continuation.assert_called_once_with(details, request) -- op.add_response_metadata.assert_not_called() - - @CrossSync.pytest - async def test_unary_unary_interceptor_failure_generic(self): -- """test generic exception""" -+ """Test generic exception""" -+ - instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- ActiveOperationMetric._active_operation_context.set(op) - exc = ValueError("test") - continuation = CrossSync.Mock(side_effect=exc) -- call = continuation.return_value -- call.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")]) -- call.initial_metadata = CrossSync.Mock(return_value=[("c", "d")]) -- details = ClientCallDetails() -+ details = mock.Mock() - request = mock.Mock() - with pytest.raises(ValueError) as e: - await instance.intercept_unary_unary(continuation, details, request) - assert e.value == exc - continuation.assert_called_once_with(details, request) -- op.add_response_metadata.assert_not_called() -- -- @CrossSync.pytest -- async def test_unary_stream_interceptor_op_not_found(self): -- """Test that interceptor calls continuation if op is not found""" -- instance = self._make_one() -- continuation = CrossSync.Mock() -- details = ClientCallDetails() -- details.metadata = [] -- request = mock.Mock() -- await instance.intercept_unary_stream(continuation, details, request) -- continuation.assert_called_once_with(details, request) - - @CrossSync.pytest - async def test_unary_stream_interceptor_success(self): - """Test that interceptor handles successful unary-stream calls""" -+ - instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- op.start_time_ns = 0 -- op.first_response_latency = None -- ActiveOperationMetric._active_operation_context.set(op) - - continuation = CrossSync.Mock(return_value=_make_mock_stream_call([1, 2])) -- call = continuation.return_value -- call.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")]) -- call.initial_metadata = CrossSync.Mock(return_value=[("c", "d")]) -- details = ClientCallDetails() -+ details = mock.Mock() - request = mock.Mock() - wrapper = await instance.intercept_unary_stream(continuation, details, request) - results = [val async for val in wrapper] - assert results == [1, 2] - continuation.assert_called_once_with(details, request) -- assert op.first_response_latency_ns is not None -- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) -- op.end_attempt_with_status.assert_not_called() - - @CrossSync.pytest - async def test_unary_stream_interceptor_failure_mid_stream(self): - """Test that interceptor handles failures mid-stream""" -- from grpc.aio import AioRpcError, Metadata -- - instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- op.start_time_ns = 0 -- op.first_response_latency = None -- ActiveOperationMetric._active_operation_context.set(op) -- exc = AioRpcError(0, Metadata(), Metadata(("a", "b"), ("c", "d"))) -+ exc = ValueError("test") - continuation = CrossSync.Mock(return_value=_make_mock_stream_call([1], exc=exc)) -- details = ClientCallDetails() -+ details = mock.Mock() - request = mock.Mock() - wrapper = await instance.intercept_unary_stream(continuation, details, request) -- with pytest.raises(AioRpcError) as e: -+ with pytest.raises(ValueError) as e: - [val async for val in wrapper] - assert e.value == exc - continuation.assert_called_once_with(details, request) -- assert op.first_response_latency_ns is not None -- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) - - @CrossSync.pytest - async def test_unary_stream_interceptor_failure_start_stream(self): - """Test that interceptor handles failures at start of stream with RpcError with metadata""" -- instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- op.start_time_ns = 0 -- op.first_response_latency = None -- ActiveOperationMetric._active_operation_context.set(op) -- exc = RpcError("test") -- exc.trailing_metadata = CrossSync.Mock(return_value=[("a", "b")]) -- exc.initial_metadata = CrossSync.Mock(return_value=[("c", "d")]) -- -- continuation = CrossSync.Mock() -- continuation.side_effect = exc -- details = ClientCallDetails() -- request = mock.Mock() -- with pytest.raises(RpcError) as e: -- await instance.intercept_unary_stream(continuation, details, request) -- assert e.value == exc -- continuation.assert_called_once_with(details, request) -- assert op.first_response_latency_ns is not None -- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) - -- @CrossSync.pytest -- async def test_unary_stream_interceptor_failure_start_stream_no_metadata(self): -- """Test that interceptor handles failures at start of stream with RpcError with no metadata""" - instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- op.start_time_ns = 0 -- op.first_response_latency = None -- ActiveOperationMetric._active_operation_context.set(op) - exc = RpcError("test") - - continuation = CrossSync.Mock() - continuation.side_effect = exc -- details = ClientCallDetails() -+ details = mock.Mock() - request = mock.Mock() - with pytest.raises(RpcError) as e: - await instance.intercept_unary_stream(continuation, details, request) - assert e.value == exc - continuation.assert_called_once_with(details, request) -- assert op.first_response_latency_ns is not None -- op.add_response_metadata.assert_not_called() - - @CrossSync.pytest - async def test_unary_stream_interceptor_failure_start_stream_generic(self): - """Test that interceptor handles failures at start of stream with generic exception""" -+ - instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- op.start_time_ns = 0 -- op.first_response_latency = None -- ActiveOperationMetric._active_operation_context.set(op) - exc = ValueError("test") - - continuation = CrossSync.Mock() - continuation.side_effect = exc -- details = ClientCallDetails() -+ details = mock.Mock() - request = mock.Mock() - with pytest.raises(ValueError) as e: - await instance.intercept_unary_stream(continuation, details, request) - assert e.value == exc - continuation.assert_called_once_with(details, request) -- assert op.first_response_latency_ns is not None -- op.add_response_metadata.assert_not_called() -- -- @CrossSync.pytest -- @pytest.mark.parametrize( -- "initial_state", [OperationState.CREATED, OperationState.BETWEEN_ATTEMPTS] -- ) -- async def test_unary_unary_interceptor_start_operation(self, initial_state): -- """if called with a newly created operation, it should be started""" -- instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = initial_state -- ActiveOperationMetric._active_operation_context.set(op) -- continuation = CrossSync.Mock() -- call = continuation.return_value -- call.trailing_metadata = CrossSync.Mock(return_value=[]) -- call.initial_metadata = CrossSync.Mock(return_value=[]) -- details = ClientCallDetails() -- request = mock.Mock() -- await instance.intercept_unary_unary(continuation, details, request) -- op.start_attempt.assert_called_once() -- -- @CrossSync.pytest -- @pytest.mark.parametrize( -- "initial_state", [OperationState.CREATED, OperationState.BETWEEN_ATTEMPTS] -- ) -- async def test_unary_stream_interceptor_start_operation(self, initial_state): -- """if called with a newly created operation, it should be started""" -- instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = initial_state -- ActiveOperationMetric._active_operation_context.set(op) -- -- continuation = CrossSync.Mock(return_value=_make_mock_stream_call([1, 2])) -- call = continuation.return_value -- call.trailing_metadata = CrossSync.Mock(return_value=[]) -- call.initial_metadata = CrossSync.Mock(return_value=[]) -- details = ClientCallDetails() -- request = mock.Mock() -- await instance.intercept_unary_stream(continuation, details, request) -- op.start_attempt.assert_called_once() -diff --git a/tests/unit/data/_metrics/__init__.py b/tests/unit/data/_metrics/__init__.py -deleted file mode 100644 -index e69de29b..00000000 -diff --git a/tests/unit/data/_metrics/test_data_model.py b/tests/unit/data/_metrics/test_data_model.py -deleted file mode 100644 -index 93e73c9d..00000000 ---- a/tests/unit/data/_metrics/test_data_model.py -+++ /dev/null -@@ -1,730 +0,0 @@ --# Copyright 2023 Google LLC --# --# 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 pytest --import mock -- --from google.cloud.bigtable.data._metrics.data_model import OperationState as State --from google.cloud.bigtable_v2.types import ResponseParams -- -- --class TestActiveOperationMetric: -- def _make_one(self, *args, **kwargs): -- from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric -- -- return ActiveOperationMetric(*args, **kwargs) -- -- @mock.patch("time.monotonic_ns") -- def test_ctor_defaults(self, mock_monotonic_ns): -- """ -- create an instance with default values -- """ -- expected_timestamp = 123456789 -- mock_monotonic_ns.return_value = expected_timestamp -- mock_type = mock.Mock() -- metric = self._make_one(mock_type) -- assert metric.op_type == mock_type -- assert metric.start_time_ns == expected_timestamp -- assert metric.active_attempt is None -- assert metric.cluster_id is None -- assert metric.zone is None -- assert len(metric.completed_attempts) == 0 -- assert len(metric.handlers) == 0 -- assert metric.is_streaming is False -- assert metric.flow_throttling_time_ns == 0 -- assert metric.state == State.CREATED -- -- def test_ctor_explicit(self): -- """ -- test with explicit arguments -- """ -- expected_type = mock.Mock() -- expected_start_time_ns = 7 -- expected_active_attempt = mock.Mock() -- expected_cluster_id = "cluster" -- expected_zone = "zone" -- expected_completed_attempts = [mock.Mock()] -- expected_state = State.COMPLETED -- expected_handlers = [mock.Mock()] -- expected_is_streaming = True -- expected_flow_throttling = 12 -- metric = self._make_one( -- op_type=expected_type, -- start_time_ns=expected_start_time_ns, -- active_attempt=expected_active_attempt, -- cluster_id=expected_cluster_id, -- zone=expected_zone, -- state=expected_state, -- completed_attempts=expected_completed_attempts, -- handlers=expected_handlers, -- is_streaming=expected_is_streaming, -- flow_throttling_time_ns=expected_flow_throttling, -- ) -- assert metric.op_type == expected_type -- assert metric.start_time_ns == expected_start_time_ns -- assert metric.active_attempt == expected_active_attempt -- assert metric.cluster_id == expected_cluster_id -- assert metric.zone == expected_zone -- assert metric.completed_attempts == expected_completed_attempts -- assert metric.state == expected_state -- assert metric.handlers == expected_handlers -- assert metric.is_streaming == expected_is_streaming -- assert metric.flow_throttling_time_ns == expected_flow_throttling -- -- def test_state_machine_w_methods(self): -- """ -- Exercise the state machine by calling methods to move between states -- """ -- metric = self._make_one(mock.Mock()) -- assert metric.state == State.CREATED -- metric.start() -- assert metric.state == State.CREATED -- metric.start_attempt() -- assert metric.state == State.ACTIVE_ATTEMPT -- metric.end_attempt_with_status(Exception()) -- assert metric.state == State.BETWEEN_ATTEMPTS -- metric.start_attempt() -- assert metric.state == State.ACTIVE_ATTEMPT -- metric.end_with_success() -- assert metric.state == State.COMPLETED -- -- def test_state_machine(self): -- """ -- Exercise state machine by moving through states -- """ -- metric = self._make_one(mock.Mock()) -- assert metric.state == State.CREATED -- metric.start_attempt() -- assert metric.state == State.ACTIVE_ATTEMPT -- metric.end_attempt_with_status(0) -- assert metric.state == State.BETWEEN_ATTEMPTS -- metric.end_with_success() -- assert metric.state == State.COMPLETED -- -- @pytest.mark.parametrize( -- "method,args,valid_states,error_method_name", -- [ -- ("start", (), (State.CREATED,), None), -- ("start_attempt", (), (State.CREATED, State.BETWEEN_ATTEMPTS), None), -- ("add_response_metadata", ({},), (State.ACTIVE_ATTEMPT,), None), -- ("end_attempt_with_status", (mock.Mock(),), (State.ACTIVE_ATTEMPT,), None), -- ( -- "end_with_status", -- (mock.Mock(),), -- ( -- State.CREATED, -- State.ACTIVE_ATTEMPT, -- State.BETWEEN_ATTEMPTS, -- ), -- None, -- ), -- ( -- "end_with_success", -- (), -- ( -- State.CREATED, -- State.ACTIVE_ATTEMPT, -- State.BETWEEN_ATTEMPTS, -- ), -- "end_with_status", -- ), -- ], -- ids=lambda x: x if isinstance(x, str) else "", -- ) -- def test_error_invalid_states(self, method, args, valid_states, error_method_name): -- """ -- each method only works for certain states. Make sure _handle_error is called for invalid states -- """ -- cls = type(self._make_one(mock.Mock())) -- invalid_states = set(State) - set(valid_states) -- error_method_name = error_method_name or method -- for state in invalid_states: -- with mock.patch.object(cls, "_handle_error") as mock_handle_error: -- mock_handle_error.return_value = None -- metric = self._make_one(mock.Mock(), state=state) -- return_obj = getattr(metric, method)(*args) -- assert return_obj is None -- assert mock_handle_error.call_count == 1 -- assert ( -- mock_handle_error.call_args[0][0] -- == f"Invalid state for {error_method_name}: {state}" -- ) -- -- @mock.patch("time.monotonic_ns") -- def test_start(self, mock_monotonic_ns): -- """ -- calling start op operation should reset start_time -- """ -- expected_timestamp = 123456789 -- mock_monotonic_ns.return_value = expected_timestamp -- orig_time = 0 -- metric = self._make_one(mock.Mock(), start_time_ns=orig_time) -- assert metric.start_time_ns == 0 -- metric.start() -- assert metric.start_time_ns != orig_time -- assert metric.start_time_ns == expected_timestamp -- # should remain in CREATED state after completing -- assert metric.state == State.CREATED -- -- @mock.patch("time.monotonic_ns") -- def test_start_attempt(self, mock_monotonic_ns): -- """ -- calling start_attempt should create a new emptu atempt metric -- """ -- from google.cloud.bigtable.data._metrics.data_model import ActiveAttemptMetric -- -- expected_timestamp = 123456789 -- mock_monotonic_ns.return_value = expected_timestamp -- metric = self._make_one(mock.Mock()) -- assert metric.active_attempt is None -- metric.start_attempt() -- assert isinstance(metric.active_attempt, ActiveAttemptMetric) -- # make sure it was initialized with the correct values -- assert metric.active_attempt.start_time_ns == expected_timestamp -- assert metric.active_attempt.gfe_latency_ns is None -- # should be in ACTIVE_ATTEMPT state after completing -- assert metric.state == State.ACTIVE_ATTEMPT -- -- def test_start_attempt_with_backoff_generator(self): -- """ -- If operation has a backoff generator, it should be used to attach backoff -- times to attempts -- """ -- from google.cloud.bigtable.data._helpers import TrackedBackoffGenerator -- -- generator = TrackedBackoffGenerator() -- # pre-seed generator with exepcted values -- generator.history = list(range(10)) -- metric = self._make_one(mock.Mock(), backoff_generator=generator) -- metric.start_attempt() -- assert len(metric.completed_attempts) == 0 -- # first attempt should always be 0 -- assert metric.active_attempt.backoff_before_attempt_ns == 0 -- # later attempts should have their attempt number as backoff time -- for i in range(10): -- metric.end_attempt_with_status(mock.Mock()) -- assert len(metric.completed_attempts) == i + 1 -- metric.start_attempt() -- # expect the backoff to be converted froms seconds to ns -- assert metric.active_attempt.backoff_before_attempt_ns == (i * 1e9) -- -- @pytest.mark.parametrize( -- "start_cluster,start_zone,metadata_proto,end_cluster,end_zone", -- [ -- (None, None, None, None, None), -- ("orig_cluster", "orig_zone", None, "orig_cluster", "orig_zone"), -- (None, None, ResponseParams(), None, None), -- ( -- "orig_cluster", -- "orig_zone", -- ResponseParams(), -- "orig_cluster", -- "orig_zone", -- ), -- ( -- None, -- None, -- ResponseParams(cluster_id="test-cluster", zone_id="us-central1-b"), -- "test-cluster", -- "us-central1-b", -- ), -- ( -- None, -- "filled", -- ResponseParams(cluster_id="cluster", zone_id="zone"), -- "cluster", -- "zone", -- ), -- (None, "filled", ResponseParams(cluster_id="cluster"), "cluster", "filled"), -- (None, "filled", ResponseParams(zone_id="zone"), None, "zone"), -- ( -- "filled", -- None, -- ResponseParams(cluster_id="cluster", zone_id="zone"), -- "cluster", -- "zone", -- ), -- ("filled", None, ResponseParams(cluster_id="cluster"), "cluster", None), -- ("filled", None, ResponseParams(zone_id="zone"), "filled", "zone"), -- ], -- ) -- def test_add_response_metadata_cbt_header( -- self, start_cluster, start_zone, metadata_proto, end_cluster, end_zone -- ): -- """ -- calling add_response_metadata should update fields based on grpc response metadata -- The x-goog-ext-425905942-bin field contains cluster and zone info -- """ -- import grpc -- -- cls = type(self._make_one(mock.Mock())) -- with mock.patch.object(cls, "_handle_error") as mock_handle_error: -- metric = self._make_one( -- mock.Mock(), -- cluster_id=start_cluster, -- zone=start_zone, -- state=State.ACTIVE_ATTEMPT, -- ) -- metric.active_attempt = mock.Mock() -- metric.active_attempt.gfe_latency_ns = None -- metadata = grpc.aio.Metadata() -- if metadata_proto is not None: -- metadata["x-goog-ext-425905942-bin"] = ResponseParams.serialize( -- metadata_proto -- ) -- metric.add_response_metadata(metadata) -- assert metric.cluster_id == end_cluster -- assert metric.zone == end_zone -- # should remain in ACTIVE_ATTEMPT state after completing -- assert metric.state == State.ACTIVE_ATTEMPT -- # no errors encountered -- assert mock_handle_error.call_count == 0 -- # gfe latency should not be touched -- assert metric.active_attempt.gfe_latency_ns is None -- -- @pytest.mark.parametrize( -- "metadata_field", -- [ -- b"bad-input", -- "cluster zone", # expect bytes -- ], -- ) -- def test_add_response_metadata_cbt_header_w_error(self, metadata_field): -- """ -- If the x-goog-ext-425905942-bin field is present, but not structured properly, -- _handle_error should be called -- -- Extra fields should not result in parsingerror -- """ -- import grpc -- -- cls = type(self._make_one(mock.Mock())) -- with mock.patch.object(cls, "_handle_error") as mock_handle_error: -- metric = self._make_one(mock.Mock(), state=State.ACTIVE_ATTEMPT) -- metric.cluster_id = None -- metric.zone = None -- metric.active_attempt = mock.Mock() -- metadata = grpc.aio.Metadata() -- metadata["x-goog-ext-425905942-bin"] = metadata_field -- metric.add_response_metadata(metadata) -- # should remain in ACTIVE_ATTEMPT state after completing -- assert metric.state == State.ACTIVE_ATTEMPT -- # no errors encountered -- assert mock_handle_error.call_count == 1 -- assert ( -- "Failed to decode x-goog-ext-425905942-bin metadata:" -- in mock_handle_error.call_args[0][0] -- ) -- assert str(metadata_field) in mock_handle_error.call_args[0][0] -- -- @pytest.mark.parametrize( -- "metadata_field,expected_latency_ns", -- [ -- (None, None), -- ("gfet4t7; dur=1000", 1000e6), -- ("gfet4t7; dur=1000.0", 1000e6), -- ("gfet4t7; dur=1000.1", 1000.1e6), -- ("gcp; dur=15, gfet4t7; dur=300", 300e6), -- ("gfet4t7;dur=350,gcp;dur=12", 350e6), -- ("ignore_megfet4t7;dur=90ignore_me", 90e6), -- ("gfet4t7;dur=2000", 2000e6), -- ("gfet4t7; dur=0.001", 1000), -- ("gfet4t7; dur=0.000001", 1), -- ("gfet4t7; dur=0.0000001", 0), # below recording resolution -- ("gfet4t7; dur=0", 0), -- ("gfet4t7; dur=empty", None), -- ("gfet4t7;", None), -- ("", None), -- ], -- ) -- def test_add_response_metadata_server_timing_header( -- self, metadata_field, expected_latency_ns -- ): -- """ -- calling add_response_metadata should update fields based on grpc response metadata -- The server-timing field contains gfle latency info -- """ -- import grpc -- -- cls = type(self._make_one(mock.Mock())) -- with mock.patch.object(cls, "_handle_error") as mock_handle_error: -- metric = self._make_one(mock.Mock(), state=State.ACTIVE_ATTEMPT) -- metric.active_attempt = mock.Mock() -- metric.active_attempt.gfe_latency_ns = None -- metadata = grpc.aio.Metadata() -- if metadata_field: -- metadata["server-timing"] = metadata_field -- metric.add_response_metadata(metadata) -- if metric.active_attempt.gfe_latency_ns is None: -- assert expected_latency_ns is None -- else: -- assert metric.active_attempt.gfe_latency_ns == int(expected_latency_ns) -- # should remain in ACTIVE_ATTEMPT state after completing -- assert metric.state == State.ACTIVE_ATTEMPT -- # no errors encountered -- assert mock_handle_error.call_count == 0 -- # cluster and zone should not be touched -- assert metric.cluster_id is None -- assert metric.zone is None -- -- @mock.patch("time.monotonic_ns") -- def test_end_attempt_with_status(self, mock_monotonic_ns): -- """ -- ending the attempt should: -- - add one to completed_attempts -- - reset active_attempt to None -- - update state -- - notify handlers -- """ -- expected_mock_time = 123456789 -- mock_monotonic_ns.return_value = expected_mock_time -- expected_start_time = 1 -- expected_status = object() -- expected_gfe_latency_ns = 5 -- expected_app_blocking = 12 -- expected_backoff = 2 -- handlers = [mock.Mock(), mock.Mock()] -- -- metric = self._make_one(mock.Mock(), handlers=handlers) -- assert metric.active_attempt is None -- assert len(metric.completed_attempts) == 0 -- metric.start_attempt() -- metric.active_attempt.start_time_ns = expected_start_time -- metric.active_attempt.gfe_latency_ns = expected_gfe_latency_ns -- metric.active_attempt.application_blocking_time_ns = expected_app_blocking -- metric.active_attempt.backoff_before_attempt_ns = expected_backoff -- metric.end_attempt_with_status(expected_status) -- assert len(metric.completed_attempts) == 1 -- got_attempt = metric.completed_attempts[0] -- expected_duration = expected_mock_time - expected_start_time -- assert got_attempt.duration_ns == expected_duration -- assert got_attempt.end_status == expected_status -- assert got_attempt.gfe_latency_ns == expected_gfe_latency_ns -- assert got_attempt.application_blocking_time_ns == expected_app_blocking -- assert got_attempt.backoff_before_attempt_ns == expected_backoff -- # state should be changed to BETWEEN_ATTEMPTS -- assert metric.state == State.BETWEEN_ATTEMPTS -- # check handlers -- for h in handlers: -- assert h.on_attempt_complete.call_count == 1 -- assert h.on_attempt_complete.call_args[0][0] == got_attempt -- assert h.on_attempt_complete.call_args[0][1] == metric -- -- def test_end_attempt_with_status_w_exception(self): -- """ -- exception inputs should be converted to grpc status objects -- """ -- input_status = ValueError("test") -- expected_status = object() -- -- metric = self._make_one(mock.Mock()) -- metric.start_attempt() -- with mock.patch.object( -- metric, "_exc_to_status", return_value=expected_status -- ) as mock_exc_to_status: -- metric.end_attempt_with_status(input_status) -- assert mock_exc_to_status.call_count == 1 -- assert mock_exc_to_status.call_args[0][0] == input_status -- assert metric.completed_attempts[0].end_status == expected_status -- -- @mock.patch("time.monotonic_ns") -- def test_end_attempt_with_negative_duration_ns(self, mock_monotonic_ns): -- """ -- If duration_ns is negative, it should be set to 0 and _handle_error should be called -- """ -- cls = type(self._make_one(mock.Mock())) -- with mock.patch.object(cls, "_handle_error") as mock_handle_error: -- metric = self._make_one(mock.Mock()) -- metric.start_attempt() -- metric.active_attempt.start_time_ns = 100 -- mock_monotonic_ns.return_value = 50 # Simulate time going backwards -- metric.end_attempt_with_status(mock.Mock()) -- -- assert mock_handle_error.call_count == 1 -- assert ( -- "received negative value for duration" -- in mock_handle_error.call_args[0][0] -- ) -- assert metric.completed_attempts[0].duration_ns == 0 -- -- @mock.patch("time.monotonic_ns") -- def test_end_with_status(self, mock_monotonic_ns): -- """ -- ending the operation should: -- - end active attempt -- - mark operation as completed -- - update handlers -- """ -- from google.cloud.bigtable.data._metrics.data_model import ActiveAttemptMetric -- -- expected_mock_time = 123456789 -- mock_monotonic_ns.return_value = expected_mock_time -- expected_attempt_start_time = 0 -- expected_attempt_gfe_latency_ns = 5 -- expected_flow_time = 16 -- -- expected_first_response_latency_ns = 9 -- expected_status = object() -- expected_type = object() -- expected_start_time = 1 -- expected_cluster = object() -- expected_zone = object() -- is_streaming = object() -- -- handlers = [mock.Mock(), mock.Mock()] -- metric = self._make_one( -- expected_type, -- handlers=handlers, -- start_time_ns=expected_start_time, -- state=State.ACTIVE_ATTEMPT, -- ) -- metric.cluster_id = expected_cluster -- metric.zone = expected_zone -- metric.is_streaming = is_streaming -- metric.flow_throttling_time_ns = expected_flow_time -- metric.first_response_latency_ns = expected_first_response_latency_ns -- attempt = ActiveAttemptMetric( -- start_time_ns=expected_attempt_start_time, -- gfe_latency_ns=expected_attempt_gfe_latency_ns, -- ) -- metric.active_attempt = attempt -- metric.end_with_status(expected_status) -- # test that ActiveOperation was updated to terminal state -- assert metric.state == State.COMPLETED -- assert metric.active_attempt is None -- assert len(metric.completed_attempts) == 1 -- # check that finalized operation was passed to handlers -- for h in handlers: -- assert h.on_operation_complete.call_count == 1 -- assert len(h.on_operation_complete.call_args[0]) == 1 -- called_with = h.on_operation_complete.call_args[0][0] -- assert called_with.op_type == expected_type -- expected_duration = expected_mock_time - expected_start_time -- assert called_with.duration_ns == expected_duration -- assert called_with.final_status == expected_status -- assert called_with.cluster_id == expected_cluster -- assert called_with.zone == expected_zone -- assert called_with.is_streaming == is_streaming -- assert called_with.flow_throttling_time_ns == expected_flow_time -- assert ( -- called_with.first_response_latency_ns -- == expected_first_response_latency_ns -- ) -- # check the attempt -- assert len(called_with.completed_attempts) == 1 -- final_attempt = called_with.completed_attempts[0] -- assert final_attempt.gfe_latency_ns == expected_attempt_gfe_latency_ns -- assert final_attempt.end_status == expected_status -- expected_duration = expected_mock_time - expected_attempt_start_time -- assert final_attempt.duration_ns == expected_duration -- -- @mock.patch("time.monotonic_ns") -- def test_end_with_negative_duration_ns(self, mock_monotonic_ns): -- """ -- If operation duration_ns is negative, it should be set to 0 and _handle_error should be called -- """ -- cls = type(self._make_one(mock.Mock())) -- with mock.patch.object(cls, "_handle_error") as mock_handle_error: -- metric = self._make_one(mock.Mock(), handlers=[mock.Mock()]) -- metric.start_time_ns = 100 -- mock_monotonic_ns.return_value = 50 # Simulate time going backwards -- metric.end_with_status(mock.Mock()) -- -- assert mock_handle_error.call_count == 1 -- assert ( -- "received negative value for duration" -- in mock_handle_error.call_args[0][0] -- ) -- final_op = metric.handlers[0].on_operation_complete.call_args[0][0] -- assert final_op.duration_ns == 0 -- -- def test_end_with_status_w_exception(self): -- """ -- exception inputs should be converted to grpc status objects -- """ -- input_status = ValueError("test") -- expected_status = object() -- handlers = [mock.Mock()] -- -- metric = self._make_one(mock.Mock(), handlers=handlers) -- metric.start_attempt() -- with mock.patch.object( -- metric, "_exc_to_status", return_value=expected_status -- ) as mock_exc_to_status: -- metric.end_with_status(input_status) -- assert mock_exc_to_status.call_count == 1 -- assert mock_exc_to_status.call_args[0][0] == input_status -- assert metric.completed_attempts[0].end_status == expected_status -- final_op = handlers[0].on_operation_complete.call_args[0][0] -- assert final_op.final_status == expected_status -- -- def test_end_with_status_with_default_cluster_zone(self): -- """ -- ending the operation should use default cluster and zone if not set -- """ -- from google.cloud.bigtable.data._metrics.data_model import ( -- DEFAULT_CLUSTER_ID, -- DEFAULT_ZONE, -- ) -- -- handlers = [mock.Mock()] -- metric = self._make_one(mock.Mock(), handlers=handlers) -- assert metric.cluster_id is None -- assert metric.zone is None -- metric.end_with_status(mock.Mock()) -- assert metric.state == State.COMPLETED -- # check that finalized operation was passed to handlers -- for h in handlers: -- assert h.on_operation_complete.call_count == 1 -- called_with = h.on_operation_complete.call_args[0][0] -- assert called_with.cluster_id == DEFAULT_CLUSTER_ID -- assert called_with.zone == DEFAULT_ZONE -- -- def test_end_with_success(self): -- """ -- end with success should be a pass-through helper for end_with_status -- """ -- from grpc import StatusCode -- -- inner_result = object() -- -- metric = self._make_one(mock.Mock()) -- with mock.patch.object(metric, "end_with_status") as mock_end_with_status: -- mock_end_with_status.return_value = inner_result -- got_result = metric.end_with_success() -- assert mock_end_with_status.call_count == 1 -- assert mock_end_with_status.call_args[0][0] == StatusCode.OK -- assert got_result is inner_result -- -- def test_end_on_empty_operation(self): -- """ -- Should be able to end an operation without any attempts -- """ -- from grpc import StatusCode -- -- handlers = [mock.Mock()] -- metric = self._make_one(mock.Mock(), handlers=handlers) -- metric.end_with_success() -- assert metric.state == State.COMPLETED -- final_op = handlers[0].on_operation_complete.call_args[0][0] -- assert final_op.final_status == StatusCode.OK -- assert final_op.completed_attempts == [] -- -- def test__exc_to_status(self): -- """ -- Should return grpc_status_code if grpc error, otherwise UNKNOWN -- -- If BigtableExceptionGroup, use the most recent exception in the group -- """ -- from grpc import StatusCode -- from google.api_core import exceptions as core_exc -- from google.cloud.bigtable.data import exceptions as bt_exc -- -- cls = type(self._make_one(object())) -- # unknown for non-grpc errors -- assert cls._exc_to_status(ValueError()) == StatusCode.UNKNOWN -- assert cls._exc_to_status(RuntimeError()) == StatusCode.UNKNOWN -- # grpc status code for grpc errors -- assert ( -- cls._exc_to_status(core_exc.InvalidArgument("msg")) -- == StatusCode.INVALID_ARGUMENT -- ) -- assert cls._exc_to_status(core_exc.NotFound("msg")) == StatusCode.NOT_FOUND -- assert ( -- cls._exc_to_status(core_exc.AlreadyExists("msg")) -- == StatusCode.ALREADY_EXISTS -- ) -- assert ( -- cls._exc_to_status(core_exc.PermissionDenied("msg")) -- == StatusCode.PERMISSION_DENIED -- ) -- cause_exc = core_exc.AlreadyExists("msg") -- w_cause = core_exc.DeadlineExceeded("msg") -- w_cause.__cause__ = cause_exc -- assert cls._exc_to_status(w_cause) == StatusCode.DEADLINE_EXCEEDED -- # use cause if available -- w_cause = ValueError("msg") -- w_cause.__cause__ = cause_exc -- cause_exc.grpc_status_code = object() -- custom_excs = [ -- bt_exc.FailedMutationEntryError(1, mock.Mock(), cause=cause_exc), -- bt_exc.FailedQueryShardError(1, {}, cause=cause_exc), -- w_cause, -- ] -- for exc in custom_excs: -- assert cls._exc_to_status(exc) == cause_exc.grpc_status_code, exc -- # extract most recent exception for bigtable exception groups -- exc_groups = [ -- bt_exc._BigtableExceptionGroup("", [ValueError(), cause_exc]), -- bt_exc.RetryExceptionGroup([RuntimeError(), cause_exc]), -- bt_exc.ShardedReadRowsExceptionGroup( -- [bt_exc.FailedQueryShardError(1, {}, cause=cause_exc)], [], 2 -- ), -- bt_exc.MutationsExceptionGroup( -- [bt_exc.FailedMutationEntryError(1, mock.Mock(), cause=cause_exc)], 2 -- ), -- ] -- for exc in exc_groups: -- assert cls._exc_to_status(exc) == cause_exc.grpc_status_code, exc -- -- def test__handle_error(self): -- """ -- handle_error should write log -- """ -- input_message = "test message" -- expected_message = f"Error in Bigtable Metrics: {input_message}" -- with mock.patch( -- "google.cloud.bigtable.data._metrics.data_model.LOGGER" -- ) as logger_mock: -- type(self._make_one(object()))._handle_error(input_message) -- assert logger_mock.warning.call_count == 1 -- assert logger_mock.warning.call_args[0][0] == expected_message -- assert len(logger_mock.warning.call_args[0]) == 1 -- -- @pytest.mark.asyncio -- async def test_context_manager(self): -- """ -- Should implement context manager protocol -- """ -- metric = self._make_one(object()) -- with mock.patch.object(metric, "end_with_success") as end_with_success_mock: -- end_with_success_mock.side_effect = lambda: metric.end_with_status(object()) -- with metric as context: -- assert context == metric -- # inside context manager, still active -- assert end_with_success_mock.call_count == 0 -- assert metric.state == State.CREATED -- # outside context manager, should be ended -- assert end_with_success_mock.call_count == 1 -- assert metric.state == State.COMPLETED -- -- @pytest.mark.asyncio -- async def test_context_manager_exception(self): -- """ -- Exception within context manager causes end_with_status to be called with error -- """ -- expected_exc = ValueError("expected") -- metric = self._make_one(object()) -- with mock.patch.object(metric, "end_with_status") as end_with_status_mock: -- try: -- with metric: -- # inside context manager, still active -- assert end_with_status_mock.call_count == 0 -- assert metric.state == State.CREATED -- raise expected_exc -- except ValueError as e: -- assert e == expected_exc -- # outside context manager, should be ended -- assert end_with_status_mock.call_count == 1 -- assert end_with_status_mock.call_args[0][0] == expected_exc -diff --git a/tests/unit/data/_metrics/test_metrics_controller.py b/tests/unit/data/_metrics/test_metrics_controller.py -deleted file mode 100644 -index 125c2be1..00000000 ---- a/tests/unit/data/_metrics/test_metrics_controller.py -+++ /dev/null -@@ -1,96 +0,0 @@ --# Copyright 2025 Google LLC --# --# 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 mock -- -- --class TestBigtableClientSideMetricsController: -- def _make_one(self, *args, **kwargs): -- from google.cloud.bigtable.data._metrics import ( -- BigtableClientSideMetricsController, -- ) -- -- return BigtableClientSideMetricsController(*args, **kwargs) -- -- def test_ctor_defaults(self): -- """ -- should create instance with GCP Exporter handler by default -- """ -- instance = self._make_one() -- assert len(instance.handlers) == 0 -- -- def ctor_custom_handlers(self): -- """ -- if handlers are passed to init, use those instead -- """ -- custom_handler = object() -- custom_interceptor = object() -- controller = self._make_one(custom_interceptor, handlers=[custom_handler]) -- assert controller.interceptor == custom_interceptor -- assert len(controller.handlers) == 1 -- assert controller.handlers[0] is custom_handler -- -- def test_add_handler(self): -- """ -- New handlers should be added to list -- """ -- controller = self._make_one(handlers=[object()]) -- initial_handler_count = len(controller.handlers) -- new_handler = object() -- controller.add_handler(new_handler) -- assert len(controller.handlers) == initial_handler_count + 1 -- assert controller.handlers[-1] is new_handler -- -- def test_create_operation_mock(self): -- """ -- All args should be passed through, as well as the handlers -- """ -- from google.cloud.bigtable.data._metrics import ActiveOperationMetric -- -- controller = self._make_one(handlers=[object()]) -- arg = object() -- kwargs = {"a": 1, "b": 2} -- with mock.patch( -- "google.cloud.bigtable.data._metrics.ActiveOperationMetric.__init__" -- ) as mock_op: -- mock_op.return_value = None -- op = controller.create_operation(arg, **kwargs) -- assert isinstance(op, ActiveOperationMetric) -- assert mock_op.call_count == 1 -- mock_op.assert_called_with(arg, **kwargs, handlers=controller.handlers) -- -- def test_create_operation(self): -- from google.cloud.bigtable.data._metrics import ActiveOperationMetric -- -- handler = object() -- expected_type = object() -- expected_is_streaming = True -- expected_zone = object() -- controller = self._make_one(handlers=[handler]) -- op = controller.create_operation( -- expected_type, is_streaming=expected_is_streaming, zone=expected_zone -- ) -- assert isinstance(op, ActiveOperationMetric) -- assert op.op_type is expected_type -- assert op.is_streaming is expected_is_streaming -- assert op.zone is expected_zone -- assert len(op.handlers) == 1 -- assert op.handlers[0] is handler -- -- def test_close(self): -- handlers = [mock.Mock() for _ in range(3)] -- controller = self._make_one(handlers=handlers) -- controller.close() -- for handler in handlers: -- handler.close.assert_called_once() -diff --git a/tests/unit/data/_metrics/test_tracked_retry.py b/tests/unit/data/_metrics/test_tracked_retry.py -deleted file mode 100644 -index 39713dc6..00000000 ---- a/tests/unit/data/_metrics/test_tracked_retry.py -+++ /dev/null -@@ -1,232 +0,0 @@ --# Copyright 2025 Google LLC --# --# 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 pytest --import inspect --import mock --import sys --from grpc import StatusCode --from google.api_core import exceptions as core_exceptions --from google.api_core.retry import RetryFailureReason --import google.api_core.retry as retry_module -- -- --class TestTrackRetryableError: -- def _call_fut(self, operation): -- from google.cloud.bigtable.data._metrics.tracked_retry import ( -- _track_retryable_error, -- ) -- -- return _track_retryable_error(operation) -- -- def test_basic_exception(self): -- """should call operation.end_attempt_with_status with the exception for basic exceptions.""" -- operation = mock.Mock() -- wrapper = self._call_fut(operation) -- -- exc = RuntimeError("test") -- wrapper(exc) -- -- operation.end_attempt_with_status.assert_called_once_with(exc) -- -- def test_mutate_rows_incomplete(self): -- """should call operation.end_attempt_with_status with StatusCode.OK for _MutateRowsIncomplete exceptions.""" -- from google.cloud.bigtable.data.exceptions import _MutateRowsIncomplete -- -- operation = mock.Mock() -- wrapper = self._call_fut(operation) -- -- exc = _MutateRowsIncomplete("test") -- wrapper(exc) -- -- operation.end_attempt_with_status.assert_called_once_with(StatusCode.OK) -- -- def test_rpc_error_metadata(self): -- """should extract and add metadata from GoogleAPICallError.""" -- operation = mock.Mock() -- wrapper = self._call_fut(operation) -- -- rpc_error = mock.Mock() -- rpc_error.trailing_metadata.return_value = (("key1", "val1"),) -- rpc_error.initial_metadata.return_value = (("key2", "val2"),) -- -- exc = core_exceptions.GoogleAPICallError("test", errors=[rpc_error]) -- wrapper(exc) -- -- operation.add_response_metadata.assert_called_once_with( -- {"key1": "val1", "key2": "val2"} -- ) -- operation.end_attempt_with_status.assert_called_once_with(exc) -- -- def test_metadata_error_ignored(self): -- """should ignore errors during metadata collection.""" -- operation = mock.Mock() -- operation.add_response_metadata.side_effect = RuntimeError("metadata error") -- wrapper = self._call_fut(operation) -- -- rpc_error = mock.Mock() -- rpc_error.trailing_metadata.return_value = () -- rpc_error.initial_metadata.return_value = () -- exc = core_exceptions.GoogleAPICallError("test", errors=[rpc_error]) -- -- # should not raise -- wrapper(exc) -- -- operation.end_attempt_with_status.assert_called_once_with(exc) -- -- --class TestTrackTerminalError: -- def _call_fut(self, operation, factory): -- from google.cloud.bigtable.data._metrics.tracked_retry import ( -- _track_terminal_error, -- ) -- -- return _track_terminal_error(operation, factory) -- -- def test_basic_pass_through(self): -- """should call the exception_factory and end the operation with its result.""" -- operation = mock.Mock() -- factory = mock.Mock() -- expected_exc = RuntimeError("source") -- expected_cause = RuntimeError("cause") -- factory.return_value = (expected_exc, expected_cause) -- -- wrapper = self._call_fut(operation, factory) -- -- exc_list = [RuntimeError("attempt1")] -- reason = RetryFailureReason.TIMEOUT -- timeout_val = 1.0 -- -- result = wrapper(exc_list, reason, timeout_val) -- -- assert result == (expected_exc, expected_cause) -- factory.assert_called_once_with(exc_list, reason, timeout_val) -- operation.end_with_status.assert_called_once_with(expected_exc) -- -- def test_timeout_active_attempt(self): -- """should end attempt if fails on timeout.""" -- from google.cloud.bigtable.data._metrics import OperationState -- -- operation = mock.Mock() -- operation.state = OperationState.ACTIVE_ATTEMPT -- factory = mock.Mock() -- factory.return_value = (RuntimeError("timeout"), None) -- -- wrapper = self._call_fut(operation, factory) -- -- last_exc = RuntimeError("last attempt error") -- exc_list = [last_exc] -- -- wrapper(exc_list, RetryFailureReason.TIMEOUT, 1.0) -- -- # expect call to end_attempt_with_status via the _track_retryable_error logic -- operation.end_attempt_with_status.assert_called_once_with(last_exc) -- operation.end_with_status.assert_called_once() -- -- def test_rpc_error_metadata(self): -- """should extract and add metadata from GoogleAPICallError in terminal errors.""" -- operation = mock.Mock() -- factory = mock.Mock() -- -- rpc_error = mock.Mock() -- rpc_error.trailing_metadata.return_value = (("k", "v"),) -- rpc_error.initial_metadata.return_value = () -- source_exc = core_exceptions.GoogleAPICallError("test", errors=[rpc_error]) -- -- factory.return_value = (source_exc, None) -- -- wrapper = self._call_fut(operation, factory) -- wrapper([], RetryFailureReason.NON_RETRYABLE_ERROR, None) -- -- operation.add_response_metadata.assert_called_once_with({"k": "v"}) -- operation.end_with_status.assert_called_once_with(source_exc) -- -- --class TestTrackedRetry: -- def _call_fut(self, **kwargs): -- from google.cloud.bigtable.data._metrics.tracked_retry import tracked_retry -- -- return tracked_retry(**kwargs) -- -- def test_call_args(self): -- """should correctly pass arguments to the retry_fn.""" -- operation = mock.Mock() -- retry_fn = mock.Mock() -- retry_fn.return_value = "result" -- -- result = self._call_fut(retry_fn=retry_fn, operation=operation, other_arg=123) -- -- assert result == "result" -- retry_fn.assert_called_once() -- call_kwargs = retry_fn.call_args[1] -- -- assert call_kwargs["sleep_generator"] == operation.backoff_generator -- assert "on_error" in call_kwargs -- assert "exception_factory" in call_kwargs -- assert call_kwargs["other_arg"] == 123 -- -- def test_tracked_retry_wraps_components(self): -- """should wrap on_error and exception_factory with tracking logic.""" -- from google.cloud.bigtable.data._metrics import tracked_retry -- -- module = sys.modules[tracked_retry.__module__] -- -- with mock.patch.object(module, "_track_retryable_error") as mock_track_retry: -- with mock.patch.object( -- module, "_track_terminal_error" -- ) as mock_track_terminal: -- operation = mock.Mock() -- retry_fn = mock.Mock() -- custom_factory = mock.Mock() -- -- self._call_fut( -- retry_fn=retry_fn, -- operation=operation, -- exception_factory=custom_factory, -- arg=1, -- ) -- -- mock_track_retry.assert_called_once_with(operation) -- mock_track_terminal.assert_called_once_with(operation, custom_factory) -- -- retry_fn.assert_called_once_with( -- sleep_generator=operation.backoff_generator, -- on_error=mock_track_retry.return_value, -- exception_factory=mock_track_terminal.return_value, -- arg=1, -- ) -- -- @pytest.mark.parametrize( -- "fn_name,type_verifier", -- [ -- ("retry_target", callable), -- ("retry_target_stream", inspect.isgenerator), -- ("retry_target_async", inspect.iscoroutine), -- ("retry_target_stream_async", inspect.isasyncgen), -- ], -- ) -- def test_wrapping_api_core(self, fn_name, type_verifier): -- """Test building tracked retry from different supported retry functions""" -- from google.cloud.bigtable.data._metrics import ActiveOperationMetric -- -- operation = ActiveOperationMetric("type") -- fn = getattr(retry_module, fn_name) -- tracked_retry = self._call_fut( -- retry_fn=fn, -- operation=operation, -- target=mock.Mock(), -- timeout=None, -- predicate=lambda x: False, -- ) -- assert type_verifier(tracked_retry) -diff --git a/tests/unit/data/_sync_autogen/test_client.py b/tests/unit/data/_sync_autogen/test_client.py -index 54be1f17..49ed41ad 100644 ---- a/tests/unit/data/_sync_autogen/test_client.py -+++ b/tests/unit/data/_sync_autogen/test_client.py -@@ -47,13 +47,9 @@ from tests.unit.data.execute_query.sql_helpers import ( - ) - from google.api_core import grpc_helpers - from google.cloud.bigtable.data._sync_autogen._swappable_channel import SwappableChannel --from google.cloud.bigtable.data._sync_autogen.metrics_interceptor import ( -- BigtableMetricsInterceptor, --) - - CrossSync._Sync_Impl.add_mapping("grpc_helpers", grpc_helpers) - CrossSync._Sync_Impl.add_mapping("SwappableChannel", SwappableChannel) --CrossSync._Sync_Impl.add_mapping("MetricsInterceptor", BigtableMetricsInterceptor) - - - @CrossSync._Sync_Impl.add_mapping_decorator("TestBigtableDataClient") -@@ -89,9 +85,6 @@ class TestBigtableDataClient: - assert not client._active_instances - assert client._channel_refresh_task is not None - assert client.transport._credentials == expected_credentials -- assert isinstance( -- client._metrics_interceptor, CrossSync._Sync_Impl.MetricsInterceptor -- ) - client.close() - - def test_ctor_super_inits(self): -@@ -940,9 +933,6 @@ class TestTable: - - def test_ctor(self): - from google.cloud.bigtable.data._helpers import _WarmedInstanceKey -- from google.cloud.bigtable.data._metrics import ( -- BigtableClientSideMetricsController, -- ) - - expected_table_id = "table-id" - expected_instance_id = "instance-id" -@@ -983,7 +973,6 @@ class TestTable: - instance_key = _WarmedInstanceKey(table.instance_name, table.app_profile_id) - assert instance_key in client._active_instances - assert client._instance_owners[instance_key] == {id(table)} -- assert isinstance(table._metrics, BigtableClientSideMetricsController) - assert table.default_operation_timeout == expected_operation_timeout - assert table.default_attempt_timeout == expected_attempt_timeout - assert ( -@@ -1176,21 +1165,6 @@ class TestTable: - else: - assert "app_profile_id=" in routing_str - -- def test_close(self): -- client = self._make_client() -- table = self._make_one(client) -- with mock.patch.object( -- table._metrics, "close", mock.Mock() -- ) as metric_close_mock: -- with mock.patch.object( -- client, "_remove_instance_registration" -- ) as remove_mock: -- table.close() -- remove_mock.assert_called_once_with( -- table.instance_id, table.app_profile_id, id(table) -- ) -- metric_close_mock.assert_called_once() -- - - @CrossSync._Sync_Impl.add_mapping_decorator("TestAuthorizedView") - class TestAuthorizedView(CrossSync._Sync_Impl.TestTable): -@@ -1217,9 +1191,6 @@ class TestAuthorizedView(CrossSync._Sync_Impl.TestTable): - - def test_ctor(self): - from google.cloud.bigtable.data._helpers import _WarmedInstanceKey -- from google.cloud.bigtable.data._metrics import ( -- BigtableClientSideMetricsController, -- ) - - expected_table_id = "table-id" - expected_instance_id = "instance-id" -@@ -1267,7 +1238,6 @@ class TestAuthorizedView(CrossSync._Sync_Impl.TestTable): - instance_key = _WarmedInstanceKey(view.instance_name, view.app_profile_id) - assert instance_key in client._active_instances - assert client._instance_owners[instance_key] == {id(view)} -- assert isinstance(view._metrics, BigtableClientSideMetricsController) - assert view.default_operation_timeout == expected_operation_timeout - assert view.default_attempt_timeout == expected_attempt_timeout - assert ( -@@ -1463,7 +1433,8 @@ class TestReadRows: - ) - - @pytest.mark.parametrize( -- "per_request_t, operation_t, expected_num", [(0.1, 0.19, 2), (0.1, 0.29, 3)] -+ "per_request_t, operation_t, expected_num", -+ [(0.05, 0.08, 2), (0.05, 0.14, 3), (0.05, 0.24, 5)], - ) - def test_read_rows_attempt_timeout(self, per_request_t, operation_t, expected_num): - """Ensures that the attempt_timeout is respected and that the number of -diff --git a/tests/unit/data/_sync_autogen/test_metrics_interceptor.py b/tests/unit/data/_sync_autogen/test_metrics_interceptor.py -index c4efcc5b..56a6f365 100644 ---- a/tests/unit/data/_sync_autogen/test_metrics_interceptor.py -+++ b/tests/unit/data/_sync_autogen/test_metrics_interceptor.py -@@ -17,9 +17,6 @@ - - import pytest - from grpc import RpcError --from grpc import ClientCallDetails --from google.cloud.bigtable.data._metrics.data_model import ActiveOperationMetric --from google.cloud.bigtable.data._metrics.data_model import OperationState - from google.cloud.bigtable.data._cross_sync import CrossSync - - try: -@@ -53,255 +50,91 @@ class TestMetricsInterceptor: - def _make_one(self, *args, **kwargs): - return self._get_target_class()(*args, **kwargs) - -- def test_unary_unary_interceptor_op_not_found(self): -- """Test that interceptor call continuation if op is not found""" -- instance = self._make_one() -- continuation = CrossSync._Sync_Impl.Mock() -- details = ClientCallDetails() -- details.metadata = [] -- request = mock.Mock() -- instance.intercept_unary_unary(continuation, details, request) -- continuation.assert_called_once_with(details, request) -- - def test_unary_unary_interceptor_success(self): - """Test that interceptor handles successful unary-unary calls""" - instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- ActiveOperationMetric._active_operation_context.set(op) - continuation = CrossSync._Sync_Impl.Mock() - call = continuation.return_value -- call.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[("a", "b")]) -- call.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[("c", "d")]) -- details = ClientCallDetails() -+ details = mock.Mock() - request = mock.Mock() - result = instance.intercept_unary_unary(continuation, details, request) - assert result == call - continuation.assert_called_once_with(details, request) -- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) -- op.end_attempt_with_status.assert_not_called() - - def test_unary_unary_interceptor_failure(self): -- """test a failed RpcError with metadata""" -- instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- ActiveOperationMetric._active_operation_context.set(op) -- exc = RpcError("test") -- exc.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[("a", "b")]) -- exc.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[("c", "d")]) -- continuation = CrossSync._Sync_Impl.Mock(side_effect=exc) -- details = ClientCallDetails() -- request = mock.Mock() -- with pytest.raises(RpcError) as e: -- instance.intercept_unary_unary(continuation, details, request) -- assert e.value == exc -- continuation.assert_called_once_with(details, request) -- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) -- -- def test_unary_unary_interceptor_failure_no_metadata(self): -- """test with RpcError without without metadata attached""" -+ """Test a failed RpcError with metadata""" - instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- ActiveOperationMetric._active_operation_context.set(op) - exc = RpcError("test") - continuation = CrossSync._Sync_Impl.Mock(side_effect=exc) -- call = continuation.return_value -- call.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[("a", "b")]) -- call.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[("c", "d")]) -- details = ClientCallDetails() -+ details = mock.Mock() - request = mock.Mock() - with pytest.raises(RpcError) as e: - instance.intercept_unary_unary(continuation, details, request) - assert e.value == exc - continuation.assert_called_once_with(details, request) -- op.add_response_metadata.assert_not_called() - - def test_unary_unary_interceptor_failure_generic(self): -- """test generic exception""" -+ """Test generic exception""" - instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- ActiveOperationMetric._active_operation_context.set(op) - exc = ValueError("test") - continuation = CrossSync._Sync_Impl.Mock(side_effect=exc) -- call = continuation.return_value -- call.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[("a", "b")]) -- call.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[("c", "d")]) -- details = ClientCallDetails() -+ details = mock.Mock() - request = mock.Mock() - with pytest.raises(ValueError) as e: - instance.intercept_unary_unary(continuation, details, request) - assert e.value == exc - continuation.assert_called_once_with(details, request) -- op.add_response_metadata.assert_not_called() -- -- def test_unary_stream_interceptor_op_not_found(self): -- """Test that interceptor calls continuation if op is not found""" -- instance = self._make_one() -- continuation = CrossSync._Sync_Impl.Mock() -- details = ClientCallDetails() -- details.metadata = [] -- request = mock.Mock() -- instance.intercept_unary_stream(continuation, details, request) -- continuation.assert_called_once_with(details, request) - - def test_unary_stream_interceptor_success(self): - """Test that interceptor handles successful unary-stream calls""" - instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- op.start_time_ns = 0 -- op.first_response_latency = None -- ActiveOperationMetric._active_operation_context.set(op) - continuation = CrossSync._Sync_Impl.Mock( - return_value=_make_mock_stream_call([1, 2]) - ) -- call = continuation.return_value -- call.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[("a", "b")]) -- call.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[("c", "d")]) -- details = ClientCallDetails() -+ details = mock.Mock() - request = mock.Mock() - wrapper = instance.intercept_unary_stream(continuation, details, request) - results = [val for val in wrapper] - assert results == [1, 2] - continuation.assert_called_once_with(details, request) -- assert op.first_response_latency_ns is not None -- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) -- op.end_attempt_with_status.assert_not_called() - - def test_unary_stream_interceptor_failure_mid_stream(self): - """Test that interceptor handles failures mid-stream""" -- from grpc.aio import AioRpcError, Metadata -- - instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- op.start_time_ns = 0 -- op.first_response_latency = None -- ActiveOperationMetric._active_operation_context.set(op) -- exc = AioRpcError(0, Metadata(), Metadata(("a", "b"), ("c", "d"))) -+ exc = ValueError("test") - continuation = CrossSync._Sync_Impl.Mock( - return_value=_make_mock_stream_call([1], exc=exc) - ) -- details = ClientCallDetails() -+ details = mock.Mock() - request = mock.Mock() - wrapper = instance.intercept_unary_stream(continuation, details, request) -- with pytest.raises(AioRpcError) as e: -+ with pytest.raises(ValueError) as e: - [val for val in wrapper] - assert e.value == exc - continuation.assert_called_once_with(details, request) -- assert op.first_response_latency_ns is not None -- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) - - def test_unary_stream_interceptor_failure_start_stream(self): - """Test that interceptor handles failures at start of stream with RpcError with metadata""" - instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- op.start_time_ns = 0 -- op.first_response_latency = None -- ActiveOperationMetric._active_operation_context.set(op) -- exc = RpcError("test") -- exc.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[("a", "b")]) -- exc.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[("c", "d")]) -- continuation = CrossSync._Sync_Impl.Mock() -- continuation.side_effect = exc -- details = ClientCallDetails() -- request = mock.Mock() -- with pytest.raises(RpcError) as e: -- instance.intercept_unary_stream(continuation, details, request) -- assert e.value == exc -- continuation.assert_called_once_with(details, request) -- assert op.first_response_latency_ns is not None -- op.add_response_metadata.assert_called_once_with({"a": "b", "c": "d"}) -- -- def test_unary_stream_interceptor_failure_start_stream_no_metadata(self): -- """Test that interceptor handles failures at start of stream with RpcError with no metadata""" -- instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- op.start_time_ns = 0 -- op.first_response_latency = None -- ActiveOperationMetric._active_operation_context.set(op) - exc = RpcError("test") - continuation = CrossSync._Sync_Impl.Mock() - continuation.side_effect = exc -- details = ClientCallDetails() -+ details = mock.Mock() - request = mock.Mock() - with pytest.raises(RpcError) as e: - instance.intercept_unary_stream(continuation, details, request) - assert e.value == exc - continuation.assert_called_once_with(details, request) -- assert op.first_response_latency_ns is not None -- op.add_response_metadata.assert_not_called() - - def test_unary_stream_interceptor_failure_start_stream_generic(self): - """Test that interceptor handles failures at start of stream with generic exception""" - instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = OperationState.ACTIVE_ATTEMPT -- op.start_time_ns = 0 -- op.first_response_latency = None -- ActiveOperationMetric._active_operation_context.set(op) - exc = ValueError("test") - continuation = CrossSync._Sync_Impl.Mock() - continuation.side_effect = exc -- details = ClientCallDetails() -+ details = mock.Mock() - request = mock.Mock() - with pytest.raises(ValueError) as e: - instance.intercept_unary_stream(continuation, details, request) - assert e.value == exc - continuation.assert_called_once_with(details, request) -- assert op.first_response_latency_ns is not None -- op.add_response_metadata.assert_not_called() -- -- @pytest.mark.parametrize( -- "initial_state", [OperationState.CREATED, OperationState.BETWEEN_ATTEMPTS] -- ) -- def test_unary_unary_interceptor_start_operation(self, initial_state): -- """if called with a newly created operation, it should be started""" -- instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = initial_state -- ActiveOperationMetric._active_operation_context.set(op) -- continuation = CrossSync._Sync_Impl.Mock() -- call = continuation.return_value -- call.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[]) -- call.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[]) -- details = ClientCallDetails() -- request = mock.Mock() -- instance.intercept_unary_unary(continuation, details, request) -- op.start_attempt.assert_called_once() -- -- @pytest.mark.parametrize( -- "initial_state", [OperationState.CREATED, OperationState.BETWEEN_ATTEMPTS] -- ) -- def test_unary_stream_interceptor_start_operation(self, initial_state): -- """if called with a newly created operation, it should be started""" -- instance = self._make_one() -- op = mock.Mock() -- op.uuid = "test-uuid" -- op.state = initial_state -- ActiveOperationMetric._active_operation_context.set(op) -- continuation = CrossSync._Sync_Impl.Mock( -- return_value=_make_mock_stream_call([1, 2]) -- ) -- call = continuation.return_value -- call.trailing_metadata = CrossSync._Sync_Impl.Mock(return_value=[]) -- call.initial_metadata = CrossSync._Sync_Impl.Mock(return_value=[]) -- details = ClientCallDetails() -- request = mock.Mock() -- instance.intercept_unary_stream(continuation, details, request) -- op.start_attempt.assert_called_once() -diff --git a/tests/unit/data/test__helpers.py b/tests/unit/data/test__helpers.py -index c8540024..96c726a2 100644 ---- a/tests/unit/data/test__helpers.py -+++ b/tests/unit/data/test__helpers.py -@@ -266,98 +266,3 @@ class TestGetRetryableErrors: - setattr(fake_table, f"{key}_retryable_errors", input_table[key]) - result = _helpers._get_retryable_errors(input_codes, fake_table) - assert result == expected -- -- --class TestTrackedBackoffGenerator: -- def test_tracked_backoff_generator_history(self): -- """ -- Should be able to retrieve historical results from backoff generator -- """ -- generator = _helpers.TrackedBackoffGenerator( -- initial=0, multiplier=2, maximum=10 -- ) -- got_list = [next(generator) for _ in range(20)] -- -- # check all values are correct -- for i in range(19, 0, -1): -- assert generator.get_attempt_backoff(i) == got_list[i] -- # check a random value out of order -- assert generator.get_attempt_backoff(5) == got_list[5] -- -- @mock.patch("random.uniform", side_effect=lambda a, b: b) -- def test_tracked_backoff_generator_defaults(self, mock_uniform): -- """ -- Should generate values with default parameters -- -- initial=0.01, multiplier=2, maximum=60 -- """ -- generator = _helpers.TrackedBackoffGenerator() -- expected_values = [0.01, 0.02, 0.04, 0.08, 0.16] -- for expected in expected_values: -- assert next(generator) == pytest.approx(expected) -- -- @mock.patch("random.uniform", side_effect=lambda a, b: b) -- def test_tracked_backoff_generator_with_maximum(self, mock_uniform): -- """ -- Should cap the backoff at the maximum value -- """ -- generator = _helpers.TrackedBackoffGenerator(initial=1, multiplier=2, maximum=5) -- expected_values = [1, 2, 4, 5, 5, 5] -- for expected in expected_values: -- assert next(generator) == expected -- -- def test_get_attempt_backoff_out_of_bounds(self): -- """ -- get_attempt_backoff should raise IndexError for out of bounds index -- """ -- generator = _helpers.TrackedBackoffGenerator() -- next(generator) -- next(generator) -- with pytest.raises(IndexError): -- generator.get_attempt_backoff(2) -- with pytest.raises(IndexError): -- generator.get_attempt_backoff(-3) -- -- def test_set_next_full_set(self): -- """ -- try always using set_next to populate generator -- """ -- generator = _helpers.TrackedBackoffGenerator() -- for idx, val in enumerate(range(100, 0, -1)): -- generator.set_next(val) -- got = next(generator) -- assert got == val -- assert generator.get_attempt_backoff(idx) == val -- -- def test_set_next_negative_value(self): -- generator = _helpers.TrackedBackoffGenerator() -- with pytest.raises(ValueError): -- generator.set_next(-1) -- -- @mock.patch("random.uniform", side_effect=lambda a, b: b) -- def test_interleaved_set_next(self, mock_uniform): -- import itertools -- -- generator = _helpers.TrackedBackoffGenerator( -- initial=1, multiplier=2, maximum=128 -- ) -- # values we expect generator to create -- expected_values = [2**i for i in range(8)] -- # values we will insert -- inserted_values = [9, 61, 0, 4, 33, 12, 18, 2] -- for idx in range(8): -- assert next(generator) == expected_values[idx] -- generator.set_next(inserted_values[idx]) -- assert next(generator) == inserted_values[idx] -- # check to make sure history is as we expect -- generator.history = itertools.chain.from_iterable( -- zip(expected_values, inserted_values) -- ) -- -- @mock.patch("random.uniform", side_effect=lambda a, b: b) -- def test_set_next_replacement(self, mock_uniform): -- generator = _helpers.TrackedBackoffGenerator(initial=1) -- generator.set_next(99) -- generator.set_next(88) -- assert next(generator) == 88 -- assert next(generator) == 1 -diff --git a/tests/unit/v2_client/test_backup.py b/tests/unit/v2_client/test_backup.py -index cc9251a3..a5d205af 100644 ---- a/tests/unit/v2_client/test_backup.py -+++ b/tests/unit/v2_client/test_backup.py -@@ -19,7 +19,6 @@ import mock - import pytest - - from ._testing import _make_credentials --from google.cloud._helpers import UTC - - PROJECT_ID = "project-id" - INSTANCE_ID = "instance-id" -@@ -38,7 +37,7 @@ ALT_BACKUP_NAME = ALT_CLUSTER_NAME + "/backups/" + BACKUP_ID - - - def _make_timestamp(): -- return datetime.datetime.utcnow().replace(tzinfo=UTC) -+ return datetime.datetime.now(datetime.timezone.utc) - - - def _make_table_admin_client(): -diff --git a/tests/unit/v2_client/test_cluster.py b/tests/unit/v2_client/test_cluster.py -index 65ed4743..a2110454 100644 ---- a/tests/unit/v2_client/test_cluster.py -+++ b/tests/unit/v2_client/test_cluster.py -@@ -420,7 +420,7 @@ def test_cluster_create(): - from google.cloud.bigtable_admin_v2.types import instance as instance_v2_pb2 - from google.cloud.bigtable.enums import StorageType - -- NOW = datetime.datetime.utcnow() -+ NOW = datetime.datetime.now(datetime.timezone.utc) - NOW_PB = _datetime_to_pb_timestamp(NOW) - credentials = _make_credentials() - client = _make_client(project=PROJECT, credentials=credentials, admin=True) -@@ -475,7 +475,7 @@ def test_cluster_create_w_cmek(): - from google.cloud.bigtable_admin_v2.types import instance as instance_v2_pb2 - from google.cloud.bigtable.enums import StorageType - -- NOW = datetime.datetime.utcnow() -+ NOW = datetime.datetime.now(datetime.timezone.utc) - NOW_PB = _datetime_to_pb_timestamp(NOW) - credentials = _make_credentials() - client = _make_client(project=PROJECT, credentials=credentials, admin=True) -@@ -535,7 +535,7 @@ def test_cluster_create_w_autoscaling(): - from google.cloud.bigtable_admin_v2.types import instance as instance_v2_pb2 - from google.cloud.bigtable.enums import StorageType - -- NOW = datetime.datetime.utcnow() -+ NOW = datetime.datetime.now(datetime.timezone.utc) - NOW_PB = _datetime_to_pb_timestamp(NOW) - credentials = _make_credentials() - client = _make_client(project=PROJECT, credentials=credentials, admin=True) -@@ -602,7 +602,7 @@ def test_cluster_update(): - ) - from google.cloud.bigtable.enums import StorageType - -- NOW = datetime.datetime.utcnow() -+ NOW = datetime.datetime.now(datetime.timezone.utc) - NOW_PB = _datetime_to_pb_timestamp(NOW) - - credentials = _make_credentials() -@@ -669,7 +669,7 @@ def test_cluster_update_w_autoscaling(): - ) - from google.cloud.bigtable.enums import StorageType - -- NOW = datetime.datetime.utcnow() -+ NOW = datetime.datetime.now(datetime.timezone.utc) - NOW_PB = _datetime_to_pb_timestamp(NOW) - - credentials = _make_credentials() -@@ -728,7 +728,7 @@ def test_cluster_update_w_partial_autoscaling_config(): - ) - from google.cloud.bigtable.enums import StorageType - -- NOW = datetime.datetime.utcnow() -+ NOW = datetime.datetime.now(datetime.timezone.utc) - NOW_PB = _datetime_to_pb_timestamp(NOW) - - credentials = _make_credentials() -@@ -812,7 +812,7 @@ def test_cluster_update_w_both_manual_and_autoscaling(): - ) - from google.cloud.bigtable.enums import StorageType - -- NOW = datetime.datetime.utcnow() -+ NOW = datetime.datetime.now(datetime.timezone.utc) - NOW_PB = _datetime_to_pb_timestamp(NOW) - - credentials = _make_credentials() -@@ -873,7 +873,7 @@ def test_cluster_disable_autoscaling(): - from google.cloud.bigtable.instance import Instance - from google.cloud.bigtable.enums import StorageType - -- NOW = datetime.datetime.utcnow() -+ NOW = datetime.datetime.now(datetime.timezone.utc) - NOW_PB = _datetime_to_pb_timestamp(NOW) - credentials = _make_credentials() - client = _make_client(project=PROJECT, credentials=credentials, admin=True) -diff --git a/tests/unit/v2_client/test_instance.py b/tests/unit/v2_client/test_instance.py -index 712fab1f..c5ef9c9b 100644 ---- a/tests/unit/v2_client/test_instance.py -+++ b/tests/unit/v2_client/test_instance.py -@@ -277,7 +277,7 @@ def _instance_api_response_for_create(): - ) - from google.cloud.bigtable_admin_v2.types import instance - -- NOW = datetime.datetime.utcnow() -+ NOW = datetime.datetime.now(datetime.timezone.utc) - NOW_PB = _datetime_to_pb_timestamp(NOW) - metadata = messages_v2_pb2.CreateInstanceMetadata(request_time=NOW_PB) - type_url = "type.googleapis.com/{}".format( -@@ -503,7 +503,7 @@ def _instance_api_response_for_update(): - ) - from google.cloud.bigtable_admin_v2.types import instance - -- NOW = datetime.datetime.utcnow() -+ NOW = datetime.datetime.now(datetime.timezone.utc) - NOW_PB = _datetime_to_pb_timestamp(NOW) - metadata = messages_v2_pb2.UpdateInstanceMetadata(request_time=NOW_PB) - type_url = "type.googleapis.com/{}".format( -diff --git a/tests/unit/v2_client/test_table.py b/tests/unit/v2_client/test_table.py -index 1d183e2f..6b31a5e2 100644 ---- a/tests/unit/v2_client/test_table.py -+++ b/tests/unit/v2_client/test_table.py -@@ -1378,13 +1378,12 @@ def test_table_backup_factory_defaults(): - - def test_table_backup_factory_non_defaults(): - import datetime -- from google.cloud._helpers import UTC - from google.cloud.bigtable.backup import Backup - from google.cloud.bigtable.instance import Instance - - instance = Instance(INSTANCE_ID, None) - table = _make_table(TABLE_ID, instance) -- timestamp = datetime.datetime.utcnow().replace(tzinfo=UTC) -+ timestamp = datetime.datetime.now(datetime.timezone.utc) - backup = table.backup( - BACKUP_ID, - cluster_id=CLUSTER_ID,