From 72dfdc440c22db0f4c372e6f11a9f7dc83fed350 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:04:28 -0800 Subject: [PATCH 01/10] feat: add PeerInfo proto in Bigtable API (#1190) - [ ] Regenerate this pull request now. BEGIN_COMMIT_OVERRIDE feat: add PeerInfo proto in Bigtable API fix: Add ReadRows/SampleRowKeys bindings for materialized views fix: Deprecate credentials_file argument feat: Add Type API updates needed to support structured keys in materialized views feat: Add encodings for STRUCT and the Timestamp type END_COMMIT_OVERRIDE PiperOrigin-RevId: 829585900 Source-Link: https://github.com/googleapis/googleapis/commit/1b5f8632487bce889ce05366647addc6ef5ee36d Source-Link: https://github.com/googleapis/googleapis-gen/commit/1a692875003e2754729dc2a4cca88055051d2aae Copy-Tag: eyJwIjoiLmdpdGh1Yi8uT3dsQm90LnlhbWwiLCJoIjoiMWE2OTI4NzUwMDNlMjc1NDcyOWRjMmE0Y2NhODgwNTUwNTFkMmFhZSJ9 BEGIN_NESTED_COMMIT fix: Add ReadRows/SampleRowKeys bindings for materialized views fix: Deprecate credentials_file argument chore: Update gapic-generator-python to 1.28.0 PiperOrigin-RevId: 816753840 Source-Link: https://github.com/googleapis/googleapis/commit/d06cf27a47074d1de3fde6f0ca48680a96229306 Source-Link: https://github.com/googleapis/googleapis-gen/commit/a524e7310882bbb99bfe1399b18bed328979211c Copy-Tag: eyJwIjoiLmdpdGh1Yi8uT3dsQm90LnlhbWwiLCJoIjoiYTUyNGU3MzEwODgyYmJiOTliZmUxMzk5YjE4YmVkMzI4OTc5MjExYyJ9 END_NESTED_COMMIT BEGIN_NESTED_COMMIT feat: Add Type API updates needed to support structured keys in materialized views feat: Add encodings for STRUCT and the Timestamp type PiperOrigin-RevId: 805031861 Source-Link: https://github.com/googleapis/googleapis/commit/6d1dca2b8e3d50914609414e219df2778b2b20ba Source-Link: https://github.com/googleapis/googleapis-gen/commit/ecd9d8860bae8bb37b452bfc6eefbdd22d028f09 Copy-Tag: eyJwIjoiLmdpdGh1Yi8uT3dsQm90LnlhbWwiLCJoIjoiZWNkOWQ4ODYwYmFlOGJiMzdiNDUyYmZjNmVlZmJkZDIyZDAyOGYwOSJ9 END_NESTED_COMMIT BEGIN_NESTED_COMMIT chore: Update gapic-generator-python to 1.26.2 PiperOrigin-RevId: 802200836 Source-Link: https://github.com/googleapis/googleapis/commit/d300b151a973ce0425ae4ad07b3de957ca31bec6 Source-Link: https://github.com/googleapis/googleapis-gen/commit/a1ff0ae72ddcb68a259215d8c77661e2cdbb9b02 Copy-Tag: eyJwIjoiLmdpdGh1Yi8uT3dsQm90LnlhbWwiLCJoIjoiYTFmZjBhZTcyZGRjYjY4YTI1OTIxNWQ4Yzc3NjYxZTJjZGJiOWIwMiJ9 END_NESTED_COMMIT BEGIN_NESTED_COMMIT chore: update Python generator version to 1.25.1 PiperOrigin-RevId: 800535761 Source-Link: https://github.com/googleapis/googleapis/commit/4cf1f99cccc014627af5e8a6c0f80a3e6ec0d268 Source-Link: https://github.com/googleapis/googleapis-gen/commit/133d25b68e712116e1c5dc71fc3eb3c5e717022a Copy-Tag: eyJwIjoiLmdpdGh1Yi8uT3dsQm90LnlhbWwiLCJoIjoiMTMzZDI1YjY4ZTcxMjExNmUxYzVkYzcxZmMzZWIzYzVlNzE3MDIyYSJ9 END_NESTED_COMMIT BEGIN_NESTED_COMMIT fix: Add ReadRows/SampleRowKeys bindings for materialized views PiperOrigin-RevId: 793800781 Source-Link: https://github.com/googleapis/googleapis/commit/fe06a492944dc3a8360ed5b426942d34631eeca7 Source-Link: https://github.com/googleapis/googleapis-gen/commit/c4550f60725dc4c07adfe68cee0ac72eb4a5b1bb Copy-Tag: eyJwIjoiLmdpdGh1Yi8uT3dsQm90LnlhbWwiLCJoIjoiYzQ1NTBmNjA3MjVkYzRjMDdhZGZlNjhjZWUwYWM3MmViNGE1YjFiYiJ9 END_NESTED_COMMIT --------- Co-authored-by: Owl Bot Co-authored-by: Anthonios Partheniou --- .github/workflows/lint.yml | 2 +- .github/workflows/unittest.yml | 2 +- .../bigtable_instance_admin/async_client.py | 16 +- .../bigtable_instance_admin/client.py | 16 +- .../transports/base.py | 5 +- .../transports/grpc.py | 8 +- .../transports/grpc_asyncio.py | 8 +- .../transports/rest.py | 5 +- .../bigtable_table_admin/async_client.py | 32 +- .../services/bigtable_table_admin/client.py | 32 +- .../bigtable_table_admin/transports/base.py | 5 +- .../bigtable_table_admin/transports/grpc.py | 8 +- .../transports/grpc_asyncio.py | 8 +- .../bigtable_table_admin/transports/rest.py | 5 +- .../types/bigtable_table_admin.py | 106 +++--- .../cloud/bigtable_admin_v2/types/instance.py | 18 +- google/cloud/bigtable_admin_v2/types/table.py | 79 ++-- google/cloud/bigtable_admin_v2/types/types.py | 95 +++-- google/cloud/bigtable_v2/__init__.py | 2 + .../services/bigtable/async_client.py | 14 + .../bigtable_v2/services/bigtable/client.py | 14 + .../services/bigtable/transports/base.py | 5 +- .../services/bigtable/transports/grpc.py | 8 +- .../bigtable/transports/grpc_asyncio.py | 9 +- .../services/bigtable/transports/rest.py | 101 ++++- .../services/bigtable/transports/rest_base.py | 9 + google/cloud/bigtable_v2/types/__init__.py | 4 + google/cloud/bigtable_v2/types/bigtable.py | 8 +- google/cloud/bigtable_v2/types/data.py | 69 ++-- .../cloud/bigtable_v2/types/feature_flags.py | 7 + google/cloud/bigtable_v2/types/peer_info.py | 118 ++++++ .../bigtable_v2/types/response_params.py | 10 + google/cloud/bigtable_v2/types/types.py | 354 +++++++++++++++--- owlbot.py | 1 + samples/beam/requirements.txt | 3 +- ...pet_metadata_google.bigtable.admin.v2.json | 2 +- tests/unit/gapic/bigtable_v2/test_bigtable.py | 269 +++++++++++++ 37 files changed, 1117 insertions(+), 340 deletions(-) create mode 100644 google/cloud/bigtable_v2/types/peer_info.py diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4866193af..9a0598202 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.13" - name: Install nox run: | python -m pip install --upgrade setuptools pip wheel diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index d59bbb1b8..dad646c6b 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -45,7 +45,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.13" - name: Install coverage run: | python -m pip install --upgrade setuptools pip wheel diff --git a/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/async_client.py b/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/async_client.py index a1aee2370..632496543 100644 --- a/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/async_client.py +++ b/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/async_client.py @@ -2565,19 +2565,19 @@ async def sample_get_iam_policy(): constraints based on attributes of the request, the resource, or both. To learn which resources support conditions in their IAM policies, see the [IAM - documentation](\ https://cloud.google.com/iam/help/conditions/resource-policies). + documentation](https://cloud.google.com/iam/help/conditions/resource-policies). **JSON example:** - :literal:`\` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` + :literal:`` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` **YAML example:** - :literal:`\` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` + :literal:`` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` For a description of IAM and its features, see the [IAM - documentation](\ https://cloud.google.com/iam/docs/). + documentation](https://cloud.google.com/iam/docs/). """ # Create or coerce a protobuf request object. @@ -2704,19 +2704,19 @@ async def sample_set_iam_policy(): constraints based on attributes of the request, the resource, or both. To learn which resources support conditions in their IAM policies, see the [IAM - documentation](\ https://cloud.google.com/iam/help/conditions/resource-policies). + documentation](https://cloud.google.com/iam/help/conditions/resource-policies). **JSON example:** - :literal:`\` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` + :literal:`` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` **YAML example:** - :literal:`\` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` + :literal:`` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` For a description of IAM and its features, see the [IAM - documentation](\ https://cloud.google.com/iam/docs/). + documentation](https://cloud.google.com/iam/docs/). """ # Create or coerce a protobuf request object. diff --git a/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/client.py b/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/client.py index 84df01058..7c72be997 100644 --- a/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/client.py +++ b/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/client.py @@ -3067,19 +3067,19 @@ def sample_get_iam_policy(): constraints based on attributes of the request, the resource, or both. To learn which resources support conditions in their IAM policies, see the [IAM - documentation](\ https://cloud.google.com/iam/help/conditions/resource-policies). + documentation](https://cloud.google.com/iam/help/conditions/resource-policies). **JSON example:** - :literal:`\` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` + :literal:`` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` **YAML example:** - :literal:`\` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` + :literal:`` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` For a description of IAM and its features, see the [IAM - documentation](\ https://cloud.google.com/iam/docs/). + documentation](https://cloud.google.com/iam/docs/). """ # Create or coerce a protobuf request object. @@ -3207,19 +3207,19 @@ def sample_set_iam_policy(): constraints based on attributes of the request, the resource, or both. To learn which resources support conditions in their IAM policies, see the [IAM - documentation](\ https://cloud.google.com/iam/help/conditions/resource-policies). + documentation](https://cloud.google.com/iam/help/conditions/resource-policies). **JSON example:** - :literal:`\` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` + :literal:`` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` **YAML example:** - :literal:`\` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` + :literal:`` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` For a description of IAM and its features, see the [IAM - documentation](\ https://cloud.google.com/iam/docs/). + documentation](https://cloud.google.com/iam/docs/). """ # Create or coerce a protobuf request object. diff --git a/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/base.py b/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/base.py index f5ceeeb68..3a05dd663 100644 --- a/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/base.py +++ b/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/base.py @@ -81,9 +81,10 @@ def __init__( credentials identify the application to the service; if none are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. - This argument is mutually exclusive with credentials. + This argument is mutually exclusive with credentials. This argument will be + removed in the next major version of this library. scopes (Optional[Sequence[str]]): A list of scopes. quota_project_id (Optional[str]): An optional project to use for billing and quota. diff --git a/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc.py b/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc.py index a294144ef..d5d5cf1e5 100644 --- a/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc.py +++ b/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc.py @@ -160,9 +160,10 @@ def __init__( are specified, the client will attempt to ascertain the credentials from the environment. This argument is ignored if a ``channel`` instance is provided. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is ignored if a ``channel`` instance is provided. + This argument will be removed in the next major version of this library. scopes (Optional(Sequence[str])): A list of scopes. This argument is ignored if a ``channel`` instance is provided. channel (Optional[Union[grpc.Channel, Callable[..., grpc.Channel]]]): @@ -296,9 +297,10 @@ def create_channel( credentials identify this application to the service. If none are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. - This argument is mutually exclusive with credentials. + This argument is mutually exclusive with credentials. This argument will be + removed in the next major version of this library. scopes (Optional[Sequence[str]]): A optional list of scopes needed for this service. These are only used when credentials are not specified and are passed to :func:`google.auth.default`. diff --git a/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc_asyncio.py b/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc_asyncio.py index aae0f44c4..7ce762764 100644 --- a/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc_asyncio.py +++ b/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc_asyncio.py @@ -157,8 +157,9 @@ def create_channel( credentials identify this application to the service. If none are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can - be loaded with :func:`google.auth.load_credentials_from_file`. + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. This argument will be + removed in the next major version of this library. scopes (Optional[Sequence[str]]): A optional list of scopes needed for this service. These are only used when credentials are not specified and are passed to :func:`google.auth.default`. @@ -209,9 +210,10 @@ def __init__( are specified, the client will attempt to ascertain the credentials from the environment. This argument is ignored if a ``channel`` instance is provided. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is ignored if a ``channel`` instance is provided. + This argument will be removed in the next major version of this library. scopes (Optional[Sequence[str]]): A optional list of scopes needed for this service. These are only used when credentials are not specified and are passed to :func:`google.auth.default`. diff --git a/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/rest.py b/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/rest.py index 12af0792b..9879c4c45 100644 --- a/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/rest.py +++ b/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/rest.py @@ -1719,9 +1719,10 @@ def __init__( are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. - This argument is ignored if ``channel`` is provided. + This argument is ignored if ``channel`` is provided. This argument will be + removed in the next major version of this library. scopes (Optional(Sequence[str])): A list of scopes. This argument is ignored if ``channel`` is provided. client_cert_source_for_mtls (Callable[[], Tuple[bytes, bytes]]): Client diff --git a/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/async_client.py b/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/async_client.py index d79d1b088..7f772c87c 100644 --- a/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/async_client.py +++ b/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/async_client.py @@ -936,14 +936,14 @@ async def sample_update_table(): specifying which fields (e.g. ``change_stream_config``) in the ``table`` field should be updated. This mask is relative to the ``table`` field, not to the request - message. The wildcard (*) path is currently not + message. The wildcard (\*) path is currently not supported. Currently UpdateTable is only supported for the following fields: - - ``change_stream_config`` - - ``change_stream_config.retention_period`` - - ``deletion_protection`` - - ``row_key_schema`` + - ``change_stream_config`` + - ``change_stream_config.retention_period`` + - ``deletion_protection`` + - ``row_key_schema`` If ``column_families`` is set in ``update_mask``, it will return an UNIMPLEMENTED error. @@ -3044,7 +3044,7 @@ async def sample_create_backup(): full backup name, of the form: ``projects/{project}/instances/{instance}/clusters/{cluster}/backups/{backup_id}``. This string must be between 1 and 50 characters in - length and match the regex [*a-zA-Z0-9][-*.a-zA-Z0-9]*. + length and match the regex [*a-zA-Z0-9][-*.a-zA-Z0-9]\*. This corresponds to the ``backup_id`` field on the ``request`` instance; if ``request`` is provided, this @@ -3293,7 +3293,7 @@ async def sample_update_backup(): required. Other fields are ignored. Update is only supported for the following fields: - - ``backup.expire_time``. + - ``backup.expire_time``. This corresponds to the ``backup`` field on the ``request`` instance; if ``request`` is provided, this @@ -3784,7 +3784,7 @@ async def sample_copy_backup(): name, of the form: ``projects/{project}/instances/{instance}/clusters/{cluster}/backups/{backup_id}``. This string must be between 1 and 50 characters in - length and match the regex [*a-zA-Z0-9][-*.a-zA-Z0-9]*. + length and match the regex [*a-zA-Z0-9][-*.a-zA-Z0-9]\*. This corresponds to the ``backup_id`` field on the ``request`` instance; if ``request`` is provided, this @@ -3971,19 +3971,19 @@ async def sample_get_iam_policy(): constraints based on attributes of the request, the resource, or both. To learn which resources support conditions in their IAM policies, see the [IAM - documentation](\ https://cloud.google.com/iam/help/conditions/resource-policies). + documentation](https://cloud.google.com/iam/help/conditions/resource-policies). **JSON example:** - :literal:`\` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` + :literal:`` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` **YAML example:** - :literal:`\` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` + :literal:`` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` For a description of IAM and its features, see the [IAM - documentation](\ https://cloud.google.com/iam/docs/). + documentation](https://cloud.google.com/iam/docs/). """ # Create or coerce a protobuf request object. @@ -4110,19 +4110,19 @@ async def sample_set_iam_policy(): constraints based on attributes of the request, the resource, or both. To learn which resources support conditions in their IAM policies, see the [IAM - documentation](\ https://cloud.google.com/iam/help/conditions/resource-policies). + documentation](https://cloud.google.com/iam/help/conditions/resource-policies). **JSON example:** - :literal:`\` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` + :literal:`` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` **YAML example:** - :literal:`\` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` + :literal:`` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` For a description of IAM and its features, see the [IAM - documentation](\ https://cloud.google.com/iam/docs/). + documentation](https://cloud.google.com/iam/docs/). """ # Create or coerce a protobuf request object. diff --git a/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/client.py b/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/client.py index d0030af92..4c6aff187 100644 --- a/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/client.py +++ b/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/client.py @@ -1486,14 +1486,14 @@ def sample_update_table(): specifying which fields (e.g. ``change_stream_config``) in the ``table`` field should be updated. This mask is relative to the ``table`` field, not to the request - message. The wildcard (*) path is currently not + message. The wildcard (\*) path is currently not supported. Currently UpdateTable is only supported for the following fields: - - ``change_stream_config`` - - ``change_stream_config.retention_period`` - - ``deletion_protection`` - - ``row_key_schema`` + - ``change_stream_config`` + - ``change_stream_config.retention_period`` + - ``deletion_protection`` + - ``row_key_schema`` If ``column_families`` is set in ``update_mask``, it will return an UNIMPLEMENTED error. @@ -3549,7 +3549,7 @@ def sample_create_backup(): full backup name, of the form: ``projects/{project}/instances/{instance}/clusters/{cluster}/backups/{backup_id}``. This string must be between 1 and 50 characters in - length and match the regex [*a-zA-Z0-9][-*.a-zA-Z0-9]*. + length and match the regex [*a-zA-Z0-9][-*.a-zA-Z0-9]\*. This corresponds to the ``backup_id`` field on the ``request`` instance; if ``request`` is provided, this @@ -3792,7 +3792,7 @@ def sample_update_backup(): required. Other fields are ignored. Update is only supported for the following fields: - - ``backup.expire_time``. + - ``backup.expire_time``. This corresponds to the ``backup`` field on the ``request`` instance; if ``request`` is provided, this @@ -4272,7 +4272,7 @@ def sample_copy_backup(): name, of the form: ``projects/{project}/instances/{instance}/clusters/{cluster}/backups/{backup_id}``. This string must be between 1 and 50 characters in - length and match the regex [*a-zA-Z0-9][-*.a-zA-Z0-9]*. + length and match the regex [*a-zA-Z0-9][-*.a-zA-Z0-9]\*. This corresponds to the ``backup_id`` field on the ``request`` instance; if ``request`` is provided, this @@ -4456,19 +4456,19 @@ def sample_get_iam_policy(): constraints based on attributes of the request, the resource, or both. To learn which resources support conditions in their IAM policies, see the [IAM - documentation](\ https://cloud.google.com/iam/help/conditions/resource-policies). + documentation](https://cloud.google.com/iam/help/conditions/resource-policies). **JSON example:** - :literal:`\` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` + :literal:`` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` **YAML example:** - :literal:`\` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` + :literal:`` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` For a description of IAM and its features, see the [IAM - documentation](\ https://cloud.google.com/iam/docs/). + documentation](https://cloud.google.com/iam/docs/). """ # Create or coerce a protobuf request object. @@ -4596,19 +4596,19 @@ def sample_set_iam_policy(): constraints based on attributes of the request, the resource, or both. To learn which resources support conditions in their IAM policies, see the [IAM - documentation](\ https://cloud.google.com/iam/help/conditions/resource-policies). + documentation](https://cloud.google.com/iam/help/conditions/resource-policies). **JSON example:** - :literal:`\` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` + :literal:`` { "bindings": [ { "role": "roles/resourcemanager.organizationAdmin", "members": [ "user:mike@example.com", "group:admins@example.com", "domain:google.com", "serviceAccount:my-project-id@appspot.gserviceaccount.com" ] }, { "role": "roles/resourcemanager.organizationViewer", "members": [ "user:eve@example.com" ], "condition": { "title": "expirable access", "description": "Does not grant access after Sep 2020", "expression": "request.time < timestamp('2020-10-01T00:00:00.000Z')", } } ], "etag": "BwWWja0YfJA=", "version": 3 }`\ \` **YAML example:** - :literal:`\` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` + :literal:`` bindings: - members: - user:mike@example.com - group:admins@example.com - domain:google.com - serviceAccount:my-project-id@appspot.gserviceaccount.com role: roles/resourcemanager.organizationAdmin - members: - user:eve@example.com role: roles/resourcemanager.organizationViewer condition: title: expirable access description: Does not grant access after Sep 2020 expression: request.time < timestamp('2020-10-01T00:00:00.000Z') etag: BwWWja0YfJA= version: 3`\ \` For a description of IAM and its features, see the [IAM - documentation](\ https://cloud.google.com/iam/docs/). + documentation](https://cloud.google.com/iam/docs/). """ # Create or coerce a protobuf request object. diff --git a/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/base.py b/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/base.py index 8e2cb7304..8ad08df3f 100644 --- a/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/base.py +++ b/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/base.py @@ -81,9 +81,10 @@ def __init__( credentials identify the application to the service; if none are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. - This argument is mutually exclusive with credentials. + This argument is mutually exclusive with credentials. This argument will be + removed in the next major version of this library. scopes (Optional[Sequence[str]]): A list of scopes. quota_project_id (Optional[str]): An optional project to use for billing and quota. diff --git a/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc.py b/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc.py index 5f46c3aa3..f8d1058c8 100644 --- a/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc.py +++ b/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc.py @@ -162,9 +162,10 @@ def __init__( are specified, the client will attempt to ascertain the credentials from the environment. This argument is ignored if a ``channel`` instance is provided. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is ignored if a ``channel`` instance is provided. + This argument will be removed in the next major version of this library. scopes (Optional(Sequence[str])): A list of scopes. This argument is ignored if a ``channel`` instance is provided. channel (Optional[Union[grpc.Channel, Callable[..., grpc.Channel]]]): @@ -298,9 +299,10 @@ def create_channel( credentials identify this application to the service. If none are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. - This argument is mutually exclusive with credentials. + This argument is mutually exclusive with credentials. This argument will be + removed in the next major version of this library. scopes (Optional[Sequence[str]]): A optional list of scopes needed for this service. These are only used when credentials are not specified and are passed to :func:`google.auth.default`. diff --git a/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc_asyncio.py b/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc_asyncio.py index 159a96eda..5017f17d0 100644 --- a/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc_asyncio.py +++ b/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc_asyncio.py @@ -159,8 +159,9 @@ def create_channel( credentials identify this application to the service. If none are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can - be loaded with :func:`google.auth.load_credentials_from_file`. + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. This argument will be + removed in the next major version of this library. scopes (Optional[Sequence[str]]): A optional list of scopes needed for this service. These are only used when credentials are not specified and are passed to :func:`google.auth.default`. @@ -211,9 +212,10 @@ def __init__( are specified, the client will attempt to ascertain the credentials from the environment. This argument is ignored if a ``channel`` instance is provided. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is ignored if a ``channel`` instance is provided. + This argument will be removed in the next major version of this library. scopes (Optional[Sequence[str]]): A optional list of scopes needed for this service. These are only used when credentials are not specified and are passed to :func:`google.auth.default`. diff --git a/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/rest.py b/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/rest.py index ec2462d4a..6c3815f79 100644 --- a/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/rest.py +++ b/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/rest.py @@ -1896,9 +1896,10 @@ def __init__( are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. - This argument is ignored if ``channel`` is provided. + This argument is ignored if ``channel`` is provided. This argument will be + removed in the next major version of this library. scopes (Optional(Sequence[str])): A list of scopes. This argument is ignored if ``channel`` is provided. client_cert_source_for_mtls (Callable[[], Tuple[bytes, bytes]]): Client diff --git a/google/cloud/bigtable_admin_v2/types/bigtable_table_admin.py b/google/cloud/bigtable_admin_v2/types/bigtable_table_admin.py index d6403fc2a..69de07a2a 100644 --- a/google/cloud/bigtable_admin_v2/types/bigtable_table_admin.py +++ b/google/cloud/bigtable_admin_v2/types/bigtable_table_admin.py @@ -235,20 +235,20 @@ class CreateTableRequest(proto.Message): Example: - - Row keys := - ``["a", "apple", "custom", "customer_1", "customer_2",`` - ``"other", "zz"]`` - - initial_split_keys := - ``["apple", "customer_1", "customer_2", "other"]`` - - Key assignment: - - - Tablet 1 ``[, apple) => {"a"}.`` - - Tablet 2 - ``[apple, customer_1) => {"apple", "custom"}.`` - - Tablet 3 - ``[customer_1, customer_2) => {"customer_1"}.`` - - Tablet 4 ``[customer_2, other) => {"customer_2"}.`` - - Tablet 5 ``[other, ) => {"other", "zz"}.`` + - Row keys := + ``["a", "apple", "custom", "customer_1", "customer_2",`` + ``"other", "zz"]`` + - initial_split_keys := + ``["apple", "customer_1", "customer_2", "other"]`` + - Key assignment: + + - Tablet 1 ``[, apple) => {"a"}.`` + - Tablet 2 + ``[apple, customer_1) => {"apple", "custom"}.`` + - Tablet 3 ``[customer_1, customer_2) => {"customer_1"}.`` + - Tablet 4 ``[customer_2, other) => {"customer_2"}.`` + - Tablet 5 + ``[other, ) => {"other", "zz"}.`` """ class Split(proto.Message): @@ -482,13 +482,13 @@ class UpdateTableRequest(proto.Message): which fields (e.g. ``change_stream_config``) in the ``table`` field should be updated. This mask is relative to the ``table`` field, not to the request message. The - wildcard (*) path is currently not supported. Currently + wildcard (\*) path is currently not supported. Currently UpdateTable is only supported for the following fields: - - ``change_stream_config`` - - ``change_stream_config.retention_period`` - - ``deletion_protection`` - - ``row_key_schema`` + - ``change_stream_config`` + - ``change_stream_config.retention_period`` + - ``deletion_protection`` + - ``row_key_schema`` If ``column_families`` is set in ``update_mask``, it will return an UNIMPLEMENTED error. @@ -1099,7 +1099,7 @@ class CreateBackupRequest(proto.Message): name, of the form: ``projects/{project}/instances/{instance}/clusters/{cluster}/backups/{backup_id}``. This string must be between 1 and 50 characters in length - and match the regex [*a-zA-Z0-9][-*.a-zA-Z0-9]*. + and match the regex [*a-zA-Z0-9][-*.a-zA-Z0-9]\*. backup (google.cloud.bigtable_admin_v2.types.Backup): Required. The backup to create. """ @@ -1167,7 +1167,7 @@ class UpdateBackupRequest(proto.Message): required. Other fields are ignored. Update is only supported for the following fields: - - ``backup.expire_time``. + - ``backup.expire_time``. update_mask (google.protobuf.field_mask_pb2.FieldMask): Required. A mask specifying which fields (e.g. ``expire_time``) in the Backup resource should be updated. @@ -1246,16 +1246,16 @@ class ListBackupsRequest(proto.Message): The fields eligible for filtering are: - - ``name`` - - ``source_table`` - - ``state`` - - ``start_time`` (and values are of the format - YYYY-MM-DDTHH:MM:SSZ) - - ``end_time`` (and values are of the format - YYYY-MM-DDTHH:MM:SSZ) - - ``expire_time`` (and values are of the format - YYYY-MM-DDTHH:MM:SSZ) - - ``size_bytes`` + - ``name`` + - ``source_table`` + - ``state`` + - ``start_time`` (and values are of the format + YYYY-MM-DDTHH:MM:SSZ) + - ``end_time`` (and values are of the format + YYYY-MM-DDTHH:MM:SSZ) + - ``expire_time`` (and values are of the format + YYYY-MM-DDTHH:MM:SSZ) + - ``size_bytes`` To filter on multiple expressions, provide each separate expression within parentheses. By default, each expression @@ -1264,20 +1264,20 @@ class ListBackupsRequest(proto.Message): Some examples of using filters are: - - ``name:"exact"`` --> The backup's name is the string - "exact". - - ``name:howl`` --> The backup's name contains the string - "howl". - - ``source_table:prod`` --> The source_table's name - contains the string "prod". - - ``state:CREATING`` --> The backup is pending creation. - - ``state:READY`` --> The backup is fully created and ready - for use. - - ``(name:howl) AND (start_time < \"2018-03-28T14:50:00Z\")`` - --> The backup name contains the string "howl" and - start_time of the backup is before 2018-03-28T14:50:00Z. - - ``size_bytes > 10000000000`` --> The backup's size is - greater than 10GB + - ``name:"exact"`` --> The backup's name is the string + "exact". + - ``name:howl`` --> The backup's name contains the string + "howl". + - ``source_table:prod`` --> The source_table's name contains + the string "prod". + - ``state:CREATING`` --> The backup is pending creation. + - ``state:READY`` --> The backup is fully created and ready + for use. + - ``(name:howl) AND (start_time < \"2018-03-28T14:50:00Z\")`` + --> The backup name contains the string "howl" and + start_time of the backup is before 2018-03-28T14:50:00Z. + - ``size_bytes > 10000000000`` --> The backup's size is + greater than 10GB order_by (str): An expression for specifying the sort order of the results of the request. The string value should specify one or more @@ -1286,13 +1286,13 @@ class ListBackupsRequest(proto.Message): Fields supported are: - - name - - source_table - - expire_time - - start_time - - end_time - - size_bytes - - state + - name + - source_table + - expire_time + - start_time + - end_time + - size_bytes + - state For example, "start_time". The default sorting order is ascending. To specify descending order for the field, a @@ -1381,7 +1381,7 @@ class CopyBackupRequest(proto.Message): to create the full backup name, of the form: ``projects/{project}/instances/{instance}/clusters/{cluster}/backups/{backup_id}``. This string must be between 1 and 50 characters in length - and match the regex [*a-zA-Z0-9][-*.a-zA-Z0-9]*. + and match the regex [*a-zA-Z0-9][-*.a-zA-Z0-9]\*. source_backup (str): Required. The source backup to be copied from. The source backup needs to be in READY state for it to be copied. diff --git a/google/cloud/bigtable_admin_v2/types/instance.py b/google/cloud/bigtable_admin_v2/types/instance.py index 865487f0d..f07414d56 100644 --- a/google/cloud/bigtable_admin_v2/types/instance.py +++ b/google/cloud/bigtable_admin_v2/types/instance.py @@ -67,15 +67,15 @@ class Instance(proto.Message): customer's organizational needs and deployment strategies. They can be used to filter resources and aggregate metrics. - - Label keys must be between 1 and 63 characters long and - must conform to the regular expression: - ``[\p{Ll}\p{Lo}][\p{Ll}\p{Lo}\p{N}_-]{0,62}``. - - Label values must be between 0 and 63 characters long and - must conform to the regular expression: - ``[\p{Ll}\p{Lo}\p{N}_-]{0,63}``. - - No more than 64 labels can be associated with a given - resource. - - Keys and values must both be under 128 bytes. + - Label keys must be between 1 and 63 characters long and + must conform to the regular expression: + ``[\p{Ll}\p{Lo}][\p{Ll}\p{Lo}\p{N}_-]{0,62}``. + - Label values must be between 0 and 63 characters long and + must conform to the regular expression: + ``[\p{Ll}\p{Lo}\p{N}_-]{0,63}``. + - No more than 64 labels can be associated with a given + resource. + - Keys and values must both be under 128 bytes. create_time (google.protobuf.timestamp_pb2.Timestamp): Output only. A commit timestamp representing when this Instance was created. For instances created before this diff --git a/google/cloud/bigtable_admin_v2/types/table.py b/google/cloud/bigtable_admin_v2/types/table.py index c15eac799..f6d1fe729 100644 --- a/google/cloud/bigtable_admin_v2/types/table.py +++ b/google/cloud/bigtable_admin_v2/types/table.py @@ -154,9 +154,9 @@ class Table(proto.Message): i.e. deleting the following resources through Admin APIs are prohibited: - - The table. - - The column families in the table. - - The instance containing the table. + - The table. + - The column families in the table. + - The instance containing the table. Note one can still delete the data stored in the table through Data APIs. @@ -181,37 +181,22 @@ class Table(proto.Message): they encounter an invalid row key. For example, if \_key = - "some_id#2024-04-30#\x00\x13\x00\xf3" with the following - schema: - - .. code-block:: - - { - fields { - field_name: "id" - type { string { encoding: utf8_bytes {} } } - } - fields { - field_name: "date" - type { string { encoding: utf8_bytes {} } } - } - fields { - field_name: "product_code" - type { int64 { encoding: big_endian_bytes {} } } - } - encoding { delimited_bytes { delimiter: "#" } } - } - - The decoded key parts would be: - id = "some_id", date = "2024-04-30", product_code = 1245427 - The query "SELECT \_key, product_code FROM table" will return - two columns: - - +========================================+==============+ - | \_key | product_code | - +========================================+==============+ - | "some_id#2024-04-30#\x00\x13\x00\xf3" | 1245427 | - +----------------------------------------+--------------+ + "some_id#2024-04-30#\\x00\\x13\\x00\\xf3" with the following + schema: { fields { field_name: "id" type { string { + encoding: utf8_bytes {} } } } fields { field_name: "date" + type { string { encoding: utf8_bytes {} } } } fields { + field_name: "product_code" type { int64 { encoding: + big_endian_bytes {} } } } encoding { delimited_bytes { + delimiter: "#" } } } + + | The decoded key parts would be: id = "some_id", date = + "2024-04-30", product_code = 1245427 The query "SELECT + \_key, product_code FROM table" will return two columns: + /------------------------------------------------------ + | \| \_key \| product_code \| \| + --------------------------------------\|--------------\| + \| "some_id#2024-04-30#\\x00\\x13\\x00\\xf3" \| 1245427 \| + ------------------------------------------------------/ The schema has the following invariants: (1) The decoded field values are order-preserved. For read, the field values @@ -221,19 +206,19 @@ class Table(proto.Message): type is limited to scalar types only: Array, Map, Aggregate, and Struct are not allowed. (4) The field names must not collide with existing column family names and reserved - keywords "_key" and "_timestamp". + keywords "\_key" and "\_timestamp". The following update operations are allowed for row_key_schema: - - Update from an empty schema to a new schema. - - Remove the existing schema. This operation requires - setting the ``ignore_warnings`` flag to ``true``, since - it might be a backward incompatible change. Without the - flag, the update request will fail with an - INVALID_ARGUMENT error. Any other row key schema update - operation (e.g. update existing schema columns names or - types) is currently unsupported. + - Update from an empty schema to a new schema. + - Remove the existing schema. This operation requires + setting the ``ignore_warnings`` flag to ``true``, since it + might be a backward incompatible change. Without the flag, + the update request will fail with an INVALID_ARGUMENT + error. Any other row key schema update operation (e.g. + update existing schema columns names or types) is + currently unsupported. """ class TimestampGranularity(proto.Enum): @@ -572,7 +557,7 @@ class ColumnFamily(proto.Message): If ``value_type`` is ``Aggregate``, written data must be compatible with: - - ``value_type.input_type`` for ``AddInput`` mutations + - ``value_type.input_type`` for ``AddInput`` mutations """ gc_rule: "GcRule" = proto.Field( @@ -864,8 +849,8 @@ class Backup(proto.Message): backup or updating its ``expire_time``, the value must be greater than the backup creation time by: - - At least 6 hours - - At most 90 days + - At least 6 hours + - At most 90 days Once the ``expire_time`` has passed, Cloud Bigtable will delete the backup. @@ -895,7 +880,7 @@ class Backup(proto.Message): standard backup. This value must be greater than the backup creation time by: - - At least 24 hours + - At least 24 hours This field only applies for hot backups. When creating or updating a standard backup, attempting to set this field diff --git a/google/cloud/bigtable_admin_v2/types/types.py b/google/cloud/bigtable_admin_v2/types/types.py index b6ea5341d..4f56429da 100644 --- a/google/cloud/bigtable_admin_v2/types/types.py +++ b/google/cloud/bigtable_admin_v2/types/types.py @@ -40,15 +40,15 @@ class Type(proto.Message): Each encoding can operate in one of two modes: - - Sorted: In this mode, Bigtable guarantees that - ``Encode(X) <= Encode(Y)`` if and only if ``X <= Y``. This is - useful anywhere sort order is important, for example when - encoding keys. - - Distinct: In this mode, Bigtable guarantees that if ``X != Y`` - then ``Encode(X) != Encode(Y)``. However, the converse is not - guaranteed. For example, both "{'foo': '1', 'bar': '2'}" and - "{'bar': '2', 'foo': '1'}" are valid encodings of the same JSON - value. + - Sorted: In this mode, Bigtable guarantees that + ``Encode(X) <= Encode(Y)`` if and only if ``X <= Y``. This is + useful anywhere sort order is important, for example when encoding + keys. + - Distinct: In this mode, Bigtable guarantees that if ``X != Y`` + then ``Encode(X) != Encode(Y)``. However, the converse is not + guaranteed. For example, both "{'foo': '1', 'bar': '2'}" and + "{'bar': '2', 'foo': '1'}" are valid encodings of the same JSON + value. The API clearly documents which mode is used wherever an encoding can be configured. Each encoding also documents which values are @@ -205,16 +205,16 @@ class Utf8Bytes(proto.Message): Sorted mode: - - All values are supported. - - Code point order is preserved. + - All values are supported. + - Code point order is preserved. Distinct mode: all values are supported. Compatible with: - - BigQuery ``TEXT`` encoding - - HBase ``Bytes.toBytes`` - - Java ``String#getBytes(StandardCharsets.UTF_8)`` + - BigQuery ``TEXT`` encoding + - HBase ``Bytes.toBytes`` + - Java ``String#getBytes(StandardCharsets.UTF_8)`` """ @@ -276,9 +276,9 @@ class BigEndianBytes(proto.Message): Compatible with: - - BigQuery ``BINARY`` encoding - - HBase ``Bytes.toBytes`` - - Java ``ByteBuffer.putLong()`` with ``ByteOrder.BIG_ENDIAN`` + - BigQuery ``BINARY`` encoding + - HBase ``Bytes.toBytes`` + - Java ``ByteBuffer.putLong()`` with ``ByteOrder.BIG_ENDIAN`` Attributes: bytes_type (google.cloud.bigtable_admin_v2.types.Type.Bytes): @@ -358,7 +358,7 @@ class Encoding(proto.Message): Compatible with: - - Java ``Instant.truncatedTo()`` with ``ChronoUnit.MICROS`` + - Java ``Instant.truncatedTo()`` with ``ChronoUnit.MICROS`` This field is a member of `oneof`_ ``encoding``. """ @@ -455,17 +455,17 @@ class DelimitedBytes(proto.Message): Sorted mode: - - Fields are encoded in sorted mode. - - Encoded field values must not contain any bytes <= - ``delimiter[0]`` - - Element-wise order is preserved: ``A < B`` if ``A[0] < B[0]``, or - if ``A[0] == B[0] && A[1] < B[1]``, etc. Strict prefixes sort - first. + - Fields are encoded in sorted mode. + - Encoded field values must not contain any bytes <= + ``delimiter[0]`` + - Element-wise order is preserved: ``A < B`` if ``A[0] < B[0]``, or + if ``A[0] == B[0] && A[1] < B[1]``, etc. Strict prefixes sort + first. Distinct mode: - - Fields are encoded in distinct mode. - - Encoded field values must not contain ``delimiter[0]``. + - Fields are encoded in distinct mode. + - Encoded field values must not contain ``delimiter[0]``. Attributes: delimiter (bytes): @@ -488,24 +488,23 @@ class OrderedCodeBytes(proto.Message): Fields that encode to the empty string "" have special handling: - - If *every* field encodes to "", or if the STRUCT has no fields - defined, then the STRUCT is encoded as the fixed byte pair {0x00, - 0x00}. - - Otherwise, the STRUCT only encodes until the last non-empty - field, omitting any trailing empty fields. Any empty fields that - aren't omitted are replaced with the fixed byte pair {0x00, - 0x00}. + - If *every* field encodes to "", or if the STRUCT has no fields + defined, then the STRUCT is encoded as the fixed byte pair {0x00, + 0x00}. + - Otherwise, the STRUCT only encodes until the last non-empty field, + omitting any trailing empty fields. Any empty fields that aren't + omitted are replaced with the fixed byte pair {0x00, 0x00}. Examples: - - STRUCT() -> "\00\00" - - STRUCT("") -> "\00\00" - - STRUCT("", "") -> "\00\00" - - STRUCT("", "B") -> "\00\00" + "\00\01" + "B" - - STRUCT("A", "") -> "A" - - STRUCT("", "B", "") -> "\00\00" + "\00\01" + "B" - - STRUCT("A", "", "C") -> "A" + "\00\01" + "\00\00" + "\00\01" + - "C" + - STRUCT() -> "\\00\\00" + - STRUCT("") -> "\\00\\00" + - STRUCT("", "") -> "\\00\\00" + - STRUCT("", "B") -> "\\00\\00" + "\\00\\01" + "B" + - STRUCT("A", "") -> "A" + - STRUCT("", "B", "") -> "\\00\\00" + "\\00\\01" + "B" + - STRUCT("A", "", "C") -> "A" + "\\00\\01" + "\\00\\00" + "\\00\\01" + + "C" Since null bytes are always escaped, this encoding can cause size blowup for encodings like ``Int64.BigEndianBytes`` that are likely @@ -513,16 +512,16 @@ class OrderedCodeBytes(proto.Message): Sorted mode: - - Fields are encoded in sorted mode. - - All values supported by the field encodings are allowed - - Element-wise order is preserved: ``A < B`` if ``A[0] < B[0]``, or - if ``A[0] == B[0] && A[1] < B[1]``, etc. Strict prefixes sort - first. + - Fields are encoded in sorted mode. + - All values supported by the field encodings are allowed + - Element-wise order is preserved: ``A < B`` if ``A[0] < B[0]``, or + if ``A[0] == B[0] && A[1] < B[1]``, etc. Strict prefixes sort + first. Distinct mode: - - Fields are encoded in distinct mode. - - All values supported by the field encodings are allowed. + - Fields are encoded in distinct mode. + - All values supported by the field encodings are allowed. """ diff --git a/google/cloud/bigtable_v2/__init__.py b/google/cloud/bigtable_v2/__init__.py index 3a5a72c9c..a14cdab5f 100644 --- a/google/cloud/bigtable_v2/__init__.py +++ b/google/cloud/bigtable_v2/__init__.py @@ -70,6 +70,7 @@ from .types.data import Value from .types.data import ValueRange from .types.feature_flags import FeatureFlags +from .types.peer_info import PeerInfo from .types.request_stats import FullReadStatsView from .types.request_stats import ReadIterationStats from .types.request_stats import RequestLatencyStats @@ -101,6 +102,7 @@ "MutateRowsResponse", "Mutation", "PartialResultSet", + "PeerInfo", "PingAndWarmRequest", "PingAndWarmResponse", "PrepareQueryRequest", diff --git a/google/cloud/bigtable_v2/services/bigtable/async_client.py b/google/cloud/bigtable_v2/services/bigtable/async_client.py index 103ff141c..0a9442287 100644 --- a/google/cloud/bigtable_v2/services/bigtable/async_client.py +++ b/google/cloud/bigtable_v2/services/bigtable/async_client.py @@ -397,6 +397,13 @@ def read_rows( if regex_match and regex_match.group("table_name"): header_params["table_name"] = regex_match.group("table_name") + routing_param_regex = re.compile( + "^(?Pprojects/[^/]+/instances/[^/]+)(?:/.*)?$" + ) + regex_match = routing_param_regex.match(request.materialized_view_name) + if regex_match and regex_match.group("name"): + header_params["name"] = regex_match.group("name") + if header_params: metadata = tuple(metadata) + ( gapic_v1.routing_header.to_grpc_metadata(header_params), @@ -519,6 +526,13 @@ def sample_row_keys( if regex_match and regex_match.group("table_name"): header_params["table_name"] = regex_match.group("table_name") + routing_param_regex = re.compile( + "^(?Pprojects/[^/]+/instances/[^/]+)(?:/.*)?$" + ) + regex_match = routing_param_regex.match(request.materialized_view_name) + if regex_match and regex_match.group("name"): + header_params["name"] = regex_match.group("name") + if header_params: metadata = tuple(metadata) + ( gapic_v1.routing_header.to_grpc_metadata(header_params), diff --git a/google/cloud/bigtable_v2/services/bigtable/client.py b/google/cloud/bigtable_v2/services/bigtable/client.py index ffc448c25..d8e85f54e 100644 --- a/google/cloud/bigtable_v2/services/bigtable/client.py +++ b/google/cloud/bigtable_v2/services/bigtable/client.py @@ -873,6 +873,13 @@ def read_rows( if regex_match and regex_match.group("table_name"): header_params["table_name"] = regex_match.group("table_name") + routing_param_regex = re.compile( + "^(?Pprojects/[^/]+/instances/[^/]+)(?:/.*)?$" + ) + regex_match = routing_param_regex.match(request.materialized_view_name) + if regex_match and regex_match.group("name"): + header_params["name"] = regex_match.group("name") + if header_params: metadata = tuple(metadata) + ( gapic_v1.routing_header.to_grpc_metadata(header_params), @@ -992,6 +999,13 @@ def sample_row_keys( if regex_match and regex_match.group("table_name"): header_params["table_name"] = regex_match.group("table_name") + routing_param_regex = re.compile( + "^(?Pprojects/[^/]+/instances/[^/]+)(?:/.*)?$" + ) + regex_match = routing_param_regex.match(request.materialized_view_name) + if regex_match and regex_match.group("name"): + header_params["name"] = regex_match.group("name") + if header_params: metadata = tuple(metadata) + ( gapic_v1.routing_header.to_grpc_metadata(header_params), diff --git a/google/cloud/bigtable_v2/services/bigtable/transports/base.py b/google/cloud/bigtable_v2/services/bigtable/transports/base.py index 4d25d8b30..f08bca73e 100644 --- a/google/cloud/bigtable_v2/services/bigtable/transports/base.py +++ b/google/cloud/bigtable_v2/services/bigtable/transports/base.py @@ -74,9 +74,10 @@ def __init__( credentials identify the application to the service; if none are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. - This argument is mutually exclusive with credentials. + This argument is mutually exclusive with credentials. This argument will be + removed in the next major version of this library. scopes (Optional[Sequence[str]]): A list of scopes. quota_project_id (Optional[str]): An optional project to use for billing and quota. diff --git a/google/cloud/bigtable_v2/services/bigtable/transports/grpc.py b/google/cloud/bigtable_v2/services/bigtable/transports/grpc.py index 309e72662..8ddbf15a2 100644 --- a/google/cloud/bigtable_v2/services/bigtable/transports/grpc.py +++ b/google/cloud/bigtable_v2/services/bigtable/transports/grpc.py @@ -152,9 +152,10 @@ def __init__( are specified, the client will attempt to ascertain the credentials from the environment. This argument is ignored if a ``channel`` instance is provided. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is ignored if a ``channel`` instance is provided. + This argument will be removed in the next major version of this library. scopes (Optional(Sequence[str])): A list of scopes. This argument is ignored if a ``channel`` instance is provided. channel (Optional[Union[grpc.Channel, Callable[..., grpc.Channel]]]): @@ -287,9 +288,10 @@ def create_channel( credentials identify this application to the service. If none are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. - This argument is mutually exclusive with credentials. + This argument is mutually exclusive with credentials. This argument will be + removed in the next major version of this library. scopes (Optional[Sequence[str]]): A optional list of scopes needed for this service. These are only used when credentials are not specified and are passed to :func:`google.auth.default`. diff --git a/google/cloud/bigtable_v2/services/bigtable/transports/grpc_asyncio.py b/google/cloud/bigtable_v2/services/bigtable/transports/grpc_asyncio.py index 49f981d9a..3e6b70832 100644 --- a/google/cloud/bigtable_v2/services/bigtable/transports/grpc_asyncio.py +++ b/google/cloud/bigtable_v2/services/bigtable/transports/grpc_asyncio.py @@ -149,8 +149,9 @@ def create_channel( credentials identify this application to the service. If none are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can - be loaded with :func:`google.auth.load_credentials_from_file`. + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. This argument will be + removed in the next major version of this library. scopes (Optional[Sequence[str]]): A optional list of scopes needed for this service. These are only used when credentials are not specified and are passed to :func:`google.auth.default`. @@ -201,9 +202,10 @@ def __init__( are specified, the client will attempt to ascertain the credentials from the environment. This argument is ignored if a ``channel`` instance is provided. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is ignored if a ``channel`` instance is provided. + This argument will be removed in the next major version of this library. scopes (Optional[Sequence[str]]): A optional list of scopes needed for this service. These are only used when credentials are not specified and are passed to :func:`google.auth.default`. @@ -290,6 +292,7 @@ def __init__( always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, ) + if not self._grpc_channel: # initialize with the provided callable or the default channel channel_init = channel or type(self).create_channel diff --git a/google/cloud/bigtable_v2/services/bigtable/transports/rest.py b/google/cloud/bigtable_v2/services/bigtable/transports/rest.py index c84ef147f..f0a761a36 100644 --- a/google/cloud/bigtable_v2/services/bigtable/transports/rest.py +++ b/google/cloud/bigtable_v2/services/bigtable/transports/rest.py @@ -750,9 +750,10 @@ def __init__( are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. - This argument is ignored if ``channel`` is provided. + This argument is ignored if ``channel`` is provided. This argument will be + removed in the next major version of this library. scopes (Optional(Sequence[str])): A list of scopes. This argument is ignored if ``channel`` is provided. client_cert_source_for_mtls (Callable[[], Tuple[bytes, bytes]]): Client @@ -1080,6 +1081,22 @@ def __call__( resp, _ = self._interceptor.post_execute_query_with_metadata( resp, response_metadata ) + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + logging.DEBUG + ): # pragma: NO COVER + http_response = { + "headers": dict(response.headers), + "status": response.status_code, + } + _LOGGER.debug( + "Received response for google.bigtable_v2.BigtableClient.execute_query", + extra={ + "serviceName": "google.bigtable.v2.Bigtable", + "rpcName": "ExecuteQuery", + "metadata": http_response["headers"], + "httpResponse": http_response, + }, + ) return resp class _GenerateInitialChangeStreamPartitions( @@ -1228,6 +1245,22 @@ def __call__( ) = self._interceptor.post_generate_initial_change_stream_partitions_with_metadata( resp, response_metadata ) + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + logging.DEBUG + ): # pragma: NO COVER + http_response = { + "headers": dict(response.headers), + "status": response.status_code, + } + _LOGGER.debug( + "Received response for google.bigtable_v2.BigtableClient.generate_initial_change_stream_partitions", + extra={ + "serviceName": "google.bigtable.v2.Bigtable", + "rpcName": "GenerateInitialChangeStreamPartitions", + "metadata": http_response["headers"], + "httpResponse": http_response, + }, + ) return resp class _MutateRow(_BaseBigtableRestTransport._BaseMutateRow, BigtableRestStub): @@ -1515,6 +1548,22 @@ def __call__( resp, _ = self._interceptor.post_mutate_rows_with_metadata( resp, response_metadata ) + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + logging.DEBUG + ): # pragma: NO COVER + http_response = { + "headers": dict(response.headers), + "status": response.status_code, + } + _LOGGER.debug( + "Received response for google.bigtable_v2.BigtableClient.mutate_rows", + extra={ + "serviceName": "google.bigtable.v2.Bigtable", + "rpcName": "MutateRows", + "metadata": http_response["headers"], + "httpResponse": http_response, + }, + ) return resp class _PingAndWarm(_BaseBigtableRestTransport._BasePingAndWarm, BigtableRestStub): @@ -1966,6 +2015,22 @@ def __call__( resp, _ = self._interceptor.post_read_change_stream_with_metadata( resp, response_metadata ) + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + logging.DEBUG + ): # pragma: NO COVER + http_response = { + "headers": dict(response.headers), + "status": response.status_code, + } + _LOGGER.debug( + "Received response for google.bigtable_v2.BigtableClient.read_change_stream", + extra={ + "serviceName": "google.bigtable.v2.Bigtable", + "rpcName": "ReadChangeStream", + "metadata": http_response["headers"], + "httpResponse": http_response, + }, + ) return resp class _ReadModifyWriteRow( @@ -2253,6 +2318,22 @@ def __call__( resp, _ = self._interceptor.post_read_rows_with_metadata( resp, response_metadata ) + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + logging.DEBUG + ): # pragma: NO COVER + http_response = { + "headers": dict(response.headers), + "status": response.status_code, + } + _LOGGER.debug( + "Received response for google.bigtable_v2.BigtableClient.read_rows", + extra={ + "serviceName": "google.bigtable.v2.Bigtable", + "rpcName": "ReadRows", + "metadata": http_response["headers"], + "httpResponse": http_response, + }, + ) return resp class _SampleRowKeys( @@ -2383,6 +2464,22 @@ def __call__( resp, _ = self._interceptor.post_sample_row_keys_with_metadata( resp, response_metadata ) + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( + logging.DEBUG + ): # pragma: NO COVER + http_response = { + "headers": dict(response.headers), + "status": response.status_code, + } + _LOGGER.debug( + "Received response for google.bigtable_v2.BigtableClient.sample_row_keys", + extra={ + "serviceName": "google.bigtable.v2.Bigtable", + "rpcName": "SampleRowKeys", + "metadata": http_response["headers"], + "httpResponse": http_response, + }, + ) return resp @property diff --git a/google/cloud/bigtable_v2/services/bigtable/transports/rest_base.py b/google/cloud/bigtable_v2/services/bigtable/transports/rest_base.py index b2080f4a4..5eab0ded4 100644 --- a/google/cloud/bigtable_v2/services/bigtable/transports/rest_base.py +++ b/google/cloud/bigtable_v2/services/bigtable/transports/rest_base.py @@ -641,6 +641,11 @@ def _get_http_options(): "uri": "/v2/{authorized_view_name=projects/*/instances/*/tables/*/authorizedViews/*}:readRows", "body": "*", }, + { + "method": "post", + "uri": "/v2/{materialized_view_name=projects/*/instances/*/materializedViews/*}:readRows", + "body": "*", + }, ] return http_options @@ -686,6 +691,10 @@ def _get_http_options(): "method": "get", "uri": "/v2/{authorized_view_name=projects/*/instances/*/tables/*/authorizedViews/*}:sampleRowKeys", }, + { + "method": "get", + "uri": "/v2/{materialized_view_name=projects/*/instances/*/materializedViews/*}:sampleRowKeys", + }, ] return http_options diff --git a/google/cloud/bigtable_v2/types/__init__.py b/google/cloud/bigtable_v2/types/__init__.py index bd3c36154..b13c076a2 100644 --- a/google/cloud/bigtable_v2/types/__init__.py +++ b/google/cloud/bigtable_v2/types/__init__.py @@ -68,6 +68,9 @@ from .feature_flags import ( FeatureFlags, ) +from .peer_info import ( + PeerInfo, +) from .request_stats import ( FullReadStatsView, ReadIterationStats, @@ -131,6 +134,7 @@ "Value", "ValueRange", "FeatureFlags", + "PeerInfo", "FullReadStatsView", "ReadIterationStats", "RequestLatencyStats", diff --git a/google/cloud/bigtable_v2/types/bigtable.py b/google/cloud/bigtable_v2/types/bigtable.py index 0e7ac1df3..19abba67b 100644 --- a/google/cloud/bigtable_v2/types/bigtable.py +++ b/google/cloud/bigtable_v2/types/bigtable.py @@ -1330,10 +1330,10 @@ class ExecuteQueryRequest(proto.Message): Setting this field also places restrictions on several other fields: - - ``data_format`` must be empty. - - ``validate_only`` must be false. - - ``params`` must match the ``param_types`` set in the - ``PrepareQueryRequest``. + - ``data_format`` must be empty. + - ``validate_only`` must be false. + - ``params`` must match the ``param_types`` set in the + ``PrepareQueryRequest``. proto_format (google.cloud.bigtable_v2.types.ProtoFormat): Protocol buffer format as described by ProtoSchema and ProtoRows messages. diff --git a/google/cloud/bigtable_v2/types/data.py b/google/cloud/bigtable_v2/types/data.py index ad7e382f7..12ac8b2b1 100644 --- a/google/cloud/bigtable_v2/types/data.py +++ b/google/cloud/bigtable_v2/types/data.py @@ -573,26 +573,26 @@ class RowFilter(proto.Message): transformers), as well as two ways to compose simple filters into more complex ones (chains and interleaves). They work as follows: - - True filters alter the input row by excluding some of its cells - wholesale from the output row. An example of a true filter is the - ``value_regex_filter``, which excludes cells whose values don't - match the specified pattern. All regex true filters use RE2 - syntax (https://github.com/google/re2/wiki/Syntax) in raw byte - mode (RE2::Latin1), and are evaluated as full matches. An - important point to keep in mind is that ``RE2(.)`` is equivalent - by default to ``RE2([^\n])``, meaning that it does not match - newlines. When attempting to match an arbitrary byte, you should - therefore use the escape sequence ``\C``, which may need to be - further escaped as ``\\C`` in your client language. - - - Transformers alter the input row by changing the values of some - of its cells in the output, without excluding them completely. - Currently, the only supported transformer is the - ``strip_value_transformer``, which replaces every cell's value - with the empty string. - - - Chains and interleaves are described in more detail in the - RowFilter.Chain and RowFilter.Interleave documentation. + - True filters alter the input row by excluding some of its cells + wholesale from the output row. An example of a true filter is the + ``value_regex_filter``, which excludes cells whose values don't + match the specified pattern. All regex true filters use RE2 syntax + (https://github.com/google/re2/wiki/Syntax) in raw byte mode + (RE2::Latin1), and are evaluated as full matches. An important + point to keep in mind is that ``RE2(.)`` is equivalent by default + to ``RE2([^\n])``, meaning that it does not match newlines. When + attempting to match an arbitrary byte, you should therefore use + the escape sequence ``\C``, which may need to be further escaped + as ``\\C`` in your client language. + + - Transformers alter the input row by changing the values of some of + its cells in the output, without excluding them completely. + Currently, the only supported transformer is the + ``strip_value_transformer``, which replaces every cell's value + with the empty string. + + - Chains and interleaves are described in more detail in the + RowFilter.Chain and RowFilter.Interleave documentation. The total serialized size of a RowFilter message must not exceed 20480 bytes, and RowFilters may not be nested within each other (in @@ -1493,21 +1493,20 @@ class PartialResultSet(proto.Message): Having: - - queue of row results waiting to be returned ``queue`` - - extensible buffer of bytes ``buffer`` - - a place to keep track of the most recent ``resume_token`` for - each PartialResultSet ``p`` received { if p.reset { ensure - ``queue`` is empty ensure ``buffer`` is empty } if - p.estimated_batch_size != 0 { (optional) ensure ``buffer`` is - sized to at least ``p.estimated_batch_size`` } if - ``p.proto_rows_batch`` is set { append - ``p.proto_rows_batch.bytes`` to ``buffer`` } if p.batch_checksum - is set and ``buffer`` is not empty { validate the checksum - matches the contents of ``buffer`` (see comments on - ``batch_checksum``) parse ``buffer`` as ``ProtoRows`` message, - clearing ``buffer`` add parsed rows to end of ``queue`` } if - p.resume_token is set { release results in ``queue`` save - ``p.resume_token`` in ``resume_token`` } } + - queue of row results waiting to be returned ``queue`` + - extensible buffer of bytes ``buffer`` + - a place to keep track of the most recent ``resume_token`` for each + PartialResultSet ``p`` received { if p.reset { ensure ``queue`` is + empty ensure ``buffer`` is empty } if p.estimated_batch_size != 0 + { (optional) ensure ``buffer`` is sized to at least + ``p.estimated_batch_size`` } if ``p.proto_rows_batch`` is set { + append ``p.proto_rows_batch.bytes`` to ``buffer`` } if + p.batch_checksum is set and ``buffer`` is not empty { validate the + checksum matches the contents of ``buffer`` (see comments on + ``batch_checksum``) parse ``buffer`` as ``ProtoRows`` message, + clearing ``buffer`` add parsed rows to end of ``queue`` } if + p.resume_token is set { release results in ``queue`` save + ``p.resume_token`` in ``resume_token`` } } .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields diff --git a/google/cloud/bigtable_v2/types/feature_flags.py b/google/cloud/bigtable_v2/types/feature_flags.py index 69cfe1cf4..2c8ea8732 100644 --- a/google/cloud/bigtable_v2/types/feature_flags.py +++ b/google/cloud/bigtable_v2/types/feature_flags.py @@ -76,6 +76,9 @@ class FeatureFlags(proto.Message): direct_access_requested (bool): Notify the server that the client explicitly opted in for Direct Access. + peer_info (bool): + If the client can support using + BigtablePeerInfo. """ reverse_scans: bool = proto.Field( @@ -114,6 +117,10 @@ class FeatureFlags(proto.Message): proto.BOOL, number=10, ) + peer_info: bool = proto.Field( + proto.BOOL, + number=11, + ) __all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/bigtable_v2/types/peer_info.py b/google/cloud/bigtable_v2/types/peer_info.py new file mode 100644 index 000000000..b3f1203cc --- /dev/null +++ b/google/cloud/bigtable_v2/types/peer_info.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# 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. +# +from __future__ import annotations + +from typing import MutableMapping, MutableSequence + +import proto # type: ignore + + +__protobuf__ = proto.module( + package="google.bigtable.v2", + manifest={ + "PeerInfo", + }, +) + + +class PeerInfo(proto.Message): + r"""PeerInfo contains information about the peer that the client + is connecting to. + + Attributes: + google_frontend_id (int): + An opaque identifier for the Google Frontend + which serviced this request. Only set when not + using DirectAccess. + application_frontend_id (int): + An opaque identifier for the application + frontend which serviced this request. + application_frontend_zone (str): + The Cloud zone of the application frontend + that served this request. + application_frontend_subzone (str): + The subzone of the application frontend that + served this request, e.g. an identifier for + where within the zone the application frontend + is. + transport_type (google.cloud.bigtable_v2.types.PeerInfo.TransportType): + + """ + + class TransportType(proto.Enum): + r"""The transport type that the client used to connect to this + peer. + + Values: + TRANSPORT_TYPE_UNKNOWN (0): + The transport type is unknown. + TRANSPORT_TYPE_EXTERNAL (1): + The client connected to this peer via an + external network (e.g. outside Google Coud). + TRANSPORT_TYPE_CLOUD_PATH (2): + The client connected to this peer via + CloudPath. + TRANSPORT_TYPE_DIRECT_ACCESS (3): + The client connected to this peer via + DirectAccess. + TRANSPORT_TYPE_SESSION_UNKNOWN (4): + The client connected to this peer via + Bigtable Sessions using an unknown transport + type. + TRANSPORT_TYPE_SESSION_EXTERNAL (5): + The client connected to this peer via + Bigtable Sessions on an external network (e.g. + outside Google Cloud). + TRANSPORT_TYPE_SESSION_CLOUD_PATH (6): + The client connected to this peer via + Bigtable Sessions using CloudPath. + TRANSPORT_TYPE_SESSION_DIRECT_ACCESS (7): + The client connected to this peer via + Bigtable Sessions using DirectAccess. + """ + TRANSPORT_TYPE_UNKNOWN = 0 + TRANSPORT_TYPE_EXTERNAL = 1 + TRANSPORT_TYPE_CLOUD_PATH = 2 + TRANSPORT_TYPE_DIRECT_ACCESS = 3 + TRANSPORT_TYPE_SESSION_UNKNOWN = 4 + TRANSPORT_TYPE_SESSION_EXTERNAL = 5 + TRANSPORT_TYPE_SESSION_CLOUD_PATH = 6 + TRANSPORT_TYPE_SESSION_DIRECT_ACCESS = 7 + + google_frontend_id: int = proto.Field( + proto.INT64, + number=1, + ) + application_frontend_id: int = proto.Field( + proto.INT64, + number=2, + ) + application_frontend_zone: str = proto.Field( + proto.STRING, + number=3, + ) + application_frontend_subzone: str = proto.Field( + proto.STRING, + number=4, + ) + transport_type: TransportType = proto.Field( + proto.ENUM, + number=5, + enum=TransportType, + ) + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/bigtable_v2/types/response_params.py b/google/cloud/bigtable_v2/types/response_params.py index fb373d055..cc6384ab3 100644 --- a/google/cloud/bigtable_v2/types/response_params.py +++ b/google/cloud/bigtable_v2/types/response_params.py @@ -44,6 +44,11 @@ class ResponseParams(proto.Message): of bigtable resources. This field is a member of `oneof`_ ``_cluster_id``. + afe_id (int): + The AFE ID for the AFE that is served this + request. + + This field is a member of `oneof`_ ``_afe_id``. """ zone_id: str = proto.Field( @@ -56,6 +61,11 @@ class ResponseParams(proto.Message): number=2, optional=True, ) + afe_id: int = proto.Field( + proto.INT64, + number=3, + optional=True, + ) __all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/bigtable_v2/types/types.py b/google/cloud/bigtable_v2/types/types.py index 5eae9e526..0b4ddb57a 100644 --- a/google/cloud/bigtable_v2/types/types.py +++ b/google/cloud/bigtable_v2/types/types.py @@ -35,34 +35,27 @@ class Type(proto.Message): features. For compatibility with Bigtable's existing untyped APIs, each - ``Type`` includes an ``Encoding`` which describes how to convert - to/from the underlying data. - - Each encoding also defines the following properties: - - - Order-preserving: Does the encoded value sort consistently with - the original typed value? Note that Bigtable will always sort - data based on the raw encoded value, *not* the decoded type. - - - Example: BYTES values sort in the same order as their raw - encodings. - - Counterexample: Encoding INT64 as a fixed-width decimal string - does *not* preserve sort order when dealing with negative - numbers. ``INT64(1) > INT64(-1)``, but - ``STRING("-00001") > STRING("00001)``. - - - Self-delimiting: If we concatenate two encoded values, can we - always tell where the first one ends and the second one begins? - - - Example: If we encode INT64s to fixed-width STRINGs, the first - value will always contain exactly N digits, possibly preceded - by a sign. - - Counterexample: If we concatenate two UTF-8 encoded STRINGs, - we have no way to tell where the first one ends. - - - Compatibility: Which other systems have matching encoding - schemes? For example, does this encoding have a GoogleSQL - equivalent? HBase? Java? + ``Type`` includes an ``Encoding`` which describes how to convert to + or from the underlying data. + + Each encoding can operate in one of two modes: + + - Sorted: In this mode, Bigtable guarantees that + ``Encode(X) <= Encode(Y)`` if and only if ``X <= Y``. This is + useful anywhere sort order is important, for example when encoding + keys. + - Distinct: In this mode, Bigtable guarantees that if ``X != Y`` + then ``Encode(X) != Encode(Y)``. However, the converse is not + guaranteed. For example, both ``{'foo': '1', 'bar': '2'}`` and + ``{'bar': '2', 'foo': '1'}`` are valid encodings of the same JSON + value. + + The API clearly documents which mode is used wherever an encoding + can be configured. Each encoding also documents which values are + supported in which modes. For example, when encoding INT64 as a + numeric STRING, negative numbers cannot be encoded in sorted mode. + This is because ``INT64(1) > INT64(-1)``, but + ``STRING("-00001") > STRING("00001")``. This message has `oneof`_ fields (mutually exclusive fields). For each oneof, at most one member field can be set at the same time. @@ -135,12 +128,12 @@ class Bytes(proto.Message): Attributes: encoding (google.cloud.bigtable_v2.types.Type.Bytes.Encoding): - The encoding to use when converting to/from - lower level types. + The encoding to use when converting to or + from lower level types. """ class Encoding(proto.Message): - r"""Rules used to convert to/from lower level types. + r"""Rules used to convert to or from lower level types. .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields @@ -152,14 +145,26 @@ class Encoding(proto.Message): """ class Raw(proto.Message): - r"""Leaves the value "as-is" + r"""Leaves the value as-is. + + Sorted mode: all values are supported. + + Distinct mode: all values are supported. - - Order-preserving? Yes - - Self-delimiting? No - - Compatibility? N/A + Attributes: + escape_nulls (bool): + If set, allows NULL values to be encoded as the empty string + "". + The actual empty string, or any value which only contains + the null byte ``0x00``, has one more null byte appended. """ + escape_nulls: bool = proto.Field( + proto.BOOL, + number=1, + ) + raw: "Type.Bytes.Encoding.Raw" = proto.Field( proto.MESSAGE, number=1, @@ -179,12 +184,12 @@ class String(proto.Message): Attributes: encoding (google.cloud.bigtable_v2.types.Type.String.Encoding): - The encoding to use when converting to/from - lower level types. + The encoding to use when converting to or + from lower level types. """ class Encoding(proto.Message): - r"""Rules used to convert to/from lower level types. + r"""Rules used to convert to or from lower level types. This message has `oneof`_ fields (mutually exclusive fields). For each oneof, at most one member field can be set at the same time. @@ -208,18 +213,45 @@ class Utf8Raw(proto.Message): r"""Deprecated: prefer the equivalent ``Utf8Bytes``.""" class Utf8Bytes(proto.Message): - r"""UTF-8 encoding + r"""UTF-8 encoding. - - Order-preserving? Yes (code point order) - - Self-delimiting? No - - Compatibility? + Sorted mode: - - BigQuery Federation ``TEXT`` encoding - - HBase ``Bytes.toBytes`` - - Java ``String#getBytes(StandardCharsets.UTF_8)`` + - All values are supported. + - Code point order is preserved. + Distinct mode: all values are supported. + + Compatible with: + + - BigQuery ``TEXT`` encoding + - HBase ``Bytes.toBytes`` + - Java ``String#getBytes(StandardCharsets.UTF_8)`` + + Attributes: + null_escape_char (str): + Single-character escape sequence used to support NULL + values. + + If set, allows NULL values to be encoded as the empty string + "". + + The actual empty string, or any value where every character + equals ``null_escape_char``, has one more + ``null_escape_char`` appended. + + If ``null_escape_char`` is set and does not equal the ASCII + null character ``0x00``, then the encoding will not support + sorted mode. + + . """ + null_escape_char: str = proto.Field( + proto.STRING, + number=1, + ) + utf8_raw: "Type.String.Encoding.Utf8Raw" = proto.Field( proto.MESSAGE, number=1, @@ -244,12 +276,17 @@ class Int64(proto.Message): Attributes: encoding (google.cloud.bigtable_v2.types.Type.Int64.Encoding): - The encoding to use when converting to/from - lower level types. + The encoding to use when converting to or + from lower level types. """ class Encoding(proto.Message): - r"""Rules used to convert to/from lower level types. + r"""Rules used to convert to or from lower level types. + + This message has `oneof`_ fields (mutually exclusive fields). + For each oneof, at most one member field can be set at the same time. + Setting any member of the oneof automatically clears all other + members. .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields @@ -257,20 +294,25 @@ class Encoding(proto.Message): big_endian_bytes (google.cloud.bigtable_v2.types.Type.Int64.Encoding.BigEndianBytes): Use ``BigEndianBytes`` encoding. + This field is a member of `oneof`_ ``encoding``. + ordered_code_bytes (google.cloud.bigtable_v2.types.Type.Int64.Encoding.OrderedCodeBytes): + Use ``OrderedCodeBytes`` encoding. + This field is a member of `oneof`_ ``encoding``. """ class BigEndianBytes(proto.Message): - r"""Encodes the value as an 8-byte big endian twos complement ``Bytes`` - value. + r"""Encodes the value as an 8-byte big-endian two's complement value. + + Sorted mode: non-negative values are supported. - - Order-preserving? No (positive values only) - - Self-delimiting? Yes - - Compatibility? + Distinct mode: all values are supported. - - BigQuery Federation ``BINARY`` encoding - - HBase ``Bytes.toBytes`` - - Java ``ByteBuffer.putLong()`` with ``ByteOrder.BIG_ENDIAN`` + Compatible with: + + - BigQuery ``BINARY`` encoding + - HBase ``Bytes.toBytes`` + - Java ``ByteBuffer.putLong()`` with ``ByteOrder.BIG_ENDIAN`` Attributes: bytes_type (google.cloud.bigtable_v2.types.Type.Bytes): @@ -283,12 +325,28 @@ class BigEndianBytes(proto.Message): message="Type.Bytes", ) + class OrderedCodeBytes(proto.Message): + r"""Encodes the value in a variable length binary format of up to + 10 bytes. Values that are closer to zero use fewer bytes. + + Sorted mode: all values are supported. + + Distinct mode: all values are supported. + + """ + big_endian_bytes: "Type.Int64.Encoding.BigEndianBytes" = proto.Field( proto.MESSAGE, number=1, oneof="encoding", message="Type.Int64.Encoding.BigEndianBytes", ) + ordered_code_bytes: "Type.Int64.Encoding.OrderedCodeBytes" = proto.Field( + proto.MESSAGE, + number=2, + oneof="encoding", + message="Type.Int64.Encoding.OrderedCodeBytes", + ) encoding: "Type.Int64.Encoding" = proto.Field( proto.MESSAGE, @@ -315,8 +373,43 @@ class Timestamp(proto.Message): r"""Timestamp Values of type ``Timestamp`` are stored in ``Value.timestamp_value``. + Attributes: + encoding (google.cloud.bigtable_v2.types.Type.Timestamp.Encoding): + The encoding to use when converting to or + from lower level types. """ + class Encoding(proto.Message): + r"""Rules used to convert to or from lower level types. + + .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields + + Attributes: + unix_micros_int64 (google.cloud.bigtable_v2.types.Type.Int64.Encoding): + Encodes the number of microseconds since the Unix epoch + using the given ``Int64`` encoding. Values must be + microsecond-aligned. + + Compatible with: + + - Java ``Instant.truncatedTo()`` with ``ChronoUnit.MICROS`` + + This field is a member of `oneof`_ ``encoding``. + """ + + unix_micros_int64: "Type.Int64.Encoding" = proto.Field( + proto.MESSAGE, + number=1, + oneof="encoding", + message="Type.Int64.Encoding", + ) + + encoding: "Type.Timestamp.Encoding" = proto.Field( + proto.MESSAGE, + number=1, + message="Type.Timestamp.Encoding", + ) + class Date(proto.Message): r"""Date Values of type ``Date`` are stored in ``Value.date_value``.""" @@ -330,6 +423,9 @@ class Struct(proto.Message): fields (MutableSequence[google.cloud.bigtable_v2.types.Type.Struct.Field]): The names and types of the fields in this struct. + encoding (google.cloud.bigtable_v2.types.Type.Struct.Encoding): + The encoding to use when converting to or + from lower level types. """ class Field(proto.Message): @@ -353,11 +449,146 @@ class Field(proto.Message): message="Type", ) + class Encoding(proto.Message): + r"""Rules used to convert to or from lower level types. + + This message has `oneof`_ fields (mutually exclusive fields). + For each oneof, at most one member field can be set at the same time. + Setting any member of the oneof automatically clears all other + members. + + .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields + + Attributes: + singleton (google.cloud.bigtable_v2.types.Type.Struct.Encoding.Singleton): + Use ``Singleton`` encoding. + + This field is a member of `oneof`_ ``encoding``. + delimited_bytes (google.cloud.bigtable_v2.types.Type.Struct.Encoding.DelimitedBytes): + Use ``DelimitedBytes`` encoding. + + This field is a member of `oneof`_ ``encoding``. + ordered_code_bytes (google.cloud.bigtable_v2.types.Type.Struct.Encoding.OrderedCodeBytes): + User ``OrderedCodeBytes`` encoding. + + This field is a member of `oneof`_ ``encoding``. + """ + + class Singleton(proto.Message): + r"""Uses the encoding of ``fields[0].type`` as-is. Only valid if + ``fields.size == 1``. + + """ + + class DelimitedBytes(proto.Message): + r"""Fields are encoded independently and concatenated with a + configurable ``delimiter`` in between. + + A struct with no fields defined is encoded as a single + ``delimiter``. + + Sorted mode: + + - Fields are encoded in sorted mode. + - Encoded field values must not contain any bytes <= + ``delimiter[0]`` + - Element-wise order is preserved: ``A < B`` if ``A[0] < B[0]``, or + if ``A[0] == B[0] && A[1] < B[1]``, etc. Strict prefixes sort + first. + + Distinct mode: + + - Fields are encoded in distinct mode. + - Encoded field values must not contain ``delimiter[0]``. + + Attributes: + delimiter (bytes): + Byte sequence used to delimit concatenated + fields. The delimiter must contain at least 1 + character and at most 50 characters. + """ + + delimiter: bytes = proto.Field( + proto.BYTES, + number=1, + ) + + class OrderedCodeBytes(proto.Message): + r"""Fields are encoded independently and concatenated with the fixed + byte pair ``{0x00, 0x01}`` in between. + + Any null ``(0x00)`` byte in an encoded field is replaced by the + fixed byte pair ``{0x00, 0xFF}``. + + Fields that encode to the empty string "" have special handling: + + - If *every* field encodes to "", or if the STRUCT has no fields + defined, then the STRUCT is encoded as the fixed byte pair + ``{0x00, 0x00}``. + - Otherwise, the STRUCT only encodes until the last non-empty field, + omitting any trailing empty fields. Any empty fields that aren't + omitted are replaced with the fixed byte pair ``{0x00, 0x00}``. + + Examples: + + :: + + - STRUCT() -> "\00\00" + - STRUCT("") -> "\00\00" + - STRUCT("", "") -> "\00\00" + - STRUCT("", "B") -> "\00\00" + "\00\01" + "B" + - STRUCT("A", "") -> "A" + - STRUCT("", "B", "") -> "\00\00" + "\00\01" + "B" + - STRUCT("A", "", "C") -> "A" + "\00\01" + "\00\00" + "\00\01" + "C" + + Since null bytes are always escaped, this encoding can cause size + blowup for encodings like ``Int64.BigEndianBytes`` that are likely + to produce many such bytes. + + Sorted mode: + + - Fields are encoded in sorted mode. + - All values supported by the field encodings are allowed + - Element-wise order is preserved: ``A < B`` if ``A[0] < B[0]``, or + if ``A[0] == B[0] && A[1] < B[1]``, etc. Strict prefixes sort + first. + + Distinct mode: + + - Fields are encoded in distinct mode. + - All values supported by the field encodings are allowed. + + """ + + singleton: "Type.Struct.Encoding.Singleton" = proto.Field( + proto.MESSAGE, + number=1, + oneof="encoding", + message="Type.Struct.Encoding.Singleton", + ) + delimited_bytes: "Type.Struct.Encoding.DelimitedBytes" = proto.Field( + proto.MESSAGE, + number=2, + oneof="encoding", + message="Type.Struct.Encoding.DelimitedBytes", + ) + ordered_code_bytes: "Type.Struct.Encoding.OrderedCodeBytes" = proto.Field( + proto.MESSAGE, + number=3, + oneof="encoding", + message="Type.Struct.Encoding.OrderedCodeBytes", + ) + fields: MutableSequence["Type.Struct.Field"] = proto.RepeatedField( proto.MESSAGE, number=1, message="Type.Struct.Field", ) + encoding: "Type.Struct.Encoding" = proto.Field( + proto.MESSAGE, + number=2, + message="Type.Struct.Encoding", + ) class Proto(proto.Message): r"""A protobuf message type. Values of type ``Proto`` are stored in @@ -453,8 +684,8 @@ class Aggregate(proto.Message): r"""A value that combines incremental updates into a summarized value. Data is never directly written or read using type ``Aggregate``. - Writes will provide either the ``input_type`` or ``state_type``, and - reads will always return the ``state_type`` . + Writes provide either the ``input_type`` or ``state_type``, and + reads always return the ``state_type`` . This message has `oneof`_ fields (mutually exclusive fields). For each oneof, at most one member field can be set at the same time. @@ -466,13 +697,12 @@ class Aggregate(proto.Message): Attributes: input_type (google.cloud.bigtable_v2.types.Type): Type of the inputs that are accumulated by this - ``Aggregate``, which must specify a full encoding. Use - ``AddInput`` mutations to accumulate new inputs. + ``Aggregate``. Use ``AddInput`` mutations to accumulate new + inputs. state_type (google.cloud.bigtable_v2.types.Type): Output only. Type that holds the internal accumulator state for the ``Aggregate``. This is a function of the - ``input_type`` and ``aggregator`` chosen, and will always - specify a full encoding. + ``input_type`` and ``aggregator`` chosen. sum (google.cloud.bigtable_v2.types.Type.Aggregate.Sum): Sum aggregator. diff --git a/owlbot.py b/owlbot.py index b6b741b54..6b2e1ea4f 100644 --- a/owlbot.py +++ b/owlbot.py @@ -111,6 +111,7 @@ def get_staging_dirs( ], system_test_python_versions=["3.9"], unit_test_python_versions=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"], + default_python_version="3.13", ) s.move(templated_files, excludes=[".coveragerc", "README.rst", ".github/release-please.yml", "noxfile.py", "renovate.json"]) diff --git a/samples/beam/requirements.txt b/samples/beam/requirements.txt index 55b3ae719..4b84ddec3 100644 --- a/samples/beam/requirements.txt +++ b/samples/beam/requirements.txt @@ -1,3 +1,4 @@ -apache-beam==2.65.0 +apache-beam===2.60.0; python_version == '3.8' +apache-beam==2.65.0; python_version >= '3.9' google-cloud-bigtable==2.30.1 google-cloud-core==2.4.3 diff --git a/samples/generated_samples/snippet_metadata_google.bigtable.admin.v2.json b/samples/generated_samples/snippet_metadata_google.bigtable.admin.v2.json index 3d73099e8..66b5c8f67 100644 --- a/samples/generated_samples/snippet_metadata_google.bigtable.admin.v2.json +++ b/samples/generated_samples/snippet_metadata_google.bigtable.admin.v2.json @@ -8,7 +8,7 @@ ], "language": "PYTHON", "name": "google-cloud-bigtable-admin", - "version": "0.1.0" + "version": "0.0.0" }, "snippets": [ { diff --git a/tests/unit/gapic/bigtable_v2/test_bigtable.py b/tests/unit/gapic/bigtable_v2/test_bigtable.py index cb78d2b7a..24db8e269 100644 --- a/tests/unit/gapic/bigtable_v2/test_bigtable.py +++ b/tests/unit/gapic/bigtable_v2/test_bigtable.py @@ -6852,6 +6852,7 @@ def test_read_rows_routing_parameters_request_1_grpc(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -6916,6 +6917,43 @@ def test_read_rows_routing_parameters_request_3_grpc(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", + } + + # assert the expected headers are present, in any order + routing_string = next( + iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) + ) + assert all([f"{k}={v}" in routing_string for k, v in expected_headers.items()]) + + +def test_read_rows_routing_parameters_request_4_grpc(): + client = BigtableClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="grpc", + ) + + # Mock the actual call, and fake the request. + with mock.patch.object(type(client.transport.read_rows), "__call__") as call: + call.return_value = iter([bigtable.ReadRowsResponse()]) + client.read_rows( + request={ + "materialized_view_name": "projects/sample1/instances/sample2/sample3" + } + ) + + # Establish that the underlying stub method was called. + call.assert_called() + _, args, kw = call.mock_calls[0] + request_msg = bigtable.ReadRowsRequest( + **{"materialized_view_name": "projects/sample1/instances/sample2/sample3"} + ) + + assert args[0] == request_msg + + expected_headers = { + "name": "projects/sample1/instances/sample2", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -6949,6 +6987,7 @@ def test_sample_row_keys_routing_parameters_request_1_grpc(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -7013,6 +7052,43 @@ def test_sample_row_keys_routing_parameters_request_3_grpc(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", + } + + # assert the expected headers are present, in any order + routing_string = next( + iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) + ) + assert all([f"{k}={v}" in routing_string for k, v in expected_headers.items()]) + + +def test_sample_row_keys_routing_parameters_request_4_grpc(): + client = BigtableClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="grpc", + ) + + # Mock the actual call, and fake the request. + with mock.patch.object(type(client.transport.sample_row_keys), "__call__") as call: + call.return_value = iter([bigtable.SampleRowKeysResponse()]) + client.sample_row_keys( + request={ + "materialized_view_name": "projects/sample1/instances/sample2/sample3" + } + ) + + # Establish that the underlying stub method was called. + call.assert_called() + _, args, kw = call.mock_calls[0] + request_msg = bigtable.SampleRowKeysRequest( + **{"materialized_view_name": "projects/sample1/instances/sample2/sample3"} + ) + + assert args[0] == request_msg + + expected_headers = { + "name": "projects/sample1/instances/sample2", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -7046,6 +7122,7 @@ def test_mutate_row_routing_parameters_request_1_grpc(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -7110,6 +7187,7 @@ def test_mutate_row_routing_parameters_request_3_grpc(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -7143,6 +7221,7 @@ def test_mutate_rows_routing_parameters_request_1_grpc(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -7207,6 +7286,7 @@ def test_mutate_rows_routing_parameters_request_3_grpc(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -7242,6 +7322,7 @@ def test_check_and_mutate_row_routing_parameters_request_1_grpc(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -7310,6 +7391,7 @@ def test_check_and_mutate_row_routing_parameters_request_3_grpc(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -7341,6 +7423,7 @@ def test_ping_and_warm_routing_parameters_request_1_grpc(): expected_headers = { "name": "projects/sample1/instances/sample2", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -7403,6 +7486,7 @@ def test_read_modify_write_row_routing_parameters_request_1_grpc(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -7473,6 +7557,7 @@ def test_read_modify_write_row_routing_parameters_request_3_grpc(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -7506,6 +7591,7 @@ def test_prepare_query_routing_parameters_request_1_grpc(): expected_headers = { "name": "projects/sample1/instances/sample2", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -7566,6 +7652,7 @@ def test_execute_query_routing_parameters_request_1_grpc(): expected_headers = { "name": "projects/sample1/instances/sample2", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -7938,6 +8025,7 @@ async def test_read_rows_routing_parameters_request_1_grpc_asyncio(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -8012,6 +8100,48 @@ async def test_read_rows_routing_parameters_request_3_grpc_asyncio(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", + } + + # assert the expected headers are present, in any order + routing_string = next( + iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) + ) + assert all([f"{k}={v}" in routing_string for k, v in expected_headers.items()]) + + +@pytest.mark.asyncio +async def test_read_rows_routing_parameters_request_4_grpc_asyncio(): + client = BigtableAsyncClient( + credentials=async_anonymous_credentials(), + transport="grpc_asyncio", + ) + + # Mock the actual call, and fake the request. + with mock.patch.object(type(client.transport.read_rows), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = mock.Mock(aio.UnaryStreamCall, autospec=True) + call.return_value.read = mock.AsyncMock( + side_effect=[bigtable.ReadRowsResponse()] + ) + await client.read_rows( + request={ + "materialized_view_name": "projects/sample1/instances/sample2/sample3" + } + ) + + # Establish that the underlying stub method was called. + call.assert_called() + _, args, kw = call.mock_calls[0] + request_msg = bigtable.ReadRowsRequest( + **{"materialized_view_name": "projects/sample1/instances/sample2/sample3"} + ) + + assert args[0] == request_msg + + expected_headers = { + "name": "projects/sample1/instances/sample2", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -8050,6 +8180,7 @@ async def test_sample_row_keys_routing_parameters_request_1_grpc_asyncio(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -8124,6 +8255,48 @@ async def test_sample_row_keys_routing_parameters_request_3_grpc_asyncio(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", + } + + # assert the expected headers are present, in any order + routing_string = next( + iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) + ) + assert all([f"{k}={v}" in routing_string for k, v in expected_headers.items()]) + + +@pytest.mark.asyncio +async def test_sample_row_keys_routing_parameters_request_4_grpc_asyncio(): + client = BigtableAsyncClient( + credentials=async_anonymous_credentials(), + transport="grpc_asyncio", + ) + + # Mock the actual call, and fake the request. + with mock.patch.object(type(client.transport.sample_row_keys), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = mock.Mock(aio.UnaryStreamCall, autospec=True) + call.return_value.read = mock.AsyncMock( + side_effect=[bigtable.SampleRowKeysResponse()] + ) + await client.sample_row_keys( + request={ + "materialized_view_name": "projects/sample1/instances/sample2/sample3" + } + ) + + # Establish that the underlying stub method was called. + call.assert_called() + _, args, kw = call.mock_calls[0] + request_msg = bigtable.SampleRowKeysRequest( + **{"materialized_view_name": "projects/sample1/instances/sample2/sample3"} + ) + + assert args[0] == request_msg + + expected_headers = { + "name": "projects/sample1/instances/sample2", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -8161,6 +8334,7 @@ async def test_mutate_row_routing_parameters_request_1_grpc_asyncio(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -8233,6 +8407,7 @@ async def test_mutate_row_routing_parameters_request_3_grpc_asyncio(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -8271,6 +8446,7 @@ async def test_mutate_rows_routing_parameters_request_1_grpc_asyncio(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -8345,6 +8521,7 @@ async def test_mutate_rows_routing_parameters_request_3_grpc_asyncio(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -8386,6 +8563,7 @@ async def test_check_and_mutate_row_routing_parameters_request_1_grpc_asyncio(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -8466,6 +8644,7 @@ async def test_check_and_mutate_row_routing_parameters_request_3_grpc_asyncio(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -8503,6 +8682,7 @@ async def test_ping_and_warm_routing_parameters_request_1_grpc_asyncio(): expected_headers = { "name": "projects/sample1/instances/sample2", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -8573,6 +8753,7 @@ async def test_read_modify_write_row_routing_parameters_request_1_grpc_asyncio() expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -8651,6 +8832,7 @@ async def test_read_modify_write_row_routing_parameters_request_3_grpc_asyncio() expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -8690,6 +8872,7 @@ async def test_prepare_query_routing_parameters_request_1_grpc_asyncio(): expected_headers = { "name": "projects/sample1/instances/sample2", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -8761,6 +8944,7 @@ async def test_execute_query_routing_parameters_request_1_grpc_asyncio(): expected_headers = { "name": "projects/sample1/instances/sample2", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -10448,6 +10632,7 @@ def test_read_rows_routing_parameters_request_1_rest(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -10510,6 +10695,42 @@ def test_read_rows_routing_parameters_request_3_rest(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", + } + + # assert the expected headers are present, in any order + routing_string = next( + iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) + ) + assert all([f"{k}={v}" in routing_string for k, v in expected_headers.items()]) + + +def test_read_rows_routing_parameters_request_4_rest(): + client = BigtableClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # Mock the actual call, and fake the request. + with mock.patch.object(type(client.transport.read_rows), "__call__") as call: + client.read_rows( + request={ + "materialized_view_name": "projects/sample1/instances/sample2/sample3" + } + ) + + # Establish that the underlying stub method was called. + call.assert_called() + _, args, kw = call.mock_calls[0] + request_msg = bigtable.ReadRowsRequest( + **{"materialized_view_name": "projects/sample1/instances/sample2/sample3"} + ) + + assert args[0] == request_msg + + expected_headers = { + "name": "projects/sample1/instances/sample2", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -10542,6 +10763,7 @@ def test_sample_row_keys_routing_parameters_request_1_rest(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -10604,6 +10826,42 @@ def test_sample_row_keys_routing_parameters_request_3_rest(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", + } + + # assert the expected headers are present, in any order + routing_string = next( + iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) + ) + assert all([f"{k}={v}" in routing_string for k, v in expected_headers.items()]) + + +def test_sample_row_keys_routing_parameters_request_4_rest(): + client = BigtableClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # Mock the actual call, and fake the request. + with mock.patch.object(type(client.transport.sample_row_keys), "__call__") as call: + client.sample_row_keys( + request={ + "materialized_view_name": "projects/sample1/instances/sample2/sample3" + } + ) + + # Establish that the underlying stub method was called. + call.assert_called() + _, args, kw = call.mock_calls[0] + request_msg = bigtable.SampleRowKeysRequest( + **{"materialized_view_name": "projects/sample1/instances/sample2/sample3"} + ) + + assert args[0] == request_msg + + expected_headers = { + "name": "projects/sample1/instances/sample2", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -10636,6 +10894,7 @@ def test_mutate_row_routing_parameters_request_1_rest(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -10698,6 +10957,7 @@ def test_mutate_row_routing_parameters_request_3_rest(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -10730,6 +10990,7 @@ def test_mutate_rows_routing_parameters_request_1_rest(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -10792,6 +11053,7 @@ def test_mutate_rows_routing_parameters_request_3_rest(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -10826,6 +11088,7 @@ def test_check_and_mutate_row_routing_parameters_request_1_rest(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -10892,6 +11155,7 @@ def test_check_and_mutate_row_routing_parameters_request_3_rest(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -10922,6 +11186,7 @@ def test_ping_and_warm_routing_parameters_request_1_rest(): expected_headers = { "name": "projects/sample1/instances/sample2", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -10982,6 +11247,7 @@ def test_read_modify_write_row_routing_parameters_request_1_rest(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -11050,6 +11316,7 @@ def test_read_modify_write_row_routing_parameters_request_3_rest(): expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -11082,6 +11349,7 @@ def test_prepare_query_routing_parameters_request_1_rest(): expected_headers = { "name": "projects/sample1/instances/sample2", + "app_profile_id": "", } # assert the expected headers are present, in any order @@ -11140,6 +11408,7 @@ def test_execute_query_routing_parameters_request_1_rest(): expected_headers = { "name": "projects/sample1/instances/sample2", + "app_profile_id": "", } # assert the expected headers are present, in any order From 6561cfac605ba7c5b3f750c3bdca9108e517ba77 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 20 Nov 2025 01:59:16 -0800 Subject: [PATCH 02/10] feat: add basic interceptor to client (#1206) --- google/cloud/bigtable/data/_async/client.py | 50 ++++-- .../data/_async/metrics_interceptor.py | 78 ++++++++ .../bigtable/data/_sync_autogen/client.py | 30 +++- .../data/_sync_autogen/metrics_interceptor.py | 59 ++++++ tests/system/data/test_system_async.py | 19 +- tests/system/data/test_system_autogen.py | 6 +- .../data/_async/test_metrics_interceptor.py | 168 ++++++++++++++++++ .../_sync_autogen/test_metrics_interceptor.py | 140 +++++++++++++++ tests/unit/data/test_sync_up_to_date.py | 2 +- 9 files changed, 522 insertions(+), 30 deletions(-) create mode 100644 google/cloud/bigtable/data/_async/metrics_interceptor.py create mode 100644 google/cloud/bigtable/data/_sync_autogen/metrics_interceptor.py create mode 100644 tests/unit/data/_async/test_metrics_interceptor.py create mode 100644 tests/unit/data/_sync_autogen/test_metrics_interceptor.py diff --git a/google/cloud/bigtable/data/_async/client.py b/google/cloud/bigtable/data/_async/client.py index 0af7154a6..1c98f56ab 100644 --- a/google/cloud/bigtable/data/_async/client.py +++ b/google/cloud/bigtable/data/_async/client.py @@ -19,6 +19,7 @@ cast, Any, AsyncIterable, + Callable, Optional, Set, Sequence, @@ -99,18 +100,24 @@ ) from google.cloud.bigtable.data._async.mutations_batcher import _MB_SIZE from google.cloud.bigtable.data._async._swappable_channel import ( - AsyncSwappableChannel, + AsyncSwappableChannel as SwappableChannelType, + ) + from google.cloud.bigtable.data._async.metrics_interceptor import ( + AsyncBigtableMetricsInterceptor as MetricsInterceptorType, ) else: from typing import Iterable # noqa: F401 from grpc import insecure_channel + from grpc import intercept_channel from google.cloud.bigtable_v2.services.bigtable.transports import BigtableGrpcTransport as TransportType # type: ignore from google.cloud.bigtable_v2.services.bigtable import BigtableClient as GapicClient # type: ignore from google.cloud.bigtable.data._sync_autogen.mutations_batcher import _MB_SIZE from google.cloud.bigtable.data._sync_autogen._swappable_channel import ( # noqa: F401 - SwappableChannel, + SwappableChannel as SwappableChannelType, + ) + from google.cloud.bigtable.data._sync_autogen.metrics_interceptor import ( # noqa: F401 + BigtableMetricsInterceptor as MetricsInterceptorType, ) - if TYPE_CHECKING: from google.cloud.bigtable.data._helpers import RowKeySamples @@ -205,7 +212,7 @@ def __init__( credentials = google.auth.credentials.AnonymousCredentials() if project is None: project = _DEFAULT_BIGTABLE_EMULATOR_CLIENT - + self._metrics_interceptor = MetricsInterceptorType() # initialize client ClientWithProject.__init__( self, @@ -259,12 +266,11 @@ def __init__( stacklevel=2, ) - @CrossSync.convert(replace_symbols={"AsyncSwappableChannel": "SwappableChannel"}) - def _build_grpc_channel(self, *args, **kwargs) -> AsyncSwappableChannel: + def _build_grpc_channel(self, *args, **kwargs) -> SwappableChannelType: """ This method is called by the gapic transport to create a grpc channel. - The init arguments passed down are captured in a partial used by AsyncSwappableChannel + The init arguments passed down are captured in a partial used by SwappableChannel to create new channel instances in the future, as part of the channel refresh logic Emulators always use an inseucre channel @@ -275,12 +281,30 @@ def _build_grpc_channel(self, *args, **kwargs) -> AsyncSwappableChannel: Returns: a custom wrapped swappable channel """ + create_channel_fn: Callable[[], Channel] if self._emulator_host is not None: - # emulators use insecure channel + # Emulators use insecure channels create_channel_fn = partial(insecure_channel, self._emulator_host) - else: + elif CrossSync.is_async: + # For async client, use the default create_channel. create_channel_fn = partial(TransportType.create_channel, *args, **kwargs) - return AsyncSwappableChannel(create_channel_fn) + else: + # For sync client, wrap create_channel with interceptors. + def sync_create_channel_fn(): + return intercept_channel( + TransportType.create_channel(*args, **kwargs), + self._metrics_interceptor, + ) + + create_channel_fn = sync_create_channel_fn + + # Instantiate SwappableChannelType with the determined creation function. + new_channel = SwappableChannelType(create_channel_fn) + if CrossSync.is_async: + # Attach async interceptors to the channel instance itself. + new_channel._unary_unary_interceptors.append(self._metrics_interceptor) + new_channel._unary_stream_interceptors.append(self._metrics_interceptor) + return new_channel @property def universe_domain(self) -> str: @@ -402,7 +426,7 @@ def _invalidate_channel_stubs(self): self.transport._stubs = {} self.transport._prep_wrapped_messages(self.client_info) - @CrossSync.convert(replace_symbols={"AsyncSwappableChannel": "SwappableChannel"}) + @CrossSync.convert async def _manage_channel( self, refresh_interval_min: float = 60 * 35, @@ -427,10 +451,10 @@ async def _manage_channel( grace_period: time to allow previous channel to serve existing requests before closing, in seconds """ - if not isinstance(self.transport.grpc_channel, AsyncSwappableChannel): + if not isinstance(self.transport.grpc_channel, SwappableChannelType): warnings.warn("Channel does not support auto-refresh.") return - super_channel: AsyncSwappableChannel = self.transport.grpc_channel + super_channel: SwappableChannelType = self.transport.grpc_channel first_refresh = self._channel_init_time + random.uniform( refresh_interval_min, refresh_interval_max ) diff --git a/google/cloud/bigtable/data/_async/metrics_interceptor.py b/google/cloud/bigtable/data/_async/metrics_interceptor.py new file mode 100644 index 000000000..a154c0083 --- /dev/null +++ b/google/cloud/bigtable/data/_async/metrics_interceptor.py @@ -0,0 +1,78 @@ +# 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. +from __future__ import annotations + +from google.cloud.bigtable.data._cross_sync import CrossSync + +if CrossSync.is_async: + from grpc.aio import UnaryUnaryClientInterceptor + from grpc.aio import UnaryStreamClientInterceptor +else: + from grpc import UnaryUnaryClientInterceptor + from grpc import UnaryStreamClientInterceptor + + +__CROSS_SYNC_OUTPUT__ = "google.cloud.bigtable.data._sync_autogen.metrics_interceptor" + + +@CrossSync.convert_class(sync_name="BigtableMetricsInterceptor") +class AsyncBigtableMetricsInterceptor( + UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor +): + """ + An async gRPC interceptor to add client metadata and print server metadata. + """ + + @CrossSync.convert + async def intercept_unary_unary(self, continuation, client_call_details, request): + """ + Interceptor for unary rpcs: + - MutateRow + - CheckAndMutateRow + - ReadModifyWriteRow + """ + try: + call = await continuation(client_call_details, request) + return call + except Exception as rpc_error: + raise rpc_error + + @CrossSync.convert + async def intercept_unary_stream(self, continuation, client_call_details, request): + """ + Interceptor for streaming rpcs: + - ReadRows + - MutateRows + - SampleRowKeys + """ + try: + return self._streaming_generator_wrapper( + await continuation(client_call_details, request) + ) + except Exception as rpc_error: + # handle errors while intializing stream + raise rpc_error + + @staticmethod + @CrossSync.convert + async def _streaming_generator_wrapper(call): + """ + Wrapped generator to be returned by intercept_unary_stream. + """ + try: + async for response in call: + yield response + except Exception as e: + # handle errors while processing stream + raise e diff --git a/google/cloud/bigtable/data/_sync_autogen/client.py b/google/cloud/bigtable/data/_sync_autogen/client.py index adc849649..a403643f5 100644 --- a/google/cloud/bigtable/data/_sync_autogen/client.py +++ b/google/cloud/bigtable/data/_sync_autogen/client.py @@ -17,7 +17,7 @@ # This file is automatically generated by CrossSync. Do not edit manually. from __future__ import annotations -from typing import cast, Any, Optional, Set, Sequence, TYPE_CHECKING +from typing import cast, Any, Callable, Optional, Set, Sequence, TYPE_CHECKING import abc import time import warnings @@ -77,12 +77,18 @@ from google.cloud.bigtable.data._cross_sync import CrossSync from typing import Iterable from grpc import insecure_channel +from grpc import intercept_channel from google.cloud.bigtable_v2.services.bigtable.transports import ( BigtableGrpcTransport as TransportType, ) from google.cloud.bigtable_v2.services.bigtable import BigtableClient as GapicClient from google.cloud.bigtable.data._sync_autogen.mutations_batcher import _MB_SIZE -from google.cloud.bigtable.data._sync_autogen._swappable_channel import SwappableChannel +from google.cloud.bigtable.data._sync_autogen._swappable_channel import ( + SwappableChannel as SwappableChannelType, +) +from google.cloud.bigtable.data._sync_autogen.metrics_interceptor import ( + BigtableMetricsInterceptor as MetricsInterceptorType, +) if TYPE_CHECKING: from google.cloud.bigtable.data._helpers import RowKeySamples @@ -145,6 +151,7 @@ def __init__( credentials = google.auth.credentials.AnonymousCredentials() if project is None: project = _DEFAULT_BIGTABLE_EMULATOR_CLIENT + self._metrics_interceptor = MetricsInterceptorType() ClientWithProject.__init__( self, credentials=credentials, @@ -188,7 +195,7 @@ def __init__( stacklevel=2, ) - def _build_grpc_channel(self, *args, **kwargs) -> SwappableChannel: + def _build_grpc_channel(self, *args, **kwargs) -> SwappableChannelType: """This method is called by the gapic transport to create a grpc channel. The init arguments passed down are captured in a partial used by SwappableChannel @@ -201,11 +208,20 @@ def _build_grpc_channel(self, *args, **kwargs) -> SwappableChannel: - **kwargs: keyword arguments passed by the gapic layer to create a new channel with Returns: a custom wrapped swappable channel""" + create_channel_fn: Callable[[], Channel] if self._emulator_host is not None: create_channel_fn = partial(insecure_channel, self._emulator_host) else: - create_channel_fn = partial(TransportType.create_channel, *args, **kwargs) - return SwappableChannel(create_channel_fn) + + def sync_create_channel_fn(): + return intercept_channel( + TransportType.create_channel(*args, **kwargs), + self._metrics_interceptor, + ) + + create_channel_fn = sync_create_channel_fn + new_channel = SwappableChannelType(create_channel_fn) + return new_channel @property def universe_domain(self) -> str: @@ -326,10 +342,10 @@ def _manage_channel( between `refresh_interval_min` and `refresh_interval_max` grace_period: time to allow previous channel to serve existing requests before closing, in seconds""" - if not isinstance(self.transport.grpc_channel, SwappableChannel): + if not isinstance(self.transport.grpc_channel, SwappableChannelType): warnings.warn("Channel does not support auto-refresh.") return - super_channel: SwappableChannel = self.transport.grpc_channel + super_channel: SwappableChannelType = self.transport.grpc_channel first_refresh = self._channel_init_time + random.uniform( refresh_interval_min, refresh_interval_max ) diff --git a/google/cloud/bigtable/data/_sync_autogen/metrics_interceptor.py b/google/cloud/bigtable/data/_sync_autogen/metrics_interceptor.py new file mode 100644 index 000000000..9e47313b0 --- /dev/null +++ b/google/cloud/bigtable/data/_sync_autogen/metrics_interceptor.py @@ -0,0 +1,59 @@ +# 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. + +# This file is automatically generated by CrossSync. Do not edit manually. + +from __future__ import annotations +from grpc import UnaryUnaryClientInterceptor +from grpc import UnaryStreamClientInterceptor + + +class BigtableMetricsInterceptor( + UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor +): + """ + An async gRPC interceptor to add client metadata and print server metadata. + """ + + def intercept_unary_unary(self, continuation, client_call_details, request): + """Interceptor for unary rpcs: + - MutateRow + - CheckAndMutateRow + - ReadModifyWriteRow""" + try: + call = continuation(client_call_details, request) + return call + except Exception as rpc_error: + raise rpc_error + + def intercept_unary_stream(self, continuation, client_call_details, request): + """Interceptor for streaming rpcs: + - ReadRows + - MutateRows + - SampleRowKeys""" + try: + return self._streaming_generator_wrapper( + continuation(client_call_details, request) + ) + except Exception as rpc_error: + raise rpc_error + + @staticmethod + def _streaming_generator_wrapper(call): + """Wrapped generator to be returned by intercept_unary_stream.""" + try: + for response in call: + yield response + except Exception as e: + raise e diff --git a/tests/system/data/test_system_async.py b/tests/system/data/test_system_async.py index c96570b76..39c454996 100644 --- a/tests/system/data/test_system_async.py +++ b/tests/system/data/test_system_async.py @@ -285,23 +285,28 @@ async def test_channel_refresh(self, table_id, instance_id, temp_rows): async with client.get_table(instance_id, table_id) as table: rows = await table.read_rows({}) channel_wrapper = client.transport.grpc_channel - first_channel = client.transport.grpc_channel._channel + first_channel = channel_wrapper._channel assert len(rows) == 2 await CrossSync.sleep(2) rows_after_refresh = await table.read_rows({}) assert len(rows_after_refresh) == 2 assert client.transport.grpc_channel is channel_wrapper - assert client.transport.grpc_channel._channel is not first_channel - # ensure gapic's logging interceptor is still active + updated_channel = channel_wrapper._channel + assert updated_channel is not first_channel + # ensure interceptors are kept (gapic's logging interceptor, and metric interceptor) if CrossSync.is_async: - interceptors = ( - client.transport.grpc_channel._channel._unary_unary_interceptors - ) - assert GapicInterceptor in [type(i) for i in interceptors] + unary_interceptors = updated_channel._unary_unary_interceptors + assert len(unary_interceptors) == 2 + assert GapicInterceptor in [type(i) for i in unary_interceptors] + assert client._metrics_interceptor in unary_interceptors + stream_interceptors = updated_channel._unary_stream_interceptors + assert len(stream_interceptors) == 1 + assert client._metrics_interceptor in stream_interceptors else: assert isinstance( client.transport._logged_channel._interceptor, GapicInterceptor ) + assert updated_channel._interceptor == client._metrics_interceptor finally: await client.close() diff --git a/tests/system/data/test_system_autogen.py b/tests/system/data/test_system_autogen.py index 44895808a..37c00f2ae 100644 --- a/tests/system/data/test_system_autogen.py +++ b/tests/system/data/test_system_autogen.py @@ -237,16 +237,18 @@ def test_channel_refresh(self, table_id, instance_id, temp_rows): with client.get_table(instance_id, table_id) as table: rows = table.read_rows({}) channel_wrapper = client.transport.grpc_channel - first_channel = client.transport.grpc_channel._channel + first_channel = channel_wrapper._channel assert len(rows) == 2 CrossSync._Sync_Impl.sleep(2) rows_after_refresh = table.read_rows({}) assert len(rows_after_refresh) == 2 assert client.transport.grpc_channel is channel_wrapper - assert client.transport.grpc_channel._channel is not first_channel + updated_channel = channel_wrapper._channel + assert updated_channel is not first_channel assert isinstance( client.transport._logged_channel._interceptor, GapicInterceptor ) + assert updated_channel._interceptor == client._metrics_interceptor finally: client.close() diff --git a/tests/unit/data/_async/test_metrics_interceptor.py b/tests/unit/data/_async/test_metrics_interceptor.py new file mode 100644 index 000000000..6ea958358 --- /dev/null +++ b/tests/unit/data/_async/test_metrics_interceptor.py @@ -0,0 +1,168 @@ +# 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 +from grpc import RpcError + +from google.cloud.bigtable.data._cross_sync import CrossSync + +# try/except added for compatibility with python < 3.8 +try: + from unittest import mock +except ImportError: # pragma: NO COVER + import mock # type: ignore + +if CrossSync.is_async: + from google.cloud.bigtable.data._async.metrics_interceptor import ( + AsyncBigtableMetricsInterceptor, + ) +else: + from google.cloud.bigtable.data._sync_autogen.metrics_interceptor import ( # noqa: F401 + BigtableMetricsInterceptor, + ) + + +__CROSS_SYNC_OUTPUT__ = "tests.unit.data._sync_autogen.test_metrics_interceptor" + + +@CrossSync.convert(replace_symbols={"__aiter__": "__iter__"}) +def _make_mock_stream_call(values, exc=None): + """ + Create a mock call object that can be used for streaming calls + """ + call = CrossSync.Mock() + + async def gen(): + for val in values: + yield val + if exc: + raise exc + + call.__aiter__ = mock.Mock(return_value=gen()) + return call + + +@CrossSync.convert_class(sync_name="TestMetricsInterceptor") +class TestMetricsInterceptorAsync: + @staticmethod + @CrossSync.convert( + replace_symbols={ + "AsyncBigtableMetricsInterceptor": "BigtableMetricsInterceptor" + } + ) + def _get_target_class(): + return AsyncBigtableMetricsInterceptor + + def _make_one(self, *args, **kwargs): + return self._get_target_class()(*args, **kwargs) + + @CrossSync.pytest + async def test_unary_unary_interceptor_success(self): + """Test that interceptor handles successful unary-unary calls""" + instance = self._make_one() + continuation = CrossSync.Mock() + call = continuation.return_value + 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) + + @CrossSync.pytest + async def test_unary_unary_interceptor_failure(self): + """Test a failed RpcError with metadata""" + + instance = self._make_one() + exc = RpcError("test") + continuation = CrossSync.Mock(side_effect=exc) + 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) + + @CrossSync.pytest + async def test_unary_unary_interceptor_failure_generic(self): + """Test generic exception""" + + instance = self._make_one() + exc = ValueError("test") + continuation = CrossSync.Mock(side_effect=exc) + 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) + + @CrossSync.pytest + async def test_unary_stream_interceptor_success(self): + """Test that interceptor handles successful unary-stream calls""" + + instance = self._make_one() + + continuation = CrossSync.Mock(return_value=_make_mock_stream_call([1, 2])) + 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) + + @CrossSync.pytest + async def test_unary_stream_interceptor_failure_mid_stream(self): + """Test that interceptor handles failures mid-stream""" + instance = self._make_one() + exc = ValueError("test") + continuation = CrossSync.Mock(return_value=_make_mock_stream_call([1], exc=exc)) + details = mock.Mock() + request = mock.Mock() + wrapper = await instance.intercept_unary_stream(continuation, details, request) + with pytest.raises(ValueError) as e: + [val async for val in wrapper] + assert e.value == exc + continuation.assert_called_once_with(details, request) + + @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() + exc = RpcError("test") + + continuation = CrossSync.Mock() + continuation.side_effect = exc + 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) + + @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() + exc = ValueError("test") + + continuation = CrossSync.Mock() + continuation.side_effect = exc + 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) diff --git a/tests/unit/data/_sync_autogen/test_metrics_interceptor.py b/tests/unit/data/_sync_autogen/test_metrics_interceptor.py new file mode 100644 index 000000000..56a6f3650 --- /dev/null +++ b/tests/unit/data/_sync_autogen/test_metrics_interceptor.py @@ -0,0 +1,140 @@ +# 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. + + +# This file is automatically generated by CrossSync. Do not edit manually. + +import pytest +from grpc import RpcError +from google.cloud.bigtable.data._cross_sync import CrossSync + +try: + from unittest import mock +except ImportError: + import mock +from google.cloud.bigtable.data._sync_autogen.metrics_interceptor import ( + BigtableMetricsInterceptor, +) + + +def _make_mock_stream_call(values, exc=None): + """Create a mock call object that can be used for streaming calls""" + call = CrossSync._Sync_Impl.Mock() + + def gen(): + for val in values: + yield val + if exc: + raise exc + + call.__iter__ = mock.Mock(return_value=gen()) + return call + + +class TestMetricsInterceptor: + @staticmethod + def _get_target_class(): + return BigtableMetricsInterceptor + + def _make_one(self, *args, **kwargs): + return self._get_target_class()(*args, **kwargs) + + def test_unary_unary_interceptor_success(self): + """Test that interceptor handles successful unary-unary calls""" + instance = self._make_one() + continuation = CrossSync._Sync_Impl.Mock() + call = continuation.return_value + details = mock.Mock() + request = mock.Mock() + result = instance.intercept_unary_unary(continuation, details, request) + assert result == call + continuation.assert_called_once_with(details, request) + + def test_unary_unary_interceptor_failure(self): + """Test a failed RpcError with metadata""" + instance = self._make_one() + exc = RpcError("test") + continuation = CrossSync._Sync_Impl.Mock(side_effect=exc) + 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) + + def test_unary_unary_interceptor_failure_generic(self): + """Test generic exception""" + instance = self._make_one() + exc = ValueError("test") + continuation = CrossSync._Sync_Impl.Mock(side_effect=exc) + 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) + + def test_unary_stream_interceptor_success(self): + """Test that interceptor handles successful unary-stream calls""" + instance = self._make_one() + continuation = CrossSync._Sync_Impl.Mock( + return_value=_make_mock_stream_call([1, 2]) + ) + 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) + + def test_unary_stream_interceptor_failure_mid_stream(self): + """Test that interceptor handles failures mid-stream""" + instance = self._make_one() + exc = ValueError("test") + continuation = CrossSync._Sync_Impl.Mock( + return_value=_make_mock_stream_call([1], exc=exc) + ) + details = mock.Mock() + request = mock.Mock() + wrapper = instance.intercept_unary_stream(continuation, details, request) + with pytest.raises(ValueError) as e: + [val for val in wrapper] + assert e.value == exc + continuation.assert_called_once_with(details, request) + + 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() + exc = RpcError("test") + continuation = CrossSync._Sync_Impl.Mock() + continuation.side_effect = exc + 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) + + 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() + exc = ValueError("test") + continuation = CrossSync._Sync_Impl.Mock() + continuation.side_effect = exc + 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) diff --git a/tests/unit/data/test_sync_up_to_date.py b/tests/unit/data/test_sync_up_to_date.py index d4623a6c8..e6bce9cf6 100644 --- a/tests/unit/data/test_sync_up_to_date.py +++ b/tests/unit/data/test_sync_up_to_date.py @@ -90,7 +90,7 @@ def test_verify_headers(sync_file): \#\ distributed\ under\ the\ License\ is\ distributed\ on\ an\ \"AS\ IS\"\ BASIS,\n \#\ WITHOUT\ WARRANTIES\ OR\ CONDITIONS\ OF\ ANY\ KIND,\ either\ express\ or\ implied\.\n \#\ See\ the\ License\ for\ the\ specific\ language\ governing\ permissions\ and\n - \#\ limitations\ under\ the\ License\. + \#\ limitations\ under\ the\ License """ pattern = re.compile(license_regex, re.VERBOSE) From 663c01f43fbfed90e80d7095b7a2c3c94cae0fc4 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 20 Nov 2025 11:24:36 -0500 Subject: [PATCH 03/10] chore(librarian): onboard to librarian (#1232) Towards https://github.com/googleapis/librarian/issues/2455 --- .github/.OwlBot.lock.yaml | 17 - .github/.OwlBot.yaml | 28 - .github/auto-approve.yml | 3 - .github/release-please.yml | 12 - .github/release-trigger.yml | 2 - .github/sync-repo-settings.yaml | 58 -- .../generator-input/.repo-metadata.json | 80 +++ .../generator-input/librarian.py | 101 +--- .librarian/generator-input/noxfile.py | 569 ++++++++++++++++++ .librarian/generator-input/setup.py | 104 ++++ .librarian/state.yaml | 40 ++ .release-please-manifest.json | 3 - google/cloud/bigtable_admin/gapic_version.py | 2 +- .../cloud/bigtable_admin_v2/gapic_version.py | 2 +- google/cloud/bigtable_v2/gapic_version.py | 2 +- noxfile.py | 2 +- release-please-config.json | 22 - ...pet_metadata_google.bigtable.admin.v2.json | 2 +- tests/unit/gapic/bigtable_v2/test_bigtable.py | 78 --- 19 files changed, 820 insertions(+), 307 deletions(-) delete mode 100644 .github/.OwlBot.lock.yaml delete mode 100644 .github/.OwlBot.yaml delete mode 100644 .github/auto-approve.yml delete mode 100644 .github/release-please.yml delete mode 100644 .github/release-trigger.yml delete mode 100644 .github/sync-repo-settings.yaml create mode 100644 .librarian/generator-input/.repo-metadata.json rename owlbot.py => .librarian/generator-input/librarian.py (73%) create mode 100644 .librarian/generator-input/noxfile.py create mode 100644 .librarian/generator-input/setup.py create mode 100644 .librarian/state.yaml delete mode 100644 .release-please-manifest.json delete mode 100644 release-please-config.json diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml deleted file mode 100644 index 9a7846675..000000000 --- a/.github/.OwlBot.lock.yaml +++ /dev/null @@ -1,17 +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. -docker: - image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:4a9e5d44b98e8672e2037ee22bc6b4f8e844a2d75fcb78ea8a4b38510112abc6 -# created: 2025-10-07 diff --git a/.github/.OwlBot.yaml b/.github/.OwlBot.yaml deleted file mode 100644 index fe2f7841a..000000000 --- a/.github/.OwlBot.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# 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. - -docker: - image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - -deep-remove-regex: - - /owl-bot-staging - -deep-copy-regex: - - source: /google/bigtable/admin/(v.*)/.*-py/(.*) - dest: /owl-bot-staging/bigtable_admin/$1/$2 - - source: /google/bigtable/(v.*)/.*-py/(.*) - dest: /owl-bot-staging/bigtable/$1/$2 - -begin-after-commit-hash: a21f1091413a260393548c1b2ac44b7347923f08 - diff --git a/.github/auto-approve.yml b/.github/auto-approve.yml deleted file mode 100644 index 311ebbb85..000000000 --- a/.github/auto-approve.yml +++ /dev/null @@ -1,3 +0,0 @@ -# https://github.com/googleapis/repo-automation-bots/tree/main/packages/auto-approve -processes: - - "OwlBotTemplateChanges" diff --git a/.github/release-please.yml b/.github/release-please.yml deleted file mode 100644 index 593e83f9f..000000000 --- a/.github/release-please.yml +++ /dev/null @@ -1,12 +0,0 @@ -releaseType: python -handleGHRelease: true -# NOTE: this section is generated by synthtool.languages.python -# See https://github.com/googleapis/synthtool/blob/master/synthtool/languages/python.py -manifest: true -branches: -- branch: v1 - handleGHRelease: true - releaseType: python -- branch: v0 - handleGHRelease: true - releaseType: python diff --git a/.github/release-trigger.yml b/.github/release-trigger.yml deleted file mode 100644 index 0bbdd8e4c..000000000 --- a/.github/release-trigger.yml +++ /dev/null @@ -1,2 +0,0 @@ -enabled: true -multiScmName: python-bigtable diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml deleted file mode 100644 index 14e32d6fc..000000000 --- a/.github/sync-repo-settings.yaml +++ /dev/null @@ -1,58 +0,0 @@ -# Whether or not rebase-merging is enabled on this repository. -# Defaults to `true` -rebaseMergeAllowed: true - -# Whether or not squash-merging is enabled on this repository. -# Defaults to `true` -squashMergeAllowed: true - -# Whether or not PRs are merged with a merge commit on this repository. -# Defaults to `false` -mergeCommitAllowed: false - -# Rules for main branch protection -branchProtectionRules: -# Identifies the protection rule pattern. Name of the branch to be protected. -# Defaults to `main` -- pattern: main - # Can admins overwrite branch protection. - # Defaults to `true` - isAdminEnforced: true - # Number of approving reviews required to update matching branches. - # Defaults to `1` - requiredApprovingReviewCount: 1 - # Are reviews from code owners required to update matching branches. - # Defaults to `false` - requiresCodeOwnerReviews: true - # Require up to date branches - requiresStrictStatusChecks: false - # List of required status check contexts that must pass for commits to be accepted to matching branches. - requiredStatusCheckContexts: - - 'Kokoro' - - 'Kokoro system' - - 'cla/google' - - 'OwlBot Post Processor' - - 'lint' - - 'mypy' - - 'docs' - - 'docfx' - - 'unit-3.9' - - 'unit-3.10' - - 'unit-3.11' - - 'unit-3.12' - - 'unit-3.13' - - 'unit-3.14' -# List of explicit permissions to add (additive only) -permissionRules: - # Team slug to add to repository permissions - - team: yoshi-admins - # Access level required, one of push|pull|admin|maintain|triage - permission: admin - # Team slug to add to repository permissions - - team: yoshi-python-admins - # Access level required, one of push|pull|admin|maintain|triage - permission: admin - # Team slug to add to repository permissions - - team: yoshi-python - # Access level required, one of push|pull|admin|maintain|triage - permission: push diff --git a/.librarian/generator-input/.repo-metadata.json b/.librarian/generator-input/.repo-metadata.json new file mode 100644 index 000000000..9de4b5f92 --- /dev/null +++ b/.librarian/generator-input/.repo-metadata.json @@ -0,0 +1,80 @@ +{ + "name": "bigtable", + "name_pretty": "Cloud Bigtable", + "product_documentation": "https://cloud.google.com/bigtable", + "client_documentation": "https://cloud.google.com/python/docs/reference/bigtable/latest", + "issue_tracker": "https://issuetracker.google.com/savedsearches/559777", + "release_level": "stable", + "language": "python", + "library_type": "GAPIC_COMBO", + "repo": "googleapis/python-bigtable", + "distribution_name": "google-cloud-bigtable", + "api_id": "bigtable.googleapis.com", + "requires_billing": true, + "samples": [ + { + "name": "Hello World in Cloud Bigtable", + "description": "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", + "file": "main.py", + "runnable": true, + "custom_content": "
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)
", + "override_path": "hello" + }, + { + "name": "Hello World using HappyBase", + "description": "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", + "file": "main.py", + "runnable": true, + "custom_content": "
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)
", + "override_path": "hello_happybase" + }, + { + "name": "cbt Command Demonstration", + "description": "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", + "file": "instanceadmin.py", + "runnable": true, + "custom_content": "
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)
", + "override_path": "instanceadmin" + }, + { + "name": "Metric Scaler", + "description": "This sample demonstrates how to use Stackdriver Monitoring to scale Cloud Bigtable based on CPU usage.", + "file": "metricscaler.py", + "runnable": true, + "custom_content": "
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
", + "override_path": "metricscaler" + }, + { + "name": "Quickstart", + "description": "Demonstrates of Cloud Bigtable. This sample creates a Bigtable client, connects to an instance and then to a table, then closes the connection.", + "file": "main.py", + "runnable": true, + "custom_content": "
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)
", + "override_path": "quickstart" + }, + { + "name": "Quickstart using HappyBase", + "description": "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.", + "file": "main.py", + "runnable": true, + "custom_content": "
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)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)
", + "override_path": "tableadmin" + } + ], + "default_version": "v2", + "codeowner_team": "@googleapis/api-bigtable @googleapis/api-bigtable-partners", + "api_shortname": "bigtable" +} diff --git a/owlbot.py b/.librarian/generator-input/librarian.py similarity index 73% rename from owlbot.py rename to .librarian/generator-input/librarian.py index 6b2e1ea4f..5b943d24b 100644 --- a/owlbot.py +++ b/.librarian/generator-input/librarian.py @@ -26,51 +26,6 @@ common = gcp.CommonTemplates() -# This is a customized version of the s.get_staging_dirs() function from synthtool to -# cater for copying 2 different folders from googleapis-gen -# which are bigtable and bigtable/admin. -# Source https://github.com/googleapis/synthtool/blob/master/synthtool/transforms.py#L280 -def get_staging_dirs( - default_version: Optional[str] = None, sub_directory: Optional[str] = None -) -> List[Path]: - """Returns the list of directories, one per version, copied from - https://github.com/googleapis/googleapis-gen. Will return in lexical sorting - order with the exception of the default_version which will be last (if specified). - - Args: - default_version (str): the default version of the API. The directory for this version - will be the last item in the returned list if specified. - sub_directory (str): if a `sub_directory` is provided, only the directories within the - specified `sub_directory` will be returned. - - Returns: the empty list if no file were copied. - """ - - staging = Path("owl-bot-staging") - - if sub_directory: - staging /= sub_directory - - if staging.is_dir(): - # Collect the subdirectories of the staging directory. - versions = [v.name for v in staging.iterdir() if v.is_dir()] - # Reorder the versions so the default version always comes last. - versions = [v for v in versions if v != default_version] - versions.sort() - if default_version is not None: - versions += [default_version] - dirs = [staging / v for v in versions] - for dir in dirs: - s._tracked_paths.add(dir) - return dirs - else: - return [] - -# This library ships clients for two different APIs, -# BigTable and BigTable Admin -bigtable_default_version = "v2" -bigtable_admin_default_version = "v2" - # These flags are needed because certain post-processing operations # append things after a certain line of text, and can infinitely loop # in a Github PR. We use these flags to only do those operations @@ -80,16 +35,12 @@ def get_staging_dirs( is_fresh_admin_v2_copy = False is_fresh_admin_docs_copy = False -for library in get_staging_dirs(bigtable_default_version, "bigtable"): - s.move(library / "google/cloud/bigtable_v2", excludes=["**/gapic_version.py"]) - s.move(library / "tests") - s.move(library / "scripts") - -for library in get_staging_dirs(bigtable_admin_default_version, "bigtable_admin"): +for library in s.get_staging_dirs("v2"): + s.move(library / "google/cloud/bigtable_v2") is_fresh_admin_copy = \ - s.move(library / "google/cloud/bigtable_admin", excludes=["**/gapic_version.py"]) + s.move(library / "google/cloud/bigtable_admin") is_fresh_admin_v2_copy = \ - s.move(library / "google/cloud/bigtable_admin_v2", excludes=["**/gapic_version.py"]) + s.move(library / "google/cloud/bigtable_admin_v2") s.move(library / "tests") s.move(library / "samples") s.move(library / "scripts") @@ -114,8 +65,10 @@ def get_staging_dirs( default_python_version="3.13", ) -s.move(templated_files, excludes=[".coveragerc", "README.rst", ".github/release-please.yml", "noxfile.py", "renovate.json"]) +s.move(templated_files, excludes=[".coveragerc", "README.rst", ".github/**", ".kokoro/**", "noxfile.py", "renovate.json"]) + +s.shell.run(["nox", "-s", "blacken"], hide_output=False) # ---------------------------------------------------------------------------- # Always supply app_profile_id in routing headers: https://github.com/googleapis/python-bigtable/pull/1109 @@ -131,16 +84,19 @@ def get_staging_dirs( s.replace( "tests/unit/gapic/bigtable_v2/test_bigtable.py", 'assert \(\n\s*gapic_v1\.routing_header\.to_grpc_metadata\(expected_headers\) in kw\["metadata"\]\n.*', - """ - # assert the expected headers are present, in any order - routing_string = next(iter([m[1] for m in kw["metadata"] if m[0] == 'x-goog-request-params'])) - assert all([f"{k}={v}" in routing_string for k,v in expected_headers.items()]) - """ + """# assert the expected headers are present, in any order + routing_string = next( + iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) + ) + assert all([f"{k}={v}" in routing_string for k, v in expected_headers.items()])""" ) s.replace( "tests/unit/gapic/bigtable_v2/test_bigtable.py", 'expected_headers = {"name": "projects/sample1/instances/sample2"}', - 'expected_headers = {"name": "projects/sample1/instances/sample2", "app_profile_id": ""}' + """expected_headers = { + "name": "projects/sample1/instances/sample2", + "app_profile_id": "", + }""" ) s.replace( "tests/unit/gapic/bigtable_v2/test_bigtable.py", @@ -148,13 +104,13 @@ def get_staging_dirs( expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3" } - """, +""", """ expected_headers = { "table_name": "projects/sample1/instances/sample2/tables/sample3", - "app_profile_id": "" + "app_profile_id": "", } - """ +""" ) # ---------------------------------------------------------------------------- @@ -163,15 +119,6 @@ def get_staging_dirs( python.py_samples(skip_readmes=True) -s.replace( - "samples/beam/noxfile.py", - """INSTALL_LIBRARY_FROM_SOURCE \= os.environ.get\("INSTALL_LIBRARY_FROM_SOURCE", False\) in \( - "True", - "true", -\)""", - """# todo(kolea2): temporary workaround to install pinned dep version -INSTALL_LIBRARY_FROM_SOURCE = False""") - # -------------------------------------------------------------------------- # Admin Overlay work # -------------------------------------------------------------------------- @@ -189,9 +136,8 @@ def add_overlay_to_init_py(init_py_location, import_statements, should_add): add_overlay_to_init_py( "google/cloud/bigtable_admin_v2/__init__.py", - """from .overlay import * # noqa: F403 -__all__ += overlay.__all__ # noqa: F405 -""", + """from .overlay import * # noqa: F403\n +__all__ += overlay.__all__ # noqa: F405""", is_fresh_admin_v2_copy, ) @@ -200,8 +146,7 @@ def add_overlay_to_init_py(init_py_location, import_statements, should_add): """import google.cloud.bigtable_admin_v2.overlay # noqa: F401 from google.cloud.bigtable_admin_v2.overlay import * # noqa: F401, F403 -__all__ += google.cloud.bigtable_admin_v2.overlay.__all__ -""", +__all__ += google.cloud.bigtable_admin_v2.overlay.__all__""", is_fresh_admin_copy, ) @@ -319,5 +264,3 @@ def add_overlay_to_init_py(init_py_location, import_statements, should_add): r"class GcRule\(proto\.Message\)\:", "class GcRule(oneof_message.OneofMessage):", ) - -s.shell.run(["nox", "-s", "blacken"], hide_output=False) diff --git a/.librarian/generator-input/noxfile.py b/.librarian/generator-input/noxfile.py new file mode 100644 index 000000000..16c8a6327 --- /dev/null +++ b/.librarian/generator-input/noxfile.py @@ -0,0 +1,569 @@ +# -*- coding: utf-8 -*- +# +# 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 +# +# https://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. + +# Generated by synthtool. DO NOT EDIT! + +from __future__ import absolute_import + +import os +import pathlib +import re +import shutil +from typing import Dict, List +import warnings + +import nox + +FLAKE8_VERSION = "flake8==6.1.0" +BLACK_VERSION = "black[jupyter]==23.3.0" +ISORT_VERSION = "isort==5.11.0" +LINT_PATHS = ["google", "tests", "noxfile.py", "setup.py"] + +DEFAULT_PYTHON_VERSION = "3.13" + +UNIT_TEST_PYTHON_VERSIONS: List[str] = [ + "3.7", + "3.8", + "3.9", + "3.10", + "3.11", + "3.12", + "3.13", + "3.14", +] +UNIT_TEST_STANDARD_DEPENDENCIES = [ + "mock", + "asyncmock", + "pytest", + "pytest-cov", + "pytest-asyncio", + BLACK_VERSION, + "autoflake", +] +UNIT_TEST_EXTERNAL_DEPENDENCIES: List[str] = [] +UNIT_TEST_LOCAL_DEPENDENCIES: List[str] = [] +UNIT_TEST_DEPENDENCIES: List[str] = [] +UNIT_TEST_EXTRAS: List[str] = [] +UNIT_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = {} + +SYSTEM_TEST_PYTHON_VERSIONS: List[str] = ["3.9", "3.14"] +SYSTEM_TEST_STANDARD_DEPENDENCIES: List[str] = [ + "mock", + "pytest", + "google-cloud-testutils", +] +SYSTEM_TEST_EXTERNAL_DEPENDENCIES: List[str] = [ + "pytest-asyncio==0.21.2", + BLACK_VERSION, + "pyyaml==6.0.2", +] +SYSTEM_TEST_LOCAL_DEPENDENCIES: List[str] = [] +SYSTEM_TEST_DEPENDENCIES: List[str] = [] +SYSTEM_TEST_EXTRAS: List[str] = [] +SYSTEM_TEST_EXTRAS_BY_PYTHON: Dict[str, List[str]] = {} + +CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() + +# 'docfx' is excluded since it only needs to run in 'docs-presubmit' +nox.options.sessions = [ + "unit-3.9", + "unit-3.10", + "unit-3.11", + "unit-3.12", + "unit-3.13", + "unit-3.14", + "system_emulated", + "system", + "mypy", + "cover", + "lint", + "lint_setup_py", + "blacken", + "docs", + "format", +] + +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def lint(session): + """Run linters. + + Returns a failure if the linters find linting errors or sufficiently + serious code quality issues. + """ + session.install(FLAKE8_VERSION, BLACK_VERSION) + session.run( + "black", + "--check", + *LINT_PATHS, + ) + session.run("flake8", "google", "tests") + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def blacken(session): + """Run black. Format code to uniform standard.""" + session.install(BLACK_VERSION) + session.run( + "black", + *LINT_PATHS, + ) + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def format(session): + """ + Run isort to sort imports. Then run black + to format code to uniform standard. + """ + session.install(BLACK_VERSION, ISORT_VERSION) + # 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", + *LINT_PATHS, + ) + session.run( + "black", + *LINT_PATHS, + ) + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def mypy(session): + """Verify type hints are mypy compatible.""" + session.install("-e", ".") + session.install( + "mypy", "types-setuptools", "types-protobuf", "types-mock", "types-requests" + ) + session.install("google-cloud-testutils") + session.run("mypy", "-p", "google.cloud.bigtable.data") + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def lint_setup_py(session): + """Verify that setup.py is valid (including RST check).""" + session.install("setuptools", "docutils", "pygments") + session.run("python", "setup.py", "check", "--restructuredtext", "--strict") + + +def install_unittest_dependencies(session, *constraints): + standard_deps = UNIT_TEST_STANDARD_DEPENDENCIES + UNIT_TEST_DEPENDENCIES + session.install(*standard_deps, *constraints) + + if UNIT_TEST_EXTERNAL_DEPENDENCIES: + warnings.warn( + "'unit_test_external_dependencies' is deprecated. Instead, please " + "use 'unit_test_dependencies' or 'unit_test_local_dependencies'.", + DeprecationWarning, + ) + session.install(*UNIT_TEST_EXTERNAL_DEPENDENCIES, *constraints) + + if UNIT_TEST_LOCAL_DEPENDENCIES: + session.install(*UNIT_TEST_LOCAL_DEPENDENCIES, *constraints) + + if UNIT_TEST_EXTRAS_BY_PYTHON: + extras = UNIT_TEST_EXTRAS_BY_PYTHON.get(session.python, []) + elif UNIT_TEST_EXTRAS: + extras = UNIT_TEST_EXTRAS + else: + extras = [] + + if extras: + session.install("-e", f".[{','.join(extras)}]", *constraints) + else: + session.install("-e", ".", *constraints) + + +@nox.session(python=UNIT_TEST_PYTHON_VERSIONS) +@nox.parametrize( + "protobuf_implementation", + ["python", "upb", "cpp"], +) +def unit(session, protobuf_implementation): + # Install all test dependencies, then install this package in-place. + py_version = tuple([int(v) for v in session.python.split(".")]) + if protobuf_implementation == "cpp" and py_version >= (3, 11): + session.skip("cpp implementation is not supported in python 3.11+") + + constraints_path = str( + CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" + ) + install_unittest_dependencies(session, "-c", constraints_path) + + # TODO(https://github.com/googleapis/synthtool/issues/1976): + # Remove the 'cpp' implementation once support for Protobuf 3.x is dropped. + # The 'cpp' implementation requires Protobuf<4. + if protobuf_implementation == "cpp": + session.install("protobuf<4") + + # Run py.test against the unit tests. + session.run( + "py.test", + "--quiet", + f"--junitxml=unit_{session.python}_sponge_log.xml", + "--cov=google", + "--cov=tests/unit", + "--cov-append", + "--cov-config=.coveragerc", + "--cov-report=", + "--cov-fail-under=0", + os.path.join("tests", "unit"), + *session.posargs, + env={ + "PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": protobuf_implementation, + }, + ) + + +def install_systemtest_dependencies(session, *constraints): + # Use pre-release gRPC for system tests. + # Exclude version 1.52.0rc1 which has a known issue. + # See https://github.com/grpc/grpc/issues/32163 + session.install("--pre", "grpcio!=1.52.0rc1") + + session.install(*SYSTEM_TEST_STANDARD_DEPENDENCIES, *constraints) + + if SYSTEM_TEST_EXTERNAL_DEPENDENCIES: + session.install(*SYSTEM_TEST_EXTERNAL_DEPENDENCIES, *constraints) + + if SYSTEM_TEST_LOCAL_DEPENDENCIES: + session.install("-e", *SYSTEM_TEST_LOCAL_DEPENDENCIES, *constraints) + + if SYSTEM_TEST_DEPENDENCIES: + session.install("-e", *SYSTEM_TEST_DEPENDENCIES, *constraints) + + if SYSTEM_TEST_EXTRAS_BY_PYTHON: + extras = SYSTEM_TEST_EXTRAS_BY_PYTHON.get(session.python, []) + elif SYSTEM_TEST_EXTRAS: + extras = SYSTEM_TEST_EXTRAS + else: + extras = [] + + if extras: + session.install("-e", f".[{','.join(extras)}]", *constraints) + else: + session.install("-e", ".", *constraints) + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def system_emulated(session): + import subprocess + import signal + + try: + subprocess.call(["gcloud", "--version"]) + except OSError: + session.skip("gcloud not found but required for emulator support") + + # Currently, CI/CD doesn't have beta component of gcloud. + subprocess.call(["gcloud", "components", "install", "beta", "bigtable"]) + + hostport = "localhost:8789" + session.env["BIGTABLE_EMULATOR_HOST"] = hostport + + p = subprocess.Popen( + ["gcloud", "beta", "emulators", "bigtable", "start", "--host-port", hostport] + ) + + try: + system(session) + finally: + # Stop Emulator + os.killpg(os.getpgid(p.pid), signal.SIGKILL) + + +@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) +@nox.parametrize("client_type", ["async", "sync", "legacy"]) +def conformance(session, client_type): + # install dependencies + constraints_path = str( + CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" + ) + install_unittest_dependencies(session, "-c", constraints_path) + with session.chdir("test_proxy"): + # download the conformance test suite + session.run( + "bash", + "-e", + "run_tests.sh", + external=True, + env={"CLIENT_TYPE": client_type}, + ) + + +@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) +def system(session): + """Run the system test suite.""" + constraints_path = str( + CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" + ) + system_test_path = os.path.join("tests", "system.py") + system_test_folder_path = os.path.join("tests", "system") + + # Check the value of `RUN_SYSTEM_TESTS` env var. It defaults to true. + if os.environ.get("RUN_SYSTEM_TESTS", "true") == "false": + session.skip("RUN_SYSTEM_TESTS is set to false, skipping") + # Install pyopenssl for mTLS testing. + if os.environ.get("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") == "true": + session.install("pyopenssl") + + system_test_exists = os.path.exists(system_test_path) + system_test_folder_exists = os.path.exists(system_test_folder_path) + # Sanity check: only run tests if found. + if not system_test_exists and not system_test_folder_exists: + session.skip("System tests were not found") + + install_systemtest_dependencies(session, "-c", constraints_path) + + # Run py.test against the system tests. + if system_test_exists: + session.run( + "py.test", + "--quiet", + f"--junitxml=system_{session.python}_sponge_log.xml", + system_test_path, + *session.posargs, + ) + if system_test_folder_exists: + session.run( + "py.test", + "--quiet", + f"--junitxml=system_{session.python}_sponge_log.xml", + system_test_folder_path, + *session.posargs, + ) + + +@nox.session(python=DEFAULT_PYTHON_VERSION) +def cover(session): + """Run the final coverage report. + + This outputs the coverage report aggregating coverage from the unit + test runs (not system test runs), and then erases coverage data. + """ + session.install("coverage", "pytest-cov") + session.run("coverage", "report", "--show-missing", "--fail-under=99") + + session.run("coverage", "erase") + + +@nox.session(python="3.10") +def docs(session): + """Build the docs for this library.""" + + session.install("-e", ".") + session.install( + # We need to pin to specific versions of the `sphinxcontrib-*` packages + # which still support sphinx 4.x. + # See https://github.com/googleapis/sphinx-docfx-yaml/issues/344 + # and https://github.com/googleapis/sphinx-docfx-yaml/issues/345. + "sphinxcontrib-applehelp==1.0.4", + "sphinxcontrib-devhelp==1.0.2", + "sphinxcontrib-htmlhelp==2.0.1", + "sphinxcontrib-qthelp==1.0.3", + "sphinxcontrib-serializinghtml==1.1.5", + "sphinx==4.5.0", + "alabaster", + "recommonmark", + ) + + shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) + session.run( + "sphinx-build", + "-W", # warnings as errors + "-T", # show full traceback on exception + "-N", # no colors + "-b", + "html", + "-d", + os.path.join("docs", "_build", "doctrees", ""), + os.path.join("docs", ""), + os.path.join("docs", "_build", "html", ""), + ) + + +@nox.session(python="3.10") +def docfx(session): + """Build the docfx yaml files for this library.""" + + session.install("-e", ".") + session.install( + # We need to pin to specific versions of the `sphinxcontrib-*` packages + # which still support sphinx 4.x. + # See https://github.com/googleapis/sphinx-docfx-yaml/issues/344 + # and https://github.com/googleapis/sphinx-docfx-yaml/issues/345. + "sphinxcontrib-applehelp==1.0.4", + "sphinxcontrib-devhelp==1.0.2", + "sphinxcontrib-htmlhelp==2.0.1", + "sphinxcontrib-qthelp==1.0.3", + "sphinxcontrib-serializinghtml==1.1.5", + "gcp-sphinx-docfx-yaml", + "alabaster", + "recommonmark", + ) + + shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True) + session.run( + "sphinx-build", + "-T", # show full traceback on exception + "-N", # no colors + "-D", + ( + "extensions=sphinx.ext.autodoc," + "sphinx.ext.autosummary," + "docfx_yaml.extension," + "sphinx.ext.intersphinx," + "sphinx.ext.coverage," + "sphinx.ext.napoleon," + "sphinx.ext.todo," + "sphinx.ext.viewcode," + "recommonmark" + ), + "-b", + "html", + "-d", + os.path.join("docs", "_build", "doctrees", ""), + os.path.join("docs", ""), + os.path.join("docs", "_build", "html", ""), + ) + # Customization: Add extra sections to the table of contents for the Classic vs Async clients + session.install("pyyaml") + session.run("python", "docs/scripts/patch_devsite_toc.py") + + +@nox.session(python="3.14") +@nox.parametrize( + "protobuf_implementation", + ["python", "upb", "cpp"], +) +def prerelease_deps(session, protobuf_implementation): + """Run all tests with prerelease versions of dependencies installed.""" + + py_version = tuple([int(v) for v in session.python.split(".")]) + if protobuf_implementation == "cpp" and py_version >= (3, 11): + session.skip("cpp implementation is not supported in python 3.11+") + + # Install all dependencies + session.install("-e", ".[all, tests, tracing]") + unit_deps_all = UNIT_TEST_STANDARD_DEPENDENCIES + UNIT_TEST_EXTERNAL_DEPENDENCIES + session.install(*unit_deps_all) + system_deps_all = ( + SYSTEM_TEST_STANDARD_DEPENDENCIES + SYSTEM_TEST_EXTERNAL_DEPENDENCIES + ) + session.install(*system_deps_all) + + # Because we test minimum dependency versions on the minimum Python + # version, the first version we test with in the unit tests sessions has a + # constraints file containing all dependencies and extras. + with open( + CURRENT_DIRECTORY + / "testing" + / f"constraints-{UNIT_TEST_PYTHON_VERSIONS[0]}.txt", + encoding="utf-8", + ) as constraints_file: + constraints_text = constraints_file.read() + + # Ignore leading whitespace and comment lines. + constraints_deps = [ + match.group(1) + for match in re.finditer( + r"^\s*(\S+)(?===\S+)", constraints_text, flags=re.MULTILINE + ) + ] + + session.install(*constraints_deps) + + prerel_deps = [ + "protobuf", + # dependency of grpc + "six", + "grpc-google-iam-v1", + "googleapis-common-protos", + "grpcio", + "grpcio-status", + "google-api-core", + "google-auth", + "proto-plus", + "google-cloud-testutils", + # dependencies of google-cloud-testutils" + "click", + ] + + for dep in prerel_deps: + session.install("--pre", "--no-deps", "--upgrade", dep) + + # Remaining dependencies + other_deps = [ + "requests", + ] + session.install(*other_deps) + + # Print out prerelease package versions + session.run( + "python", "-c", "import google.protobuf; print(google.protobuf.__version__)" + ) + session.run("python", "-c", "import grpc; print(grpc.__version__)") + session.run("python", "-c", "import google.auth; print(google.auth.__version__)") + + session.run( + "py.test", + "tests/unit", + env={ + "PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": protobuf_implementation, + }, + ) + + system_test_path = os.path.join("tests", "system.py") + system_test_folder_path = os.path.join("tests", "system") + + # Only run system tests if found. + if os.path.exists(system_test_path): + session.run( + "py.test", + "--verbose", + f"--junitxml=system_{session.python}_sponge_log.xml", + system_test_path, + *session.posargs, + env={ + "PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": protobuf_implementation, + }, + ) + if os.path.exists(system_test_folder_path): + session.run( + "py.test", + "--verbose", + f"--junitxml=system_{session.python}_sponge_log.xml", + system_test_folder_path, + *session.posargs, + env={ + "PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": protobuf_implementation, + }, + ) + + +@nox.session(python="3.10") +def generate_sync(session): + """ + Re-generate sync files for the library from CrossSync-annotated async source + """ + session.install(BLACK_VERSION) + session.install("autoflake") + session.run("python", ".cross_sync/generate.py", ".") diff --git a/.librarian/generator-input/setup.py b/.librarian/generator-input/setup.py new file mode 100644 index 000000000..cac533db6 --- /dev/null +++ b/.librarian/generator-input/setup.py @@ -0,0 +1,104 @@ +# 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 io +import os + +import setuptools + + +package_root = os.path.abspath(os.path.dirname(__file__)) + +# Package metadata. + +name = "google-cloud-bigtable" +description = "Google Cloud Bigtable API client library" + +version = {} +with open(os.path.join(package_root, "google/cloud/bigtable/gapic_version.py")) as fp: + exec(fp.read(), version) +version = version["__version__"] + + +# Should be one of: +# 'Development Status :: 3 - Alpha' +# 'Development Status :: 4 - Beta' +# 'Development Status :: 5 - Production/Stable' +release_status = "Development Status :: 5 - Production/Stable" +dependencies = [ + "google-api-core[grpc] >= 2.17.0, <3.0.0", + "google-cloud-core >= 1.4.4, <3.0.0", + "google-auth >= 2.23.0, <3.0.0,!=2.24.0,!=2.25.0", + "grpc-google-iam-v1 >= 0.12.4, <1.0.0", + "proto-plus >= 1.22.3, <2.0.0", + "proto-plus >= 1.25.0, <2.0.0; python_version>='3.13'", + "protobuf>=3.20.2,<7.0.0,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5", + "google-crc32c>=1.5.0, <2.0.0dev", +] +extras = {"libcst": "libcst >= 0.2.5"} + + +# Setup boilerplate below this line. + +package_root = os.path.abspath(os.path.dirname(__file__)) + +readme_filename = os.path.join(package_root, "README.rst") +with io.open(readme_filename, encoding="utf-8") as readme_file: + readme = readme_file.read() + +# Only include packages under the 'google' namespace. Do not include tests, +# benchmarks, etc. +packages = [ + package + for package in setuptools.find_namespace_packages() + if package.startswith("google") +] + +setuptools.setup( + name=name, + version=version, + description=description, + long_description=readme, + author="Google LLC", + author_email="googleapis-packages@google.com", + license="Apache 2.0", + url="https://github.com/googleapis/python-bigtable", + classifiers=[ + release_status, + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Operating System :: OS Independent", + "Topic :: Internet", + ], + platforms="Posix; MacOS X; Windows", + packages=packages, + install_requires=dependencies, + extras_require=extras, + scripts=[ + "scripts/fixup_bigtable_v2_keywords.py", + "scripts/fixup_admin_v2_keywords.py", + ], + python_requires=">=3.7", + include_package_data=True, + zip_safe=False, +) diff --git a/.librarian/state.yaml b/.librarian/state.yaml new file mode 100644 index 000000000..049e7b1cf --- /dev/null +++ b/.librarian/state.yaml @@ -0,0 +1,40 @@ +image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:8e2c32496077054105bd06c54a59d6a6694287bc053588e24debe6da6920ad91 +libraries: + - id: google-cloud-bigtable + version: 2.34.0 + last_generated_commit: a17b84add8318f780fcc8a027815d5fee644b9f7 + apis: + - path: google/bigtable/v2 + service_config: bigtable_v2.yaml + - path: google/bigtable/admin/v2 + service_config: bigtableadmin_v2.yaml + source_roots: + - . + preserve_regex: [] + remove_regex: + - ^.pre-commit-config.yaml + - ^.repo-metadata.json + - ^.trampolinerc + - ^docs/admin_client/bigtable + - ^docs/admin_client/services_.rst + - ^docs/admin_client/types_.rst + - ^docs/summary_overview.md + - ^google/cloud/bigtable_v2 + - ^google/cloud/bigtable_admin/ + - ^google/cloud/bigtable_admin_v2/services + - ^google/cloud/bigtable_admin_v2/types + - ^google/cloud/bigtable_admin_v2/__init__.py + - ^google/cloud/bigtable_admin_v2/gapic + - ^google/cloud/bigtable_admin_v2/py.typed + - ^samples/AUTHORING_GUIDE.md + - ^samples/CONTRIBUTING.md + - ^samples/generated_samples + - ^tests/unit/gapic + - ^noxfile.py + - ^scripts/fixup_bigtable + - ^setup.py + - ^SECURITY.md + - ^tests/__init__.py + - ^tests/unit/__init__.py + - ^tests/unit/gapic + tag_format: v{version} diff --git a/.release-please-manifest.json b/.release-please-manifest.json deleted file mode 100644 index 7887ba932..000000000 --- a/.release-please-manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "2.34.0" -} \ No newline at end of file diff --git a/google/cloud/bigtable_admin/gapic_version.py b/google/cloud/bigtable_admin/gapic_version.py index 4800b0559..b31b170e1 100644 --- a/google/cloud/bigtable_admin/gapic_version.py +++ b/google/cloud/bigtable_admin/gapic_version.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2022 Google LLC +# 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. diff --git a/google/cloud/bigtable_admin_v2/gapic_version.py b/google/cloud/bigtable_admin_v2/gapic_version.py index 4800b0559..b31b170e1 100644 --- a/google/cloud/bigtable_admin_v2/gapic_version.py +++ b/google/cloud/bigtable_admin_v2/gapic_version.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2022 Google LLC +# 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. diff --git a/google/cloud/bigtable_v2/gapic_version.py b/google/cloud/bigtable_v2/gapic_version.py index 4800b0559..b31b170e1 100644 --- a/google/cloud/bigtable_v2/gapic_version.py +++ b/google/cloud/bigtable_v2/gapic_version.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2022 Google LLC +# 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. diff --git a/noxfile.py b/noxfile.py index a182bafba..16c8a6327 100644 --- a/noxfile.py +++ b/noxfile.py @@ -30,7 +30,7 @@ FLAKE8_VERSION = "flake8==6.1.0" BLACK_VERSION = "black[jupyter]==23.3.0" ISORT_VERSION = "isort==5.11.0" -LINT_PATHS = ["docs", "google", "tests", "noxfile.py", "setup.py"] +LINT_PATHS = ["google", "tests", "noxfile.py", "setup.py"] DEFAULT_PYTHON_VERSION = "3.13" diff --git a/release-please-config.json b/release-please-config.json deleted file mode 100644 index 33d5a7e21..000000000 --- a/release-please-config.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": -"https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "packages": { - ".": { - "release-type": "python", - "extra-files": [ - "google/cloud/bigtable/gapic_version.py", - "google/cloud/bigtable_admin/gapic_version.py", - "google/cloud/bigtable_v2/gapic_version.py", - "google/cloud/bigtable_admin_v2/gapic_version.py" - ] - } - }, - "release-type": "python", - "plugins": [ - { - "type": "sentence-case" - } - ], - "initial-version": "2.13.2" -} diff --git a/samples/generated_samples/snippet_metadata_google.bigtable.admin.v2.json b/samples/generated_samples/snippet_metadata_google.bigtable.admin.v2.json index 66b5c8f67..8b4f59fbd 100644 --- a/samples/generated_samples/snippet_metadata_google.bigtable.admin.v2.json +++ b/samples/generated_samples/snippet_metadata_google.bigtable.admin.v2.json @@ -8,7 +8,7 @@ ], "language": "PYTHON", "name": "google-cloud-bigtable-admin", - "version": "0.0.0" + "version": "2.34.0" }, "snippets": [ { diff --git a/tests/unit/gapic/bigtable_v2/test_bigtable.py b/tests/unit/gapic/bigtable_v2/test_bigtable.py index 24db8e269..9922999d6 100644 --- a/tests/unit/gapic/bigtable_v2/test_bigtable.py +++ b/tests/unit/gapic/bigtable_v2/test_bigtable.py @@ -6854,7 +6854,6 @@ def test_read_rows_routing_parameters_request_1_grpc(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -6881,7 +6880,6 @@ def test_read_rows_routing_parameters_request_2_grpc(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -6919,7 +6917,6 @@ def test_read_rows_routing_parameters_request_3_grpc(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -6955,7 +6952,6 @@ def test_read_rows_routing_parameters_request_4_grpc(): "name": "projects/sample1/instances/sample2", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -6989,7 +6985,6 @@ def test_sample_row_keys_routing_parameters_request_1_grpc(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7016,7 +7011,6 @@ def test_sample_row_keys_routing_parameters_request_2_grpc(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7054,7 +7048,6 @@ def test_sample_row_keys_routing_parameters_request_3_grpc(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7090,7 +7083,6 @@ def test_sample_row_keys_routing_parameters_request_4_grpc(): "name": "projects/sample1/instances/sample2", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7124,7 +7116,6 @@ def test_mutate_row_routing_parameters_request_1_grpc(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7151,7 +7142,6 @@ def test_mutate_row_routing_parameters_request_2_grpc(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7189,7 +7179,6 @@ def test_mutate_row_routing_parameters_request_3_grpc(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7223,7 +7212,6 @@ def test_mutate_rows_routing_parameters_request_1_grpc(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7250,7 +7238,6 @@ def test_mutate_rows_routing_parameters_request_2_grpc(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7288,7 +7275,6 @@ def test_mutate_rows_routing_parameters_request_3_grpc(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7324,7 +7310,6 @@ def test_check_and_mutate_row_routing_parameters_request_1_grpc(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7353,7 +7338,6 @@ def test_check_and_mutate_row_routing_parameters_request_2_grpc(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7393,7 +7377,6 @@ def test_check_and_mutate_row_routing_parameters_request_3_grpc(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7425,7 +7408,6 @@ def test_ping_and_warm_routing_parameters_request_1_grpc(): "name": "projects/sample1/instances/sample2", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7452,7 +7434,6 @@ def test_ping_and_warm_routing_parameters_request_2_grpc(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7488,7 +7469,6 @@ def test_read_modify_write_row_routing_parameters_request_1_grpc(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7519,7 +7499,6 @@ def test_read_modify_write_row_routing_parameters_request_2_grpc(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7559,7 +7538,6 @@ def test_read_modify_write_row_routing_parameters_request_3_grpc(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7593,7 +7571,6 @@ def test_prepare_query_routing_parameters_request_1_grpc(): "name": "projects/sample1/instances/sample2", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7620,7 +7597,6 @@ def test_prepare_query_routing_parameters_request_2_grpc(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7654,7 +7630,6 @@ def test_execute_query_routing_parameters_request_1_grpc(): "name": "projects/sample1/instances/sample2", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -7681,7 +7656,6 @@ def test_execute_query_routing_parameters_request_2_grpc(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8027,7 +8001,6 @@ async def test_read_rows_routing_parameters_request_1_grpc_asyncio(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8059,7 +8032,6 @@ async def test_read_rows_routing_parameters_request_2_grpc_asyncio(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8102,7 +8074,6 @@ async def test_read_rows_routing_parameters_request_3_grpc_asyncio(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8143,7 +8114,6 @@ async def test_read_rows_routing_parameters_request_4_grpc_asyncio(): "name": "projects/sample1/instances/sample2", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8182,7 +8152,6 @@ async def test_sample_row_keys_routing_parameters_request_1_grpc_asyncio(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8214,7 +8183,6 @@ async def test_sample_row_keys_routing_parameters_request_2_grpc_asyncio(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8257,7 +8225,6 @@ async def test_sample_row_keys_routing_parameters_request_3_grpc_asyncio(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8298,7 +8265,6 @@ async def test_sample_row_keys_routing_parameters_request_4_grpc_asyncio(): "name": "projects/sample1/instances/sample2", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8336,7 +8302,6 @@ async def test_mutate_row_routing_parameters_request_1_grpc_asyncio(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8367,7 +8332,6 @@ async def test_mutate_row_routing_parameters_request_2_grpc_asyncio(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8409,7 +8373,6 @@ async def test_mutate_row_routing_parameters_request_3_grpc_asyncio(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8448,7 +8411,6 @@ async def test_mutate_rows_routing_parameters_request_1_grpc_asyncio(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8480,7 +8442,6 @@ async def test_mutate_rows_routing_parameters_request_2_grpc_asyncio(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8523,7 +8484,6 @@ async def test_mutate_rows_routing_parameters_request_3_grpc_asyncio(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8565,7 +8525,6 @@ async def test_check_and_mutate_row_routing_parameters_request_1_grpc_asyncio(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8600,7 +8559,6 @@ async def test_check_and_mutate_row_routing_parameters_request_2_grpc_asyncio(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8646,7 +8604,6 @@ async def test_check_and_mutate_row_routing_parameters_request_3_grpc_asyncio(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8684,7 +8641,6 @@ async def test_ping_and_warm_routing_parameters_request_1_grpc_asyncio(): "name": "projects/sample1/instances/sample2", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8715,7 +8671,6 @@ async def test_ping_and_warm_routing_parameters_request_2_grpc_asyncio(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8755,7 +8710,6 @@ async def test_read_modify_write_row_routing_parameters_request_1_grpc_asyncio() "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8790,7 +8744,6 @@ async def test_read_modify_write_row_routing_parameters_request_2_grpc_asyncio() assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8834,7 +8787,6 @@ async def test_read_modify_write_row_routing_parameters_request_3_grpc_asyncio() "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8874,7 +8826,6 @@ async def test_prepare_query_routing_parameters_request_1_grpc_asyncio(): "name": "projects/sample1/instances/sample2", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8907,7 +8858,6 @@ async def test_prepare_query_routing_parameters_request_2_grpc_asyncio(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8946,7 +8896,6 @@ async def test_execute_query_routing_parameters_request_1_grpc_asyncio(): "name": "projects/sample1/instances/sample2", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -8978,7 +8927,6 @@ async def test_execute_query_routing_parameters_request_2_grpc_asyncio(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -10634,7 +10582,6 @@ def test_read_rows_routing_parameters_request_1_rest(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -10660,7 +10607,6 @@ def test_read_rows_routing_parameters_request_2_rest(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -10697,7 +10643,6 @@ def test_read_rows_routing_parameters_request_3_rest(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -10732,7 +10677,6 @@ def test_read_rows_routing_parameters_request_4_rest(): "name": "projects/sample1/instances/sample2", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -10765,7 +10709,6 @@ def test_sample_row_keys_routing_parameters_request_1_rest(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -10791,7 +10734,6 @@ def test_sample_row_keys_routing_parameters_request_2_rest(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -10828,7 +10770,6 @@ def test_sample_row_keys_routing_parameters_request_3_rest(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -10863,7 +10804,6 @@ def test_sample_row_keys_routing_parameters_request_4_rest(): "name": "projects/sample1/instances/sample2", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -10896,7 +10836,6 @@ def test_mutate_row_routing_parameters_request_1_rest(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -10922,7 +10861,6 @@ def test_mutate_row_routing_parameters_request_2_rest(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -10959,7 +10897,6 @@ def test_mutate_row_routing_parameters_request_3_rest(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -10992,7 +10929,6 @@ def test_mutate_rows_routing_parameters_request_1_rest(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -11018,7 +10954,6 @@ def test_mutate_rows_routing_parameters_request_2_rest(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -11055,7 +10990,6 @@ def test_mutate_rows_routing_parameters_request_3_rest(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -11090,7 +11024,6 @@ def test_check_and_mutate_row_routing_parameters_request_1_rest(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -11118,7 +11051,6 @@ def test_check_and_mutate_row_routing_parameters_request_2_rest(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -11157,7 +11089,6 @@ def test_check_and_mutate_row_routing_parameters_request_3_rest(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -11188,7 +11119,6 @@ def test_ping_and_warm_routing_parameters_request_1_rest(): "name": "projects/sample1/instances/sample2", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -11214,7 +11144,6 @@ def test_ping_and_warm_routing_parameters_request_2_rest(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -11249,7 +11178,6 @@ def test_read_modify_write_row_routing_parameters_request_1_rest(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -11279,7 +11207,6 @@ def test_read_modify_write_row_routing_parameters_request_2_rest(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -11318,7 +11245,6 @@ def test_read_modify_write_row_routing_parameters_request_3_rest(): "table_name": "projects/sample1/instances/sample2/tables/sample3", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -11351,7 +11277,6 @@ def test_prepare_query_routing_parameters_request_1_rest(): "name": "projects/sample1/instances/sample2", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -11377,7 +11302,6 @@ def test_prepare_query_routing_parameters_request_2_rest(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -11410,7 +11334,6 @@ def test_execute_query_routing_parameters_request_1_rest(): "name": "projects/sample1/instances/sample2", "app_profile_id": "", } - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) @@ -11436,7 +11359,6 @@ def test_execute_query_routing_parameters_request_2_rest(): assert args[0] == request_msg expected_headers = {"app_profile_id": "sample1"} - # assert the expected headers are present, in any order routing_string = next( iter([m[1] for m in kw["metadata"] if m[0] == "x-goog-request-params"]) From b74fe79e47cb9c0227b50459945048e02d2dd456 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 24 Nov 2025 18:02:07 -0500 Subject: [PATCH 04/10] chore: remove releases.md (#1237) Remove [releases.md](https://github.com/googleapis/python-bigtable/blob/main/releases.md) which is unused. This points to CHANGELOG.md at `../../bigtable/CHANGELOG.md` which doesn't exist --- releases.md | 1 - 1 file changed, 1 deletion(-) delete mode 120000 releases.md diff --git a/releases.md b/releases.md deleted file mode 120000 index 4c43d4932..000000000 --- a/releases.md +++ /dev/null @@ -1 +0,0 @@ -../../bigtable/CHANGELOG.md \ No newline at end of file From 044efe74e0ae8eae46d5567276b5a5d9b699c315 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 3 Dec 2025 22:38:53 +0000 Subject: [PATCH 05/10] chore(deps): update all dependencies (#1129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [apache-beam](https://beam.apache.org) | `==2.65.0` -> `==2.69.0` | ![age](https://developer.mend.io/api/mc/badges/age/pypi/apache-beam/2.69.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/apache-beam/2.65.0/2.69.0?slim=true) | | [google-cloud-bigtable](https://redirect.github.com/googleapis/python-bigtable) | `==2.30.1` -> `==2.34.0` | ![age](https://developer.mend.io/api/mc/badges/age/pypi/google-cloud-bigtable/2.34.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/google-cloud-bigtable/2.30.1/2.34.0?slim=true) | | [google-cloud-core](https://redirect.github.com/googleapis/python-cloud-core) | `==2.4.3` -> `==2.5.0` | ![age](https://developer.mend.io/api/mc/badges/age/pypi/google-cloud-core/2.5.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/google-cloud-core/2.4.3/2.5.0?slim=true) | | [google-cloud-monitoring](https://redirect.github.com/googleapis/google-cloud-python/tree/main/packages/google-cloud-monitoring) ([source](https://redirect.github.com/googleapis/google-cloud-python)) | `==2.27.1` -> `==2.28.0` | ![age](https://developer.mend.io/api/mc/badges/age/pypi/google-cloud-monitoring/2.28.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/google-cloud-monitoring/2.27.1/2.28.0?slim=true) | | [google-cloud-testutils](https://redirect.github.com/googleapis/python-test-utils) | `==1.6.4` -> `==1.7.0` | ![age](https://developer.mend.io/api/mc/badges/age/pypi/google-cloud-testutils/1.7.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/google-cloud-testutils/1.6.4/1.7.0?slim=true) | --- ### Release Notes
googleapis/python-bigtable (google-cloud-bigtable) ### [`v2.34.0`](https://redirect.github.com/googleapis/python-bigtable/blob/HEAD/CHANGELOG.md#2340-2025-10-16) [Compare Source](https://redirect.github.com/googleapis/python-bigtable/compare/v2.33.0...v2.34.0) ##### Features - Add support for Python 3.14 ([#​1217](https://redirect.github.com/googleapis/python-bigtable/issues/1217)) ([263332a](https://redirect.github.com/googleapis/python-bigtable/commit/263332af71a229cb4fa598008a708137086a6f67)) ### [`v2.33.0`](https://redirect.github.com/googleapis/python-bigtable/blob/HEAD/CHANGELOG.md#2330-2025-10-06) [Compare Source](https://redirect.github.com/googleapis/python-bigtable/compare/v2.32.0...v2.33.0) ##### Features - Add support for Proto and Enum types ([#​1202](https://redirect.github.com/googleapis/python-bigtable/issues/1202)) ([34ceb86](https://redirect.github.com/googleapis/python-bigtable/commit/34ceb86007db08d453fa25cca4968d5b498ffcd6)) - Expose universe\_domain for tpc ([#​1150](https://redirect.github.com/googleapis/python-bigtable/issues/1150)) ([451fd97](https://redirect.github.com/googleapis/python-bigtable/commit/451fd97e435218ffed47d39423680ffc4feccac4)) ##### Bug Fixes - Fix instance registration cleanup on early iterator termination ([#​1216](https://redirect.github.com/googleapis/python-bigtable/issues/1216)) ([bbfd746](https://redirect.github.com/googleapis/python-bigtable/commit/bbfd746c61a6362efa42c7899ec3e34ceb541c83)) - Refactor channel refresh ([#​1174](https://redirect.github.com/googleapis/python-bigtable/issues/1174)) ([6fa3008](https://redirect.github.com/googleapis/python-bigtable/commit/6fa30084058bc34d4487d1fee5c87d7795ff167a)) ### [`v2.32.0`](https://redirect.github.com/googleapis/python-bigtable/blob/HEAD/CHANGELOG.md#2320-2025-08-01) [Compare Source](https://redirect.github.com/googleapis/python-bigtable/compare/v2.31.0...v2.32.0) ##### Features - Add Idempotency to Cloud Bigtable MutateRowsRequest API ([#​1143](https://redirect.github.com/googleapis/python-bigtable/issues/1143)) ([c3e3eb0](https://redirect.github.com/googleapis/python-bigtable/commit/c3e3eb0e4ce44ece72b150dc5822846627074fba)) - Add support for AddToCell in Data Client ([#​1147](https://redirect.github.com/googleapis/python-bigtable/issues/1147)) ([1a5b4b5](https://redirect.github.com/googleapis/python-bigtable/commit/1a5b4b514cadae5c83d61296314285d3774992c5)) - Implement SQL support in test proxy ([#​1106](https://redirect.github.com/googleapis/python-bigtable/issues/1106)) ([7a91bbf](https://redirect.github.com/googleapis/python-bigtable/commit/7a91bbfb9df23f7e93c40b88648840342af6f16f)) - Modernized Bigtable Admin Client featuring selective GAPIC generation ([#​1177](https://redirect.github.com/googleapis/python-bigtable/issues/1177)) ([58e7d37](https://redirect.github.com/googleapis/python-bigtable/commit/58e7d3782df6b13a42af053263afc575222a6b83)) ### [`v2.31.0`](https://redirect.github.com/googleapis/python-bigtable/blob/HEAD/CHANGELOG.md#2310-2025-05-22) [Compare Source](https://redirect.github.com/googleapis/python-bigtable/compare/v2.30.1...v2.31.0) ##### Features - Add deletion\_protection support for LVs ([#​1108](https://redirect.github.com/googleapis/python-bigtable/issues/1108)) ([c6d384d](https://redirect.github.com/googleapis/python-bigtable/commit/c6d384d4a104c182326e22dc3f10b7b905780dee)) - Support authorized views ([#​1034](https://redirect.github.com/googleapis/python-bigtable/issues/1034)) ([97a0198](https://redirect.github.com/googleapis/python-bigtable/commit/97a019833d82e617769c56761aa5548d3ab896b9)) - Throw better error on invalid metadata response ([#​1107](https://redirect.github.com/googleapis/python-bigtable/issues/1107)) ([2642317](https://redirect.github.com/googleapis/python-bigtable/commit/2642317077b723ca8fd62aa86322b524868c2c4d)) ##### Bug Fixes - Re-add py-typed file for bigtable package ([#​1085](https://redirect.github.com/googleapis/python-bigtable/issues/1085)) ([0c322c7](https://redirect.github.com/googleapis/python-bigtable/commit/0c322c79ecbe4cde3e79d8e83ac655a978d07877))
googleapis/python-cloud-core (google-cloud-core) ### [`v2.5.0`](https://redirect.github.com/googleapis/python-cloud-core/blob/HEAD/CHANGELOG.md#250-2025-10-27) [Compare Source](https://redirect.github.com/googleapis/python-cloud-core/compare/v2.4.3...v2.5.0) ##### Features - Add Python 3.14 support ([#​333](https://redirect.github.com/googleapis/python-cloud-core/issues/333)) ([c26e587](https://redirect.github.com/googleapis/python-cloud-core/commit/c26e587a79b1905f4d64e409b853d9e50bbb6c17)) ##### Bug Fixes - Remove setup.cfg configuration for creating universal wheels ([#​332](https://redirect.github.com/googleapis/python-cloud-core/issues/332)) ([78ce8a6](https://redirect.github.com/googleapis/python-cloud-core/commit/78ce8a6f7c781cf2585131d7961beec9069a206c)) - Resolve issue where pre-release versions of dependencies are installed ([#​329](https://redirect.github.com/googleapis/python-cloud-core/issues/329)) ([ab9785d](https://redirect.github.com/googleapis/python-cloud-core/commit/ab9785d1bce1d50a03ff41d9b377dc7632772105))
googleapis/google-cloud-python (google-cloud-monitoring) ### [`v2.28.0`](https://redirect.github.com/googleapis/google-cloud-python/compare/google-cloud-speech-v2.27.0...google-cloud-speech-v2.28.0) [Compare Source](https://redirect.github.com/googleapis/google-cloud-python/compare/google-cloud-monitoring-v2.27.2...google-cloud-monitoring-v2.28.0) ### [`v2.27.2`](https://redirect.github.com/googleapis/google-cloud-python/releases/tag/google-cloud-monitoring-v2.27.2): google-cloud-monitoring: v2.27.2 [Compare Source](https://redirect.github.com/googleapis/google-cloud-python/compare/google-cloud-monitoring-v2.27.1...google-cloud-monitoring-v2.27.2) ##### Documentation - Update import statement example in README ([868b006](https://redirect.github.com/googleapis/google-cloud-python/commit/868b0069baf1a4bf6705986e0b6885419b35cdcc))
googleapis/python-test-utils (google-cloud-testutils) ### [`v1.7.0`](https://redirect.github.com/googleapis/python-test-utils/blob/HEAD/CHANGELOG.md#170-2025-10-29) [Compare Source](https://redirect.github.com/googleapis/python-test-utils/compare/v1.6.4...v1.7.0) ##### Features - Add Python 3.14 support ([#​284](https://redirect.github.com/googleapis/python-test-utils/issues/284)) ([3cb8491](https://redirect.github.com/googleapis/python-test-utils/commit/3cb8491d67d65d2262aa1b65091ea9b615b583af))
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/googleapis/python-bigtable). --- samples/beam/requirements.txt | 6 +++--- samples/hello/requirements.txt | 4 ++-- samples/instanceadmin/requirements.txt | 2 +- samples/metricscaler/requirements.txt | 4 ++-- samples/quickstart/requirements.txt | 2 +- samples/snippets/data_client/requirements.txt | 2 +- samples/snippets/deletes/requirements.txt | 2 +- samples/snippets/filters/requirements.txt | 2 +- samples/snippets/reads/requirements.txt | 2 +- samples/snippets/writes/requirements.txt | 2 +- samples/tableadmin/requirements-test.txt | 2 +- samples/tableadmin/requirements.txt | 2 +- 12 files changed, 16 insertions(+), 16 deletions(-) diff --git a/samples/beam/requirements.txt b/samples/beam/requirements.txt index 4b84ddec3..bb207ddf4 100644 --- a/samples/beam/requirements.txt +++ b/samples/beam/requirements.txt @@ -1,4 +1,4 @@ apache-beam===2.60.0; python_version == '3.8' -apache-beam==2.65.0; python_version >= '3.9' -google-cloud-bigtable==2.30.1 -google-cloud-core==2.4.3 +apache-beam==2.69.0; python_version >= '3.9' +google-cloud-bigtable==2.34.0 +google-cloud-core==2.5.0 diff --git a/samples/hello/requirements.txt b/samples/hello/requirements.txt index 55d3a1ddd..ab4d1fc82 100644 --- a/samples/hello/requirements.txt +++ b/samples/hello/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-bigtable==2.30.1 -google-cloud-core==2.4.3 +google-cloud-bigtable==2.34.0 +google-cloud-core==2.5.0 diff --git a/samples/instanceadmin/requirements.txt b/samples/instanceadmin/requirements.txt index a2922fe6e..30d3bc28f 100644 --- a/samples/instanceadmin/requirements.txt +++ b/samples/instanceadmin/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-bigtable==2.30.1 +google-cloud-bigtable==2.34.0 backoff==2.2.1 diff --git a/samples/metricscaler/requirements.txt b/samples/metricscaler/requirements.txt index 522c28ae2..09af5060d 100644 --- a/samples/metricscaler/requirements.txt +++ b/samples/metricscaler/requirements.txt @@ -1,2 +1,2 @@ -google-cloud-bigtable==2.30.1 -google-cloud-monitoring==2.27.1 +google-cloud-bigtable==2.34.0 +google-cloud-monitoring==2.28.0 diff --git a/samples/quickstart/requirements.txt b/samples/quickstart/requirements.txt index 807132c7e..aea551f27 100644 --- a/samples/quickstart/requirements.txt +++ b/samples/quickstart/requirements.txt @@ -1 +1 @@ -google-cloud-bigtable==2.30.1 +google-cloud-bigtable==2.34.0 diff --git a/samples/snippets/data_client/requirements.txt b/samples/snippets/data_client/requirements.txt index 807132c7e..aea551f27 100644 --- a/samples/snippets/data_client/requirements.txt +++ b/samples/snippets/data_client/requirements.txt @@ -1 +1 @@ -google-cloud-bigtable==2.30.1 +google-cloud-bigtable==2.34.0 diff --git a/samples/snippets/deletes/requirements.txt b/samples/snippets/deletes/requirements.txt index 807132c7e..aea551f27 100644 --- a/samples/snippets/deletes/requirements.txt +++ b/samples/snippets/deletes/requirements.txt @@ -1 +1 @@ -google-cloud-bigtable==2.30.1 +google-cloud-bigtable==2.34.0 diff --git a/samples/snippets/filters/requirements.txt b/samples/snippets/filters/requirements.txt index 807132c7e..aea551f27 100644 --- a/samples/snippets/filters/requirements.txt +++ b/samples/snippets/filters/requirements.txt @@ -1 +1 @@ -google-cloud-bigtable==2.30.1 +google-cloud-bigtable==2.34.0 diff --git a/samples/snippets/reads/requirements.txt b/samples/snippets/reads/requirements.txt index 807132c7e..aea551f27 100644 --- a/samples/snippets/reads/requirements.txt +++ b/samples/snippets/reads/requirements.txt @@ -1 +1 @@ -google-cloud-bigtable==2.30.1 +google-cloud-bigtable==2.34.0 diff --git a/samples/snippets/writes/requirements.txt b/samples/snippets/writes/requirements.txt index 874788bf7..1ac867641 100644 --- a/samples/snippets/writes/requirements.txt +++ b/samples/snippets/writes/requirements.txt @@ -1 +1 @@ -google-cloud-bigtable==2.30.1 \ No newline at end of file +google-cloud-bigtable==2.34.0 \ No newline at end of file diff --git a/samples/tableadmin/requirements-test.txt b/samples/tableadmin/requirements-test.txt index d8889022d..f01fd134c 100644 --- a/samples/tableadmin/requirements-test.txt +++ b/samples/tableadmin/requirements-test.txt @@ -1,2 +1,2 @@ pytest -google-cloud-testutils==1.6.4 +google-cloud-testutils==1.7.0 diff --git a/samples/tableadmin/requirements.txt b/samples/tableadmin/requirements.txt index 807132c7e..aea551f27 100644 --- a/samples/tableadmin/requirements.txt +++ b/samples/tableadmin/requirements.txt @@ -1 +1 @@ -google-cloud-bigtable==2.30.1 +google-cloud-bigtable==2.34.0 From e3fd5d8668303db4ed35e9bf6be48b46954f9d67 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 4 Dec 2025 11:19:30 -0800 Subject: [PATCH 06/10] fix: retry cancelled errors (#1235) There's an internal race condition where when an rpc hits the timeout limit, it sometimes receives a DEADLINE_EXCEEDED, but sometimes receives a CANCELLED error. This PR marks CANCELLED as retryable, so this situation will always eventually reach a DEADLINE_EXCEEDED state. This will fix the flake currently seen in the conformance tests --- google/cloud/bigtable/data/_async/client.py | 2 ++ google/cloud/bigtable/data/_sync_autogen/client.py | 2 ++ tests/unit/data/_async/test_client.py | 2 +- tests/unit/data/_async/test_mutations_batcher.py | 1 + tests/unit/data/_sync_autogen/test_client.py | 2 +- tests/unit/data/_sync_autogen/test_mutations_batcher.py | 1 + 6 files changed, 8 insertions(+), 2 deletions(-) diff --git a/google/cloud/bigtable/data/_async/client.py b/google/cloud/bigtable/data/_async/client.py index 1c98f56ab..a2bf9a6d9 100644 --- a/google/cloud/bigtable/data/_async/client.py +++ b/google/cloud/bigtable/data/_async/client.py @@ -59,6 +59,7 @@ from google.api_core.exceptions import DeadlineExceeded from google.api_core.exceptions import ServiceUnavailable from google.api_core.exceptions import Aborted +from google.api_core.exceptions import Cancelled from google.protobuf.message import Message from google.protobuf.internal.enum_type_wrapper import EnumTypeWrapper @@ -938,6 +939,7 @@ def __init__( DeadlineExceeded, ServiceUnavailable, Aborted, + Cancelled, ), default_mutate_rows_retryable_errors: Sequence[type[Exception]] = ( DeadlineExceeded, diff --git a/google/cloud/bigtable/data/_sync_autogen/client.py b/google/cloud/bigtable/data/_sync_autogen/client.py index a403643f5..a05d4517f 100644 --- a/google/cloud/bigtable/data/_sync_autogen/client.py +++ b/google/cloud/bigtable/data/_sync_autogen/client.py @@ -49,6 +49,7 @@ from google.api_core.exceptions import DeadlineExceeded from google.api_core.exceptions import ServiceUnavailable from google.api_core.exceptions import Aborted +from google.api_core.exceptions import Cancelled from google.protobuf.message import Message from google.protobuf.internal.enum_type_wrapper import EnumTypeWrapper import google.auth.credentials @@ -729,6 +730,7 @@ def __init__( DeadlineExceeded, ServiceUnavailable, Aborted, + Cancelled, ), default_mutate_rows_retryable_errors: Sequence[type[Exception]] = ( DeadlineExceeded, diff --git a/tests/unit/data/_async/test_client.py b/tests/unit/data/_async/test_client.py index a5ec1d02d..72b3ae738 100644 --- a/tests/unit/data/_async/test_client.py +++ b/tests/unit/data/_async/test_client.py @@ -1334,6 +1334,7 @@ def test_table_ctor_sync(self): core_exceptions.DeadlineExceeded, core_exceptions.ServiceUnavailable, core_exceptions.Aborted, + core_exceptions.Cancelled, ], ), ( @@ -1832,7 +1833,6 @@ async def test_read_rows_retryable_error(self, exc_type): @pytest.mark.parametrize( "exc_type", [ - core_exceptions.Cancelled, core_exceptions.PreconditionFailed, core_exceptions.NotFound, core_exceptions.PermissionDenied, diff --git a/tests/unit/data/_async/test_mutations_batcher.py b/tests/unit/data/_async/test_mutations_batcher.py index 29f2f1026..b139f31f1 100644 --- a/tests/unit/data/_async/test_mutations_batcher.py +++ b/tests/unit/data/_async/test_mutations_batcher.py @@ -1169,6 +1169,7 @@ def test__add_exceptions(self, limit, in_e, start_e, end_e): core_exceptions.DeadlineExceeded, core_exceptions.ServiceUnavailable, core_exceptions.Aborted, + core_exceptions.Cancelled, ], ), ( diff --git a/tests/unit/data/_sync_autogen/test_client.py b/tests/unit/data/_sync_autogen/test_client.py index 6ad6c1063..49ed41ad6 100644 --- a/tests/unit/data/_sync_autogen/test_client.py +++ b/tests/unit/data/_sync_autogen/test_client.py @@ -1063,6 +1063,7 @@ def test_ctor_invalid_timeout_values(self): core_exceptions.DeadlineExceeded, core_exceptions.ServiceUnavailable, core_exceptions.Aborted, + core_exceptions.Cancelled, ], ), ( @@ -1507,7 +1508,6 @@ def test_read_rows_retryable_error(self, exc_type): @pytest.mark.parametrize( "exc_type", [ - core_exceptions.Cancelled, core_exceptions.PreconditionFailed, core_exceptions.NotFound, core_exceptions.PermissionDenied, diff --git a/tests/unit/data/_sync_autogen/test_mutations_batcher.py b/tests/unit/data/_sync_autogen/test_mutations_batcher.py index 72db64146..92d16b349 100644 --- a/tests/unit/data/_sync_autogen/test_mutations_batcher.py +++ b/tests/unit/data/_sync_autogen/test_mutations_batcher.py @@ -1021,6 +1021,7 @@ def test__add_exceptions(self, limit, in_e, start_e, end_e): core_exceptions.DeadlineExceeded, core_exceptions.ServiceUnavailable, core_exceptions.Aborted, + core_exceptions.Cancelled, ], ), ( From 2a5baf11d30dc383a7b48d5f43b6cbb6160782e3 Mon Sep 17 00:00:00 2001 From: Akshay Joshi Date: Thu, 11 Dec 2025 12:11:46 -0500 Subject: [PATCH 07/10] fix: re-export AddToCell for consistency (#1241) Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [x] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/python-bigtable/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [x] Ensure the tests and linter pass - [x] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) Fixes #1239 --- google/cloud/bigtable/data/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/google/cloud/bigtable/data/__init__.py b/google/cloud/bigtable/data/__init__.py index 9439f0f8d..c18eae683 100644 --- a/google/cloud/bigtable/data/__init__.py +++ b/google/cloud/bigtable/data/__init__.py @@ -31,6 +31,7 @@ from google.cloud.bigtable.data.mutations import Mutation from google.cloud.bigtable.data.mutations import RowMutationEntry +from google.cloud.bigtable.data.mutations import AddToCell from google.cloud.bigtable.data.mutations import SetCell from google.cloud.bigtable.data.mutations import DeleteRangeFromColumn from google.cloud.bigtable.data.mutations import DeleteAllFromFamily @@ -89,6 +90,7 @@ "RowRange", "Mutation", "RowMutationEntry", + "AddToCell", "SetCell", "DeleteRangeFromColumn", "DeleteAllFromFamily", From 544db1cd7af876298b8637f495b6c7b2a0bcf16c Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 16 Dec 2025 10:29:09 -0800 Subject: [PATCH 08/10] fix: async client uses fixed grace period (#1236) Previously, when a channel refresh occurs, the async client would use `channel.close()` with a grace parameter to allow previous channels to keep serving old requests for a time. We were seeing flakes in our tests, showing that `channel.close()` isn't reliable, and can sometimes cancel ongoing requests before the grace period ends This PR fixes this by using a fixed sleep time before calling close in the async client, like the sync client already does. This should remove the potential for cancelled requests before the grace period ends, and improve test flakiness I also updated the system test to fully capture this problematic state, instead of encountering it in a random race condition --- google/cloud/bigtable/data/_async/client.py | 11 ++- .../bigtable/data/_sync_autogen/client.py | 4 +- tests/system/data/test_system_async.py | 74 +++++++++---------- tests/system/data/test_system_autogen.py | 47 ++++++------ 4 files changed, 67 insertions(+), 69 deletions(-) diff --git a/google/cloud/bigtable/data/_async/client.py b/google/cloud/bigtable/data/_async/client.py index a2bf9a6d9..54a410361 100644 --- a/google/cloud/bigtable/data/_async/client.py +++ b/google/cloud/bigtable/data/_async/client.py @@ -481,12 +481,11 @@ async def _manage_channel( old_channel = super_channel.swap_channel(new_channel) self._invalidate_channel_stubs() # give old_channel a chance to complete existing rpcs - if CrossSync.is_async: - await old_channel.close(grace_period) - else: - if grace_period: - self._is_closed.wait(grace_period) # type: ignore - old_channel.close() # type: ignore + if grace_period: + await CrossSync.event_wait( + self._is_closed, grace_period, async_break_early=False + ) + await old_channel.close() # subtract the time spent waiting for the channel to be replaced next_refresh = random.uniform(refresh_interval_min, refresh_interval_max) next_sleep = max(next_refresh - (time.monotonic() - start_timestamp), 0) diff --git a/google/cloud/bigtable/data/_sync_autogen/client.py b/google/cloud/bigtable/data/_sync_autogen/client.py index a05d4517f..6a4da007a 100644 --- a/google/cloud/bigtable/data/_sync_autogen/client.py +++ b/google/cloud/bigtable/data/_sync_autogen/client.py @@ -365,7 +365,9 @@ def _manage_channel( old_channel = super_channel.swap_channel(new_channel) self._invalidate_channel_stubs() if grace_period: - self._is_closed.wait(grace_period) + CrossSync._Sync_Impl.event_wait( + self._is_closed, grace_period, async_break_early=False + ) old_channel.close() next_refresh = random.uniform(refresh_interval_min, refresh_interval_max) next_sleep = max(next_refresh - (time.monotonic() - start_timestamp), 0) diff --git a/tests/system/data/test_system_async.py b/tests/system/data/test_system_async.py index 39c454996..ac8a358a3 100644 --- a/tests/system/data/test_system_async.py +++ b/tests/system/data/test_system_async.py @@ -266,49 +266,49 @@ async def test_ping_and_warm(self, client, target): @CrossSync.pytest async def test_channel_refresh(self, table_id, instance_id, temp_rows): """ - change grpc channel to refresh after 1 second. Schedule a read_rows call after refresh, - to ensure new channel works + perform requests while swapping out the grpc channel. Requests should continue without error """ - await temp_rows.add_row(b"row_key_1") - await temp_rows.add_row(b"row_key_2") - client = self._make_client() - # start custom refresh task - try: + import time + + await temp_rows.add_row(b"test_row") + async with self._make_client() as client: + client._channel_refresh_task.cancel() + channel_wrapper = client.transport.grpc_channel + first_channel = channel_wrapper._channel + # swap channels frequently, with large grace windows client._channel_refresh_task = CrossSync.create_task( client._manage_channel, - refresh_interval_min=1, - refresh_interval_max=1, + refresh_interval_min=0.1, + refresh_interval_max=0.1, + grace_period=1, sync_executor=client._executor, ) - # let task run - await CrossSync.yield_to_event_loop() + + # hit channels with frequent requests + end_time = time.monotonic() + 3 async with client.get_table(instance_id, table_id) as table: - rows = await table.read_rows({}) - channel_wrapper = client.transport.grpc_channel - first_channel = channel_wrapper._channel - assert len(rows) == 2 - await CrossSync.sleep(2) - rows_after_refresh = await table.read_rows({}) - assert len(rows_after_refresh) == 2 - assert client.transport.grpc_channel is channel_wrapper - updated_channel = channel_wrapper._channel - assert updated_channel is not first_channel - # ensure interceptors are kept (gapic's logging interceptor, and metric interceptor) - if CrossSync.is_async: - unary_interceptors = updated_channel._unary_unary_interceptors - assert len(unary_interceptors) == 2 - assert GapicInterceptor in [type(i) for i in unary_interceptors] - assert client._metrics_interceptor in unary_interceptors - stream_interceptors = updated_channel._unary_stream_interceptors - assert len(stream_interceptors) == 1 - assert client._metrics_interceptor in stream_interceptors - else: - assert isinstance( - client.transport._logged_channel._interceptor, GapicInterceptor - ) - assert updated_channel._interceptor == client._metrics_interceptor - finally: - await client.close() + while time.monotonic() < end_time: + # we expect a CancelledError if a channel is closed before completion + rows = await table.read_rows({}) + assert len(rows) == 1 + await CrossSync.yield_to_event_loop() + # ensure channel was updated + updated_channel = channel_wrapper._channel + assert updated_channel is not first_channel + # ensure interceptors are kept (gapic's logging interceptor, and metric interceptor) + if CrossSync.is_async: + unary_interceptors = updated_channel._unary_unary_interceptors + assert len(unary_interceptors) == 2 + assert GapicInterceptor in [type(i) for i in unary_interceptors] + assert client._metrics_interceptor in unary_interceptors + stream_interceptors = updated_channel._unary_stream_interceptors + assert len(stream_interceptors) == 1 + assert client._metrics_interceptor in stream_interceptors + else: + assert isinstance( + client.transport._logged_channel._interceptor, GapicInterceptor + ) + assert updated_channel._interceptor == client._metrics_interceptor @CrossSync.pytest @pytest.mark.usefixtures("target") diff --git a/tests/system/data/test_system_autogen.py b/tests/system/data/test_system_autogen.py index 37c00f2ae..463235087 100644 --- a/tests/system/data/test_system_autogen.py +++ b/tests/system/data/test_system_autogen.py @@ -221,36 +221,33 @@ def test_ping_and_warm(self, client, target): reason="emulator mode doesn't refresh channel", ) def test_channel_refresh(self, table_id, instance_id, temp_rows): - """change grpc channel to refresh after 1 second. Schedule a read_rows call after refresh, - to ensure new channel works""" - temp_rows.add_row(b"row_key_1") - temp_rows.add_row(b"row_key_2") - client = self._make_client() - try: + """perform requests while swapping out the grpc channel. Requests should continue without error""" + import time + + temp_rows.add_row(b"test_row") + with self._make_client() as client: + client._channel_refresh_task.cancel() + channel_wrapper = client.transport.grpc_channel + first_channel = channel_wrapper._channel client._channel_refresh_task = CrossSync._Sync_Impl.create_task( client._manage_channel, - refresh_interval_min=1, - refresh_interval_max=1, + refresh_interval_min=0.1, + refresh_interval_max=0.1, + grace_period=1, sync_executor=client._executor, ) - CrossSync._Sync_Impl.yield_to_event_loop() + end_time = time.monotonic() + 3 with client.get_table(instance_id, table_id) as table: - rows = table.read_rows({}) - channel_wrapper = client.transport.grpc_channel - first_channel = channel_wrapper._channel - assert len(rows) == 2 - CrossSync._Sync_Impl.sleep(2) - rows_after_refresh = table.read_rows({}) - assert len(rows_after_refresh) == 2 - assert client.transport.grpc_channel is channel_wrapper - updated_channel = channel_wrapper._channel - assert updated_channel is not first_channel - assert isinstance( - client.transport._logged_channel._interceptor, GapicInterceptor - ) - assert updated_channel._interceptor == client._metrics_interceptor - finally: - client.close() + while time.monotonic() < end_time: + rows = table.read_rows({}) + assert len(rows) == 1 + CrossSync._Sync_Impl.yield_to_event_loop() + updated_channel = channel_wrapper._channel + assert updated_channel is not first_channel + assert isinstance( + client.transport._logged_channel._interceptor, GapicInterceptor + ) + assert updated_channel._interceptor == client._metrics_interceptor @pytest.mark.usefixtures("target") @CrossSync._Sync_Impl.Retry( From ca20219cf45305de25dfb715f69dd63bce9981b7 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 16 Dec 2025 13:02:20 -0800 Subject: [PATCH 09/10] feat(gapic): support mTLS certificates when available (#1249) feat: update image to us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:b8058df4c45e9a6e07f6b4d65b458d0d059241dd34c814f151c8bf6b89211209 --- .librarian/generator-input/setup.py | 4 - .librarian/state.yaml | 2 +- google/cloud/bigtable_admin_v2/__init__.py | 104 +++++++ .../bigtable_instance_admin/client.py | 46 +++- .../services/bigtable_table_admin/client.py | 46 +++- google/cloud/bigtable_v2/__init__.py | 104 +++++++ .../bigtable_v2/services/bigtable/client.py | 46 +++- noxfile.py | 4 + scripts/fixup_bigtable_admin_v2_keywords.py | 238 ---------------- scripts/fixup_bigtable_v2_keywords.py | 186 ------------- setup.py | 8 +- .../test_bigtable_instance_admin.py | 255 +++++++++++++++-- .../test_bigtable_table_admin.py | 257 ++++++++++++++++-- tests/unit/gapic/bigtable_v2/test_bigtable.py | 255 +++++++++++++++-- 14 files changed, 993 insertions(+), 562 deletions(-) delete mode 100644 scripts/fixup_bigtable_admin_v2_keywords.py delete mode 100644 scripts/fixup_bigtable_v2_keywords.py diff --git a/.librarian/generator-input/setup.py b/.librarian/generator-input/setup.py index cac533db6..fd8062970 100644 --- a/.librarian/generator-input/setup.py +++ b/.librarian/generator-input/setup.py @@ -94,10 +94,6 @@ packages=packages, install_requires=dependencies, extras_require=extras, - scripts=[ - "scripts/fixup_bigtable_v2_keywords.py", - "scripts/fixup_admin_v2_keywords.py", - ], python_requires=">=3.7", include_package_data=True, zip_safe=False, diff --git a/.librarian/state.yaml b/.librarian/state.yaml index 049e7b1cf..56a3154ff 100644 --- a/.librarian/state.yaml +++ b/.librarian/state.yaml @@ -1,4 +1,4 @@ -image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:8e2c32496077054105bd06c54a59d6a6694287bc053588e24debe6da6920ad91 +image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:b8058df4c45e9a6e07f6b4d65b458d0d059241dd34c814f151c8bf6b89211209 libraries: - id: google-cloud-bigtable version: 2.34.0 diff --git a/google/cloud/bigtable_admin_v2/__init__.py b/google/cloud/bigtable_admin_v2/__init__.py index 713b2408f..2102867f7 100644 --- a/google/cloud/bigtable_admin_v2/__init__.py +++ b/google/cloud/bigtable_admin_v2/__init__.py @@ -15,8 +15,18 @@ # from google.cloud.bigtable_admin_v2 import gapic_version as package_version +import google.api_core as api_core +import sys + __version__ = package_version.__version__ +if sys.version_info >= (3, 8): # pragma: NO COVER + from importlib import metadata +else: # pragma: NO COVER + # TODO(https://github.com/googleapis/python-api-core/issues/835): Remove + # this code path once we drop support for Python 3.7 + import importlib_metadata as metadata + from .services.bigtable_instance_admin import BigtableInstanceAdminClient from .services.bigtable_instance_admin import BigtableInstanceAdminAsyncClient @@ -143,6 +153,100 @@ from .types.table import RestoreSourceType from .types.types import Type +if hasattr(api_core, "check_python_version") and hasattr( + api_core, "check_dependency_versions" +): # pragma: NO COVER + api_core.check_python_version("google.cloud.bigtable_admin_v2") # type: ignore + api_core.check_dependency_versions("google.cloud.bigtable_admin_v2") # type: ignore +else: # pragma: NO COVER + # An older version of api_core is installed which does not define the + # functions above. We do equivalent checks manually. + try: + import warnings + import sys + + _py_version_str = sys.version.split()[0] + _package_label = "google.cloud.bigtable_admin_v2" + if sys.version_info < (3, 9): + warnings.warn( + "You are using a non-supported Python version " + + f"({_py_version_str}). Google will not post any further " + + f"updates to {_package_label} supporting this Python version. " + + "Please upgrade to the latest Python version, or at " + + f"least to Python 3.9, and then update {_package_label}.", + FutureWarning, + ) + if sys.version_info[:2] == (3, 9): + warnings.warn( + f"You are using a Python version ({_py_version_str}) " + + f"which Google will stop supporting in {_package_label} in " + + "January 2026. Please " + + "upgrade to the latest Python version, or at " + + "least to Python 3.10, before then, and " + + f"then update {_package_label}.", + FutureWarning, + ) + + def parse_version_to_tuple(version_string: str): + """Safely converts a semantic version string to a comparable tuple of integers. + Example: "4.25.8" -> (4, 25, 8) + Ignores non-numeric parts and handles common version formats. + Args: + version_string: Version string in the format "x.y.z" or "x.y.z" + Returns: + Tuple of integers for the parsed version string. + """ + parts = [] + for part in version_string.split("."): + try: + parts.append(int(part)) + except ValueError: + # If it's a non-numeric part (e.g., '1.0.0b1' -> 'b1'), stop here. + # This is a simplification compared to 'packaging.parse_version', but sufficient + # for comparing strictly numeric semantic versions. + break + return tuple(parts) + + def _get_version(dependency_name): + try: + version_string: str = metadata.version(dependency_name) + parsed_version = parse_version_to_tuple(version_string) + return (parsed_version, version_string) + except Exception: + # Catch exceptions from metadata.version() (e.g., PackageNotFoundError) + # or errors during parse_version_to_tuple + return (None, "--") + + _dependency_package = "google.protobuf" + _next_supported_version = "4.25.8" + _next_supported_version_tuple = (4, 25, 8) + _recommendation = " (we recommend 6.x)" + (_version_used, _version_used_string) = _get_version(_dependency_package) + if _version_used and _version_used < _next_supported_version_tuple: + warnings.warn( + f"Package {_package_label} depends on " + + f"{_dependency_package}, currently installed at version " + + f"{_version_used_string}. Future updates to " + + f"{_package_label} will require {_dependency_package} at " + + f"version {_next_supported_version} or higher{_recommendation}." + + " Please ensure " + + "that either (a) your Python environment doesn't pin the " + + f"version of {_dependency_package}, so that updates to " + + f"{_package_label} can require the higher version, or " + + "(b) you manually update your Python environment to use at " + + f"least version {_next_supported_version} of " + + f"{_dependency_package}.", + FutureWarning, + ) + except Exception: + warnings.warn( + "Could not determine the version of Python " + + "currently being used. To continue receiving " + + "updates for {_package_label}, ensure you are " + + "using a supported version of Python; see " + + "https://devguide.python.org/versions/" + ) + __all__ = ( "BaseBigtableTableAdminAsyncClient", "BigtableInstanceAdminAsyncClient", diff --git a/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/client.py b/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/client.py index 7c72be997..9d64108bb 100644 --- a/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/client.py +++ b/google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/client.py @@ -161,6 +161,34 @@ def _get_default_mtls_endpoint(api_endpoint): _DEFAULT_ENDPOINT_TEMPLATE = "bigtableadmin.{UNIVERSE_DOMAIN}" _DEFAULT_UNIVERSE = "googleapis.com" + @staticmethod + def _use_client_cert_effective(): + """Returns whether client certificate should be used for mTLS if the + google-auth version supports should_use_client_cert automatic mTLS enablement. + + Alternatively, read from the GOOGLE_API_USE_CLIENT_CERTIFICATE env var. + + Returns: + bool: whether client certificate should be used for mTLS + Raises: + ValueError: (If using a version of google-auth without should_use_client_cert and + GOOGLE_API_USE_CLIENT_CERTIFICATE is set to an unexpected value.) + """ + # check if google-auth version supports should_use_client_cert for automatic mTLS enablement + if hasattr(mtls, "should_use_client_cert"): # pragma: NO COVER + return mtls.should_use_client_cert() + else: # pragma: NO COVER + # if unsupported, fallback to reading from env var + use_client_cert_str = os.getenv( + "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" + ).lower() + if use_client_cert_str not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be" + " either `true` or `false`" + ) + return use_client_cert_str == "true" + @classmethod def from_service_account_info(cls, info: dict, *args, **kwargs): """Creates an instance of this client using the provided credentials @@ -503,12 +531,8 @@ def get_mtls_endpoint_and_cert_source( ) if client_options is None: client_options = client_options_lib.ClientOptions() - use_client_cert = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") + use_client_cert = BigtableInstanceAdminClient._use_client_cert_effective() use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") - if use_client_cert not in ("true", "false"): - raise ValueError( - "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) if use_mtls_endpoint not in ("auto", "never", "always"): raise MutualTLSChannelError( "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" @@ -516,7 +540,7 @@ def get_mtls_endpoint_and_cert_source( # Figure out the client cert source to use. client_cert_source = None - if use_client_cert == "true": + if use_client_cert: if client_options.client_cert_source: client_cert_source = client_options.client_cert_source elif mtls.has_default_client_cert_source(): @@ -548,20 +572,14 @@ def _read_environment_variables(): google.auth.exceptions.MutualTLSChannelError: If GOOGLE_API_USE_MTLS_ENDPOINT is not any of ["auto", "never", "always"]. """ - use_client_cert = os.getenv( - "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" - ).lower() + use_client_cert = BigtableInstanceAdminClient._use_client_cert_effective() use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower() universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") - if use_client_cert not in ("true", "false"): - raise ValueError( - "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) if use_mtls_endpoint not in ("auto", "never", "always"): raise MutualTLSChannelError( "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" ) - return use_client_cert == "true", use_mtls_endpoint, universe_domain_env + return use_client_cert, use_mtls_endpoint, universe_domain_env @staticmethod def _get_client_cert_source(provided_cert_source, use_cert_flag): diff --git a/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/client.py b/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/client.py index 4c6aff187..ce251db7d 100644 --- a/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/client.py +++ b/google/cloud/bigtable_admin_v2/services/bigtable_table_admin/client.py @@ -162,6 +162,34 @@ def _get_default_mtls_endpoint(api_endpoint): _DEFAULT_ENDPOINT_TEMPLATE = "bigtableadmin.{UNIVERSE_DOMAIN}" _DEFAULT_UNIVERSE = "googleapis.com" + @staticmethod + def _use_client_cert_effective(): + """Returns whether client certificate should be used for mTLS if the + google-auth version supports should_use_client_cert automatic mTLS enablement. + + Alternatively, read from the GOOGLE_API_USE_CLIENT_CERTIFICATE env var. + + Returns: + bool: whether client certificate should be used for mTLS + Raises: + ValueError: (If using a version of google-auth without should_use_client_cert and + GOOGLE_API_USE_CLIENT_CERTIFICATE is set to an unexpected value.) + """ + # check if google-auth version supports should_use_client_cert for automatic mTLS enablement + if hasattr(mtls, "should_use_client_cert"): # pragma: NO COVER + return mtls.should_use_client_cert() + else: # pragma: NO COVER + # if unsupported, fallback to reading from env var + use_client_cert_str = os.getenv( + "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" + ).lower() + if use_client_cert_str not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be" + " either `true` or `false`" + ) + return use_client_cert_str == "true" + @classmethod def from_service_account_info(cls, info: dict, *args, **kwargs): """Creates an instance of this client using the provided credentials @@ -510,12 +538,8 @@ def get_mtls_endpoint_and_cert_source( ) if client_options is None: client_options = client_options_lib.ClientOptions() - use_client_cert = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") + use_client_cert = BaseBigtableTableAdminClient._use_client_cert_effective() use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") - if use_client_cert not in ("true", "false"): - raise ValueError( - "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) if use_mtls_endpoint not in ("auto", "never", "always"): raise MutualTLSChannelError( "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" @@ -523,7 +547,7 @@ def get_mtls_endpoint_and_cert_source( # Figure out the client cert source to use. client_cert_source = None - if use_client_cert == "true": + if use_client_cert: if client_options.client_cert_source: client_cert_source = client_options.client_cert_source elif mtls.has_default_client_cert_source(): @@ -555,20 +579,14 @@ def _read_environment_variables(): google.auth.exceptions.MutualTLSChannelError: If GOOGLE_API_USE_MTLS_ENDPOINT is not any of ["auto", "never", "always"]. """ - use_client_cert = os.getenv( - "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" - ).lower() + use_client_cert = BaseBigtableTableAdminClient._use_client_cert_effective() use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower() universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") - if use_client_cert not in ("true", "false"): - raise ValueError( - "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) if use_mtls_endpoint not in ("auto", "never", "always"): raise MutualTLSChannelError( "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" ) - return use_client_cert == "true", use_mtls_endpoint, universe_domain_env + return use_client_cert, use_mtls_endpoint, universe_domain_env @staticmethod def _get_client_cert_source(provided_cert_source, use_cert_flag): diff --git a/google/cloud/bigtable_v2/__init__.py b/google/cloud/bigtable_v2/__init__.py index a14cdab5f..ec552a85d 100644 --- a/google/cloud/bigtable_v2/__init__.py +++ b/google/cloud/bigtable_v2/__init__.py @@ -15,8 +15,18 @@ # from google.cloud.bigtable_v2 import gapic_version as package_version +import google.api_core as api_core +import sys + __version__ = package_version.__version__ +if sys.version_info >= (3, 8): # pragma: NO COVER + from importlib import metadata +else: # pragma: NO COVER + # TODO(https://github.com/googleapis/python-api-core/issues/835): Remove + # this code path once we drop support for Python 3.7 + import importlib_metadata as metadata + from .services.bigtable import BigtableClient from .services.bigtable import BigtableAsyncClient @@ -78,6 +88,100 @@ from .types.response_params import ResponseParams from .types.types import Type +if hasattr(api_core, "check_python_version") and hasattr( + api_core, "check_dependency_versions" +): # pragma: NO COVER + api_core.check_python_version("google.cloud.bigtable_v2") # type: ignore + api_core.check_dependency_versions("google.cloud.bigtable_v2") # type: ignore +else: # pragma: NO COVER + # An older version of api_core is installed which does not define the + # functions above. We do equivalent checks manually. + try: + import warnings + import sys + + _py_version_str = sys.version.split()[0] + _package_label = "google.cloud.bigtable_v2" + if sys.version_info < (3, 9): + warnings.warn( + "You are using a non-supported Python version " + + f"({_py_version_str}). Google will not post any further " + + f"updates to {_package_label} supporting this Python version. " + + "Please upgrade to the latest Python version, or at " + + f"least to Python 3.9, and then update {_package_label}.", + FutureWarning, + ) + if sys.version_info[:2] == (3, 9): + warnings.warn( + f"You are using a Python version ({_py_version_str}) " + + f"which Google will stop supporting in {_package_label} in " + + "January 2026. Please " + + "upgrade to the latest Python version, or at " + + "least to Python 3.10, before then, and " + + f"then update {_package_label}.", + FutureWarning, + ) + + def parse_version_to_tuple(version_string: str): + """Safely converts a semantic version string to a comparable tuple of integers. + Example: "4.25.8" -> (4, 25, 8) + Ignores non-numeric parts and handles common version formats. + Args: + version_string: Version string in the format "x.y.z" or "x.y.z" + Returns: + Tuple of integers for the parsed version string. + """ + parts = [] + for part in version_string.split("."): + try: + parts.append(int(part)) + except ValueError: + # If it's a non-numeric part (e.g., '1.0.0b1' -> 'b1'), stop here. + # This is a simplification compared to 'packaging.parse_version', but sufficient + # for comparing strictly numeric semantic versions. + break + return tuple(parts) + + def _get_version(dependency_name): + try: + version_string: str = metadata.version(dependency_name) + parsed_version = parse_version_to_tuple(version_string) + return (parsed_version, version_string) + except Exception: + # Catch exceptions from metadata.version() (e.g., PackageNotFoundError) + # or errors during parse_version_to_tuple + return (None, "--") + + _dependency_package = "google.protobuf" + _next_supported_version = "4.25.8" + _next_supported_version_tuple = (4, 25, 8) + _recommendation = " (we recommend 6.x)" + (_version_used, _version_used_string) = _get_version(_dependency_package) + if _version_used and _version_used < _next_supported_version_tuple: + warnings.warn( + f"Package {_package_label} depends on " + + f"{_dependency_package}, currently installed at version " + + f"{_version_used_string}. Future updates to " + + f"{_package_label} will require {_dependency_package} at " + + f"version {_next_supported_version} or higher{_recommendation}." + + " Please ensure " + + "that either (a) your Python environment doesn't pin the " + + f"version of {_dependency_package}, so that updates to " + + f"{_package_label} can require the higher version, or " + + "(b) you manually update your Python environment to use at " + + f"least version {_next_supported_version} of " + + f"{_dependency_package}.", + FutureWarning, + ) + except Exception: + warnings.warn( + "Could not determine the version of Python " + + "currently being used. To continue receiving " + + "updates for {_package_label}, ensure you are " + + "using a supported version of Python; see " + + "https://devguide.python.org/versions/" + ) + __all__ = ( "BigtableAsyncClient", "ArrayValue", diff --git a/google/cloud/bigtable_v2/services/bigtable/client.py b/google/cloud/bigtable_v2/services/bigtable/client.py index d8e85f54e..5eb6ba894 100644 --- a/google/cloud/bigtable_v2/services/bigtable/client.py +++ b/google/cloud/bigtable_v2/services/bigtable/client.py @@ -151,6 +151,34 @@ def _get_default_mtls_endpoint(api_endpoint): _DEFAULT_ENDPOINT_TEMPLATE = "bigtable.{UNIVERSE_DOMAIN}" _DEFAULT_UNIVERSE = "googleapis.com" + @staticmethod + def _use_client_cert_effective(): + """Returns whether client certificate should be used for mTLS if the + google-auth version supports should_use_client_cert automatic mTLS enablement. + + Alternatively, read from the GOOGLE_API_USE_CLIENT_CERTIFICATE env var. + + Returns: + bool: whether client certificate should be used for mTLS + Raises: + ValueError: (If using a version of google-auth without should_use_client_cert and + GOOGLE_API_USE_CLIENT_CERTIFICATE is set to an unexpected value.) + """ + # check if google-auth version supports should_use_client_cert for automatic mTLS enablement + if hasattr(mtls, "should_use_client_cert"): # pragma: NO COVER + return mtls.should_use_client_cert() + else: # pragma: NO COVER + # if unsupported, fallback to reading from env var + use_client_cert_str = os.getenv( + "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" + ).lower() + if use_client_cert_str not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be" + " either `true` or `false`" + ) + return use_client_cert_str == "true" + @classmethod def from_service_account_info(cls, info: dict, *args, **kwargs): """Creates an instance of this client using the provided credentials @@ -401,12 +429,8 @@ def get_mtls_endpoint_and_cert_source( ) if client_options is None: client_options = client_options_lib.ClientOptions() - use_client_cert = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") + use_client_cert = BigtableClient._use_client_cert_effective() use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") - if use_client_cert not in ("true", "false"): - raise ValueError( - "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) if use_mtls_endpoint not in ("auto", "never", "always"): raise MutualTLSChannelError( "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" @@ -414,7 +438,7 @@ def get_mtls_endpoint_and_cert_source( # Figure out the client cert source to use. client_cert_source = None - if use_client_cert == "true": + if use_client_cert: if client_options.client_cert_source: client_cert_source = client_options.client_cert_source elif mtls.has_default_client_cert_source(): @@ -446,20 +470,14 @@ def _read_environment_variables(): google.auth.exceptions.MutualTLSChannelError: If GOOGLE_API_USE_MTLS_ENDPOINT is not any of ["auto", "never", "always"]. """ - use_client_cert = os.getenv( - "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" - ).lower() + use_client_cert = BigtableClient._use_client_cert_effective() use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower() universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN") - if use_client_cert not in ("true", "false"): - raise ValueError( - "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) if use_mtls_endpoint not in ("auto", "never", "always"): raise MutualTLSChannelError( "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" ) - return use_client_cert == "true", use_mtls_endpoint, universe_domain_env + return use_client_cert, use_mtls_endpoint, universe_domain_env @staticmethod def _get_client_cert_source(provided_cert_source, use_cert_flag): diff --git a/noxfile.py b/noxfile.py index 16c8a6327..29de5901b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -14,6 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +# DO NOT EDIT THIS FILE OUTSIDE OF `.librarian/generator-input` +# The source of truth for this file is `.librarian/generator-input` + + # Generated by synthtool. DO NOT EDIT! from __future__ import absolute_import diff --git a/scripts/fixup_bigtable_admin_v2_keywords.py b/scripts/fixup_bigtable_admin_v2_keywords.py deleted file mode 100644 index 1fda05668..000000000 --- a/scripts/fixup_bigtable_admin_v2_keywords.py +++ /dev/null @@ -1,238 +0,0 @@ -#! /usr/bin/env python3 -# -*- coding: utf-8 -*- -# 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 argparse -import os -import libcst as cst -import pathlib -import sys -from typing import (Any, Callable, Dict, List, Sequence, Tuple) - - -def partition( - predicate: Callable[[Any], bool], - iterator: Sequence[Any] -) -> Tuple[List[Any], List[Any]]: - """A stable, out-of-place partition.""" - results = ([], []) - - for i in iterator: - results[int(predicate(i))].append(i) - - # Returns trueList, falseList - return results[1], results[0] - - -class bigtable_adminCallTransformer(cst.CSTTransformer): - CTRL_PARAMS: Tuple[str] = ('retry', 'timeout', 'metadata') - METHOD_TO_PARAMS: Dict[str, Tuple[str]] = { - 'check_consistency': ('name', 'consistency_token', 'standard_read_remote_writes', 'data_boost_read_local_writes', ), - 'copy_backup': ('parent', 'backup_id', 'source_backup', 'expire_time', ), - 'create_app_profile': ('parent', 'app_profile_id', 'app_profile', 'ignore_warnings', ), - 'create_authorized_view': ('parent', 'authorized_view_id', 'authorized_view', ), - 'create_backup': ('parent', 'backup_id', 'backup', ), - 'create_cluster': ('parent', 'cluster_id', 'cluster', ), - 'create_instance': ('parent', 'instance_id', 'instance', 'clusters', ), - 'create_logical_view': ('parent', 'logical_view_id', 'logical_view', ), - 'create_materialized_view': ('parent', 'materialized_view_id', 'materialized_view', ), - 'create_schema_bundle': ('parent', 'schema_bundle_id', 'schema_bundle', ), - 'create_table': ('parent', 'table_id', 'table', 'initial_splits', ), - 'create_table_from_snapshot': ('parent', 'table_id', 'source_snapshot', ), - 'delete_app_profile': ('name', 'ignore_warnings', ), - 'delete_authorized_view': ('name', 'etag', ), - 'delete_backup': ('name', ), - 'delete_cluster': ('name', ), - 'delete_instance': ('name', ), - 'delete_logical_view': ('name', 'etag', ), - 'delete_materialized_view': ('name', 'etag', ), - 'delete_schema_bundle': ('name', 'etag', ), - 'delete_snapshot': ('name', ), - 'delete_table': ('name', ), - 'drop_row_range': ('name', 'row_key_prefix', 'delete_all_data_from_table', ), - 'generate_consistency_token': ('name', ), - 'get_app_profile': ('name', ), - 'get_authorized_view': ('name', 'view', ), - 'get_backup': ('name', ), - 'get_cluster': ('name', ), - 'get_iam_policy': ('resource', 'options', ), - 'get_instance': ('name', ), - 'get_logical_view': ('name', ), - 'get_materialized_view': ('name', ), - 'get_schema_bundle': ('name', ), - 'get_snapshot': ('name', ), - 'get_table': ('name', 'view', ), - 'list_app_profiles': ('parent', 'page_size', 'page_token', ), - 'list_authorized_views': ('parent', 'page_size', 'page_token', 'view', ), - 'list_backups': ('parent', 'filter', 'order_by', 'page_size', 'page_token', ), - 'list_clusters': ('parent', 'page_token', ), - 'list_hot_tablets': ('parent', 'start_time', 'end_time', 'page_size', 'page_token', ), - 'list_instances': ('parent', 'page_token', ), - 'list_logical_views': ('parent', 'page_size', 'page_token', ), - 'list_materialized_views': ('parent', 'page_size', 'page_token', ), - 'list_schema_bundles': ('parent', 'page_size', 'page_token', ), - 'list_snapshots': ('parent', 'page_size', 'page_token', ), - 'list_tables': ('parent', 'view', 'page_size', 'page_token', ), - 'modify_column_families': ('name', 'modifications', 'ignore_warnings', ), - 'partial_update_cluster': ('cluster', 'update_mask', ), - 'partial_update_instance': ('instance', 'update_mask', ), - 'restore_table': ('parent', 'table_id', 'backup', ), - 'set_iam_policy': ('resource', 'policy', 'update_mask', ), - 'snapshot_table': ('name', 'cluster', 'snapshot_id', 'ttl', 'description', ), - 'test_iam_permissions': ('resource', 'permissions', ), - 'undelete_table': ('name', ), - 'update_app_profile': ('app_profile', 'update_mask', 'ignore_warnings', ), - 'update_authorized_view': ('authorized_view', 'update_mask', 'ignore_warnings', ), - 'update_backup': ('backup', 'update_mask', ), - 'update_cluster': ('name', 'location', 'state', 'serve_nodes', 'node_scaling_factor', 'cluster_config', 'default_storage_type', 'encryption_config', ), - 'update_instance': ('display_name', 'name', 'state', 'type_', 'labels', 'create_time', 'satisfies_pzs', 'satisfies_pzi', 'tags', ), - 'update_logical_view': ('logical_view', 'update_mask', ), - 'update_materialized_view': ('materialized_view', 'update_mask', ), - 'update_schema_bundle': ('schema_bundle', 'update_mask', 'ignore_warnings', ), - 'update_table': ('table', 'update_mask', 'ignore_warnings', ), - } - - def leave_Call(self, original: cst.Call, updated: cst.Call) -> cst.CSTNode: - try: - key = original.func.attr.value - kword_params = self.METHOD_TO_PARAMS[key] - except (AttributeError, KeyError): - # Either not a method from the API or too convoluted to be sure. - return updated - - # If the existing code is valid, keyword args come after positional args. - # Therefore, all positional args must map to the first parameters. - args, kwargs = partition(lambda a: not bool(a.keyword), updated.args) - if any(k.keyword.value == "request" for k in kwargs): - # We've already fixed this file, don't fix it again. - return updated - - kwargs, ctrl_kwargs = partition( - lambda a: a.keyword.value not in self.CTRL_PARAMS, - kwargs - ) - - args, ctrl_args = args[:len(kword_params)], args[len(kword_params):] - ctrl_kwargs.extend(cst.Arg(value=a.value, keyword=cst.Name(value=ctrl)) - for a, ctrl in zip(ctrl_args, self.CTRL_PARAMS)) - - request_arg = cst.Arg( - value=cst.Dict([ - cst.DictElement( - cst.SimpleString("'{}'".format(name)), -cst.Element(value=arg.value) - ) - # Note: the args + kwargs looks silly, but keep in mind that - # the control parameters had to be stripped out, and that - # those could have been passed positionally or by keyword. - for name, arg in zip(kword_params, args + kwargs)]), - keyword=cst.Name("request") - ) - - return updated.with_changes( - args=[request_arg] + ctrl_kwargs - ) - - -def fix_files( - in_dir: pathlib.Path, - out_dir: pathlib.Path, - *, - transformer=bigtable_adminCallTransformer(), -): - """Duplicate the input dir to the output dir, fixing file method calls. - - Preconditions: - * in_dir is a real directory - * out_dir is a real, empty directory - """ - pyfile_gen = ( - pathlib.Path(os.path.join(root, f)) - for root, _, files in os.walk(in_dir) - for f in files if os.path.splitext(f)[1] == ".py" - ) - - for fpath in pyfile_gen: - with open(fpath, 'r') as f: - src = f.read() - - # Parse the code and insert method call fixes. - tree = cst.parse_module(src) - updated = tree.visit(transformer) - - # Create the path and directory structure for the new file. - updated_path = out_dir.joinpath(fpath.relative_to(in_dir)) - updated_path.parent.mkdir(parents=True, exist_ok=True) - - # Generate the updated source file at the corresponding path. - with open(updated_path, 'w') as f: - f.write(updated.code) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description="""Fix up source that uses the bigtable_admin client library. - -The existing sources are NOT overwritten but are copied to output_dir with changes made. - -Note: This tool operates at a best-effort level at converting positional - parameters in client method calls to keyword based parameters. - Cases where it WILL FAIL include - A) * or ** expansion in a method call. - B) Calls via function or method alias (includes free function calls) - C) Indirect or dispatched calls (e.g. the method is looked up dynamically) - - These all constitute false negatives. The tool will also detect false - positives when an API method shares a name with another method. -""") - parser.add_argument( - '-d', - '--input-directory', - required=True, - dest='input_dir', - help='the input directory to walk for python files to fix up', - ) - parser.add_argument( - '-o', - '--output-directory', - required=True, - dest='output_dir', - help='the directory to output files fixed via un-flattening', - ) - args = parser.parse_args() - input_dir = pathlib.Path(args.input_dir) - output_dir = pathlib.Path(args.output_dir) - if not input_dir.is_dir(): - print( - f"input directory '{input_dir}' does not exist or is not a directory", - file=sys.stderr, - ) - sys.exit(-1) - - if not output_dir.is_dir(): - print( - f"output directory '{output_dir}' does not exist or is not a directory", - file=sys.stderr, - ) - sys.exit(-1) - - if os.listdir(output_dir): - print( - f"output directory '{output_dir}' is not empty", - file=sys.stderr, - ) - sys.exit(-1) - - fix_files(input_dir, output_dir) diff --git a/scripts/fixup_bigtable_v2_keywords.py b/scripts/fixup_bigtable_v2_keywords.py deleted file mode 100644 index e65ad39a4..000000000 --- a/scripts/fixup_bigtable_v2_keywords.py +++ /dev/null @@ -1,186 +0,0 @@ -#! /usr/bin/env python3 -# -*- coding: utf-8 -*- -# 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 argparse -import os -import libcst as cst -import pathlib -import sys -from typing import (Any, Callable, Dict, List, Sequence, Tuple) - - -def partition( - predicate: Callable[[Any], bool], - iterator: Sequence[Any] -) -> Tuple[List[Any], List[Any]]: - """A stable, out-of-place partition.""" - results = ([], []) - - for i in iterator: - results[int(predicate(i))].append(i) - - # Returns trueList, falseList - return results[1], results[0] - - -class bigtableCallTransformer(cst.CSTTransformer): - CTRL_PARAMS: Tuple[str] = ('retry', 'timeout', 'metadata') - METHOD_TO_PARAMS: Dict[str, Tuple[str]] = { - 'check_and_mutate_row': ('row_key', 'table_name', 'authorized_view_name', 'app_profile_id', 'predicate_filter', 'true_mutations', 'false_mutations', ), - 'execute_query': ('instance_name', 'query', 'params', 'app_profile_id', 'prepared_query', 'proto_format', 'resume_token', ), - 'generate_initial_change_stream_partitions': ('table_name', 'app_profile_id', ), - 'mutate_row': ('row_key', 'mutations', 'table_name', 'authorized_view_name', 'app_profile_id', 'idempotency', ), - 'mutate_rows': ('entries', 'table_name', 'authorized_view_name', 'app_profile_id', ), - 'ping_and_warm': ('name', 'app_profile_id', ), - 'prepare_query': ('instance_name', 'query', 'param_types', 'app_profile_id', 'proto_format', ), - 'read_change_stream': ('table_name', 'app_profile_id', 'partition', 'start_time', 'continuation_tokens', 'end_time', 'heartbeat_duration', ), - 'read_modify_write_row': ('row_key', 'rules', 'table_name', 'authorized_view_name', 'app_profile_id', ), - 'read_rows': ('table_name', 'authorized_view_name', 'materialized_view_name', 'app_profile_id', 'rows', 'filter', 'rows_limit', 'request_stats_view', 'reversed', ), - 'sample_row_keys': ('table_name', 'authorized_view_name', 'materialized_view_name', 'app_profile_id', ), - } - - def leave_Call(self, original: cst.Call, updated: cst.Call) -> cst.CSTNode: - try: - key = original.func.attr.value - kword_params = self.METHOD_TO_PARAMS[key] - except (AttributeError, KeyError): - # Either not a method from the API or too convoluted to be sure. - return updated - - # If the existing code is valid, keyword args come after positional args. - # Therefore, all positional args must map to the first parameters. - args, kwargs = partition(lambda a: not bool(a.keyword), updated.args) - if any(k.keyword.value == "request" for k in kwargs): - # We've already fixed this file, don't fix it again. - return updated - - kwargs, ctrl_kwargs = partition( - lambda a: a.keyword.value not in self.CTRL_PARAMS, - kwargs - ) - - args, ctrl_args = args[:len(kword_params)], args[len(kword_params):] - ctrl_kwargs.extend(cst.Arg(value=a.value, keyword=cst.Name(value=ctrl)) - for a, ctrl in zip(ctrl_args, self.CTRL_PARAMS)) - - request_arg = cst.Arg( - value=cst.Dict([ - cst.DictElement( - cst.SimpleString("'{}'".format(name)), -cst.Element(value=arg.value) - ) - # Note: the args + kwargs looks silly, but keep in mind that - # the control parameters had to be stripped out, and that - # those could have been passed positionally or by keyword. - for name, arg in zip(kword_params, args + kwargs)]), - keyword=cst.Name("request") - ) - - return updated.with_changes( - args=[request_arg] + ctrl_kwargs - ) - - -def fix_files( - in_dir: pathlib.Path, - out_dir: pathlib.Path, - *, - transformer=bigtableCallTransformer(), -): - """Duplicate the input dir to the output dir, fixing file method calls. - - Preconditions: - * in_dir is a real directory - * out_dir is a real, empty directory - """ - pyfile_gen = ( - pathlib.Path(os.path.join(root, f)) - for root, _, files in os.walk(in_dir) - for f in files if os.path.splitext(f)[1] == ".py" - ) - - for fpath in pyfile_gen: - with open(fpath, 'r') as f: - src = f.read() - - # Parse the code and insert method call fixes. - tree = cst.parse_module(src) - updated = tree.visit(transformer) - - # Create the path and directory structure for the new file. - updated_path = out_dir.joinpath(fpath.relative_to(in_dir)) - updated_path.parent.mkdir(parents=True, exist_ok=True) - - # Generate the updated source file at the corresponding path. - with open(updated_path, 'w') as f: - f.write(updated.code) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description="""Fix up source that uses the bigtable client library. - -The existing sources are NOT overwritten but are copied to output_dir with changes made. - -Note: This tool operates at a best-effort level at converting positional - parameters in client method calls to keyword based parameters. - Cases where it WILL FAIL include - A) * or ** expansion in a method call. - B) Calls via function or method alias (includes free function calls) - C) Indirect or dispatched calls (e.g. the method is looked up dynamically) - - These all constitute false negatives. The tool will also detect false - positives when an API method shares a name with another method. -""") - parser.add_argument( - '-d', - '--input-directory', - required=True, - dest='input_dir', - help='the input directory to walk for python files to fix up', - ) - parser.add_argument( - '-o', - '--output-directory', - required=True, - dest='output_dir', - help='the directory to output files fixed via un-flattening', - ) - args = parser.parse_args() - input_dir = pathlib.Path(args.input_dir) - output_dir = pathlib.Path(args.output_dir) - if not input_dir.is_dir(): - print( - f"input directory '{input_dir}' does not exist or is not a directory", - file=sys.stderr, - ) - sys.exit(-1) - - if not output_dir.is_dir(): - print( - f"output directory '{output_dir}' does not exist or is not a directory", - file=sys.stderr, - ) - sys.exit(-1) - - if os.listdir(output_dir): - print( - f"output directory '{output_dir}' is not empty", - file=sys.stderr, - ) - sys.exit(-1) - - fix_files(input_dir, output_dir) diff --git a/setup.py b/setup.py index cac533db6..c8f13c372 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +# DO NOT EDIT THIS FILE OUTSIDE OF `.librarian/generator-input` +# The source of truth for this file is `.librarian/generator-input` + + import io import os @@ -94,10 +98,6 @@ packages=packages, install_requires=dependencies, extras_require=extras, - scripts=[ - "scripts/fixup_bigtable_v2_keywords.py", - "scripts/fixup_admin_v2_keywords.py", - ], python_requires=">=3.7", include_package_data=True, zip_safe=False, diff --git a/tests/unit/gapic/bigtable_admin_v2/test_bigtable_instance_admin.py b/tests/unit/gapic/bigtable_admin_v2/test_bigtable_instance_admin.py index 166f27eb8..b0ba35f0c 100644 --- a/tests/unit/gapic/bigtable_admin_v2/test_bigtable_instance_admin.py +++ b/tests/unit/gapic/bigtable_admin_v2/test_bigtable_instance_admin.py @@ -182,12 +182,19 @@ def test__read_environment_variables(): with mock.patch.dict( os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} ): - with pytest.raises(ValueError) as excinfo: - BigtableInstanceAdminClient._read_environment_variables() - assert ( - str(excinfo.value) - == "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with pytest.raises(ValueError) as excinfo: + BigtableInstanceAdminClient._read_environment_variables() + assert ( + str(excinfo.value) + == "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" + ) + else: + assert BigtableInstanceAdminClient._read_environment_variables() == ( + False, + "auto", + None, + ) with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): assert BigtableInstanceAdminClient._read_environment_variables() == ( @@ -226,6 +233,105 @@ def test__read_environment_variables(): ) +def test_use_client_cert_effective(): + # Test case 1: Test when `should_use_client_cert` returns True. + # We mock the `should_use_client_cert` function to simulate a scenario where + # the google-auth library supports automatic mTLS and determines that a + # client certificate should be used. + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", return_value=True + ): + assert BigtableInstanceAdminClient._use_client_cert_effective() is True + + # Test case 2: Test when `should_use_client_cert` returns False. + # We mock the `should_use_client_cert` function to simulate a scenario where + # the google-auth library supports automatic mTLS and determines that a + # client certificate should NOT be used. + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", return_value=False + ): + assert BigtableInstanceAdminClient._use_client_cert_effective() is False + + # Test case 3: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "true". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + assert BigtableInstanceAdminClient._use_client_cert_effective() is True + + # Test case 4: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "false". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"} + ): + assert BigtableInstanceAdminClient._use_client_cert_effective() is False + + # Test case 5: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "True". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "True"}): + assert BigtableInstanceAdminClient._use_client_cert_effective() is True + + # Test case 6: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "False". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "False"} + ): + assert BigtableInstanceAdminClient._use_client_cert_effective() is False + + # Test case 7: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "TRUE". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "TRUE"}): + assert BigtableInstanceAdminClient._use_client_cert_effective() is True + + # Test case 8: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "FALSE". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "FALSE"} + ): + assert BigtableInstanceAdminClient._use_client_cert_effective() is False + + # Test case 9: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not set. + # In this case, the method should return False, which is the default value. + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict(os.environ, clear=True): + assert BigtableInstanceAdminClient._use_client_cert_effective() is False + + # Test case 10: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to an invalid value. + # The method should raise a ValueError as the environment variable must be either + # "true" or "false". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "unsupported"} + ): + with pytest.raises(ValueError): + BigtableInstanceAdminClient._use_client_cert_effective() + + # Test case 11: Test when `should_use_client_cert` is available and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to an invalid value. + # The method should return False as the environment variable is set to an invalid value. + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "unsupported"} + ): + assert BigtableInstanceAdminClient._use_client_cert_effective() is False + + # Test case 12: Test when `should_use_client_cert` is available and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is unset. Also, + # the GOOGLE_API_CONFIG environment variable is unset. + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": ""}): + with mock.patch.dict(os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": ""}): + assert BigtableInstanceAdminClient._use_client_cert_effective() is False + + def test__get_client_cert_source(): mock_provided_cert_source = mock.Mock() mock_default_cert_source = mock.Mock() @@ -615,17 +721,6 @@ def test_bigtable_instance_admin_client_client_options( == "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" ) - # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value. - with mock.patch.dict( - os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} - ): - with pytest.raises(ValueError) as excinfo: - client = client_class(transport=transport_name) - assert ( - str(excinfo.value) - == "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) - # Check the case quota_project_id is provided options = client_options.ClientOptions(quota_project_id="octopus") with mock.patch.object(transport_class, "__init__") as patched: @@ -861,6 +956,119 @@ def test_bigtable_instance_admin_client_get_mtls_endpoint_and_cert_source(client assert api_endpoint == mock_api_endpoint assert cert_source is None + # Test the case GOOGLE_API_USE_CLIENT_CERTIFICATE is "Unsupported". + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} + ): + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + mock_client_cert_source = mock.Mock() + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, + api_endpoint=mock_api_endpoint, + ) + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source( + options + ) + assert api_endpoint == mock_api_endpoint + assert cert_source is None + + # Test cases for mTLS enablement when GOOGLE_API_USE_CLIENT_CERTIFICATE is unset. + test_cases = [ + ( + # With workloads present in config, mTLS is enabled. + { + "version": 1, + "cert_configs": { + "workload": { + "cert_path": "path/to/cert/file", + "key_path": "path/to/key/file", + } + }, + }, + mock_client_cert_source, + ), + ( + # With workloads not present in config, mTLS is disabled. + { + "version": 1, + "cert_configs": {}, + }, + None, + ), + ] + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + for config_data, expected_cert_source in test_cases: + env = os.environ.copy() + env.pop("GOOGLE_API_USE_CLIENT_CERTIFICATE", None) + with mock.patch.dict(os.environ, env, clear=True): + config_filename = "mock_certificate_config.json" + config_file_content = json.dumps(config_data) + m = mock.mock_open(read_data=config_file_content) + with mock.patch("builtins.open", m): + with mock.patch.dict( + os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} + ): + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, + api_endpoint=mock_api_endpoint, + ) + ( + api_endpoint, + cert_source, + ) = client_class.get_mtls_endpoint_and_cert_source(options) + assert api_endpoint == mock_api_endpoint + assert cert_source is expected_cert_source + + # Test cases for mTLS enablement when GOOGLE_API_USE_CLIENT_CERTIFICATE is unset(empty). + test_cases = [ + ( + # With workloads present in config, mTLS is enabled. + { + "version": 1, + "cert_configs": { + "workload": { + "cert_path": "path/to/cert/file", + "key_path": "path/to/key/file", + } + }, + }, + mock_client_cert_source, + ), + ( + # With workloads not present in config, mTLS is disabled. + { + "version": 1, + "cert_configs": {}, + }, + None, + ), + ] + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + for config_data, expected_cert_source in test_cases: + env = os.environ.copy() + env.pop("GOOGLE_API_USE_CLIENT_CERTIFICATE", "") + with mock.patch.dict(os.environ, env, clear=True): + config_filename = "mock_certificate_config.json" + config_file_content = json.dumps(config_data) + m = mock.mock_open(read_data=config_file_content) + with mock.patch("builtins.open", m): + with mock.patch.dict( + os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} + ): + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, + api_endpoint=mock_api_endpoint, + ) + ( + api_endpoint, + cert_source, + ) = client_class.get_mtls_endpoint_and_cert_source(options) + assert api_endpoint == mock_api_endpoint + assert cert_source is expected_cert_source + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "never". with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() @@ -911,18 +1119,6 @@ def test_bigtable_instance_admin_client_get_mtls_endpoint_and_cert_source(client == "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" ) - # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value. - with mock.patch.dict( - os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} - ): - with pytest.raises(ValueError) as excinfo: - client_class.get_mtls_endpoint_and_cert_source() - - assert ( - str(excinfo.value) - == "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) - @pytest.mark.parametrize( "client_class", [BigtableInstanceAdminClient, BigtableInstanceAdminAsyncClient] @@ -25948,6 +26144,7 @@ def test_bigtable_instance_admin_grpc_asyncio_transport_channel(): # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are # removed from grpc/grpc_asyncio transport constructor. +@pytest.mark.filterwarnings("ignore::FutureWarning") @pytest.mark.parametrize( "transport_class", [ diff --git a/tests/unit/gapic/bigtable_admin_v2/test_bigtable_table_admin.py b/tests/unit/gapic/bigtable_admin_v2/test_bigtable_table_admin.py index 7cbe6f3b1..cb5e8dd52 100644 --- a/tests/unit/gapic/bigtable_admin_v2/test_bigtable_table_admin.py +++ b/tests/unit/gapic/bigtable_admin_v2/test_bigtable_table_admin.py @@ -185,12 +185,19 @@ def test__read_environment_variables(): with mock.patch.dict( os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} ): - with pytest.raises(ValueError) as excinfo: - BaseBigtableTableAdminClient._read_environment_variables() - assert ( - str(excinfo.value) - == "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with pytest.raises(ValueError) as excinfo: + BaseBigtableTableAdminClient._read_environment_variables() + assert ( + str(excinfo.value) + == "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" + ) + else: + assert BaseBigtableTableAdminClient._read_environment_variables() == ( + False, + "auto", + None, + ) with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): assert BaseBigtableTableAdminClient._read_environment_variables() == ( @@ -229,6 +236,107 @@ def test__read_environment_variables(): ) +def test_use_client_cert_effective(): + # Test case 1: Test when `should_use_client_cert` returns True. + # We mock the `should_use_client_cert` function to simulate a scenario where + # the google-auth library supports automatic mTLS and determines that a + # client certificate should be used. + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", return_value=True + ): + assert BaseBigtableTableAdminClient._use_client_cert_effective() is True + + # Test case 2: Test when `should_use_client_cert` returns False. + # We mock the `should_use_client_cert` function to simulate a scenario where + # the google-auth library supports automatic mTLS and determines that a + # client certificate should NOT be used. + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", return_value=False + ): + assert BaseBigtableTableAdminClient._use_client_cert_effective() is False + + # Test case 3: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "true". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + assert BaseBigtableTableAdminClient._use_client_cert_effective() is True + + # Test case 4: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "false". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"} + ): + assert BaseBigtableTableAdminClient._use_client_cert_effective() is False + + # Test case 5: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "True". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "True"}): + assert BaseBigtableTableAdminClient._use_client_cert_effective() is True + + # Test case 6: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "False". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "False"} + ): + assert BaseBigtableTableAdminClient._use_client_cert_effective() is False + + # Test case 7: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "TRUE". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "TRUE"}): + assert BaseBigtableTableAdminClient._use_client_cert_effective() is True + + # Test case 8: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "FALSE". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "FALSE"} + ): + assert BaseBigtableTableAdminClient._use_client_cert_effective() is False + + # Test case 9: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not set. + # In this case, the method should return False, which is the default value. + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict(os.environ, clear=True): + assert BaseBigtableTableAdminClient._use_client_cert_effective() is False + + # Test case 10: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to an invalid value. + # The method should raise a ValueError as the environment variable must be either + # "true" or "false". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "unsupported"} + ): + with pytest.raises(ValueError): + BaseBigtableTableAdminClient._use_client_cert_effective() + + # Test case 11: Test when `should_use_client_cert` is available and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to an invalid value. + # The method should return False as the environment variable is set to an invalid value. + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "unsupported"} + ): + assert BaseBigtableTableAdminClient._use_client_cert_effective() is False + + # Test case 12: Test when `should_use_client_cert` is available and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is unset. Also, + # the GOOGLE_API_CONFIG environment variable is unset. + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": ""}): + with mock.patch.dict(os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": ""}): + assert ( + BaseBigtableTableAdminClient._use_client_cert_effective() is False + ) + + def test__get_client_cert_source(): mock_provided_cert_source = mock.Mock() mock_default_cert_source = mock.Mock() @@ -618,17 +726,6 @@ def test_base_bigtable_table_admin_client_client_options( == "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" ) - # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value. - with mock.patch.dict( - os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} - ): - with pytest.raises(ValueError) as excinfo: - client = client_class(transport=transport_name) - assert ( - str(excinfo.value) - == "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) - # Check the case quota_project_id is provided options = client_options.ClientOptions(quota_project_id="octopus") with mock.patch.object(transport_class, "__init__") as patched: @@ -866,6 +963,119 @@ def test_base_bigtable_table_admin_client_get_mtls_endpoint_and_cert_source( assert api_endpoint == mock_api_endpoint assert cert_source is None + # Test the case GOOGLE_API_USE_CLIENT_CERTIFICATE is "Unsupported". + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} + ): + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + mock_client_cert_source = mock.Mock() + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, + api_endpoint=mock_api_endpoint, + ) + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source( + options + ) + assert api_endpoint == mock_api_endpoint + assert cert_source is None + + # Test cases for mTLS enablement when GOOGLE_API_USE_CLIENT_CERTIFICATE is unset. + test_cases = [ + ( + # With workloads present in config, mTLS is enabled. + { + "version": 1, + "cert_configs": { + "workload": { + "cert_path": "path/to/cert/file", + "key_path": "path/to/key/file", + } + }, + }, + mock_client_cert_source, + ), + ( + # With workloads not present in config, mTLS is disabled. + { + "version": 1, + "cert_configs": {}, + }, + None, + ), + ] + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + for config_data, expected_cert_source in test_cases: + env = os.environ.copy() + env.pop("GOOGLE_API_USE_CLIENT_CERTIFICATE", None) + with mock.patch.dict(os.environ, env, clear=True): + config_filename = "mock_certificate_config.json" + config_file_content = json.dumps(config_data) + m = mock.mock_open(read_data=config_file_content) + with mock.patch("builtins.open", m): + with mock.patch.dict( + os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} + ): + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, + api_endpoint=mock_api_endpoint, + ) + ( + api_endpoint, + cert_source, + ) = client_class.get_mtls_endpoint_and_cert_source(options) + assert api_endpoint == mock_api_endpoint + assert cert_source is expected_cert_source + + # Test cases for mTLS enablement when GOOGLE_API_USE_CLIENT_CERTIFICATE is unset(empty). + test_cases = [ + ( + # With workloads present in config, mTLS is enabled. + { + "version": 1, + "cert_configs": { + "workload": { + "cert_path": "path/to/cert/file", + "key_path": "path/to/key/file", + } + }, + }, + mock_client_cert_source, + ), + ( + # With workloads not present in config, mTLS is disabled. + { + "version": 1, + "cert_configs": {}, + }, + None, + ), + ] + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + for config_data, expected_cert_source in test_cases: + env = os.environ.copy() + env.pop("GOOGLE_API_USE_CLIENT_CERTIFICATE", "") + with mock.patch.dict(os.environ, env, clear=True): + config_filename = "mock_certificate_config.json" + config_file_content = json.dumps(config_data) + m = mock.mock_open(read_data=config_file_content) + with mock.patch("builtins.open", m): + with mock.patch.dict( + os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} + ): + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, + api_endpoint=mock_api_endpoint, + ) + ( + api_endpoint, + cert_source, + ) = client_class.get_mtls_endpoint_and_cert_source(options) + assert api_endpoint == mock_api_endpoint + assert cert_source is expected_cert_source + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "never". with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() @@ -916,18 +1126,6 @@ def test_base_bigtable_table_admin_client_get_mtls_endpoint_and_cert_source( == "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" ) - # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value. - with mock.patch.dict( - os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} - ): - with pytest.raises(ValueError) as excinfo: - client_class.get_mtls_endpoint_and_cert_source() - - assert ( - str(excinfo.value) - == "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) - @pytest.mark.parametrize( "client_class", [BaseBigtableTableAdminClient, BaseBigtableTableAdminAsyncClient] @@ -29050,6 +29248,7 @@ def test_bigtable_table_admin_grpc_asyncio_transport_channel(): # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are # removed from grpc/grpc_asyncio transport constructor. +@pytest.mark.filterwarnings("ignore::FutureWarning") @pytest.mark.parametrize( "transport_class", [ diff --git a/tests/unit/gapic/bigtable_v2/test_bigtable.py b/tests/unit/gapic/bigtable_v2/test_bigtable.py index 9922999d6..ea7f0955d 100644 --- a/tests/unit/gapic/bigtable_v2/test_bigtable.py +++ b/tests/unit/gapic/bigtable_v2/test_bigtable.py @@ -151,12 +151,19 @@ def test__read_environment_variables(): with mock.patch.dict( os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} ): - with pytest.raises(ValueError) as excinfo: - BigtableClient._read_environment_variables() - assert ( - str(excinfo.value) - == "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with pytest.raises(ValueError) as excinfo: + BigtableClient._read_environment_variables() + assert ( + str(excinfo.value) + == "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" + ) + else: + assert BigtableClient._read_environment_variables() == ( + False, + "auto", + None, + ) with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): assert BigtableClient._read_environment_variables() == (False, "never", None) @@ -183,6 +190,105 @@ def test__read_environment_variables(): ) +def test_use_client_cert_effective(): + # Test case 1: Test when `should_use_client_cert` returns True. + # We mock the `should_use_client_cert` function to simulate a scenario where + # the google-auth library supports automatic mTLS and determines that a + # client certificate should be used. + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", return_value=True + ): + assert BigtableClient._use_client_cert_effective() is True + + # Test case 2: Test when `should_use_client_cert` returns False. + # We mock the `should_use_client_cert` function to simulate a scenario where + # the google-auth library supports automatic mTLS and determines that a + # client certificate should NOT be used. + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch( + "google.auth.transport.mtls.should_use_client_cert", return_value=False + ): + assert BigtableClient._use_client_cert_effective() is False + + # Test case 3: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "true". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + assert BigtableClient._use_client_cert_effective() is True + + # Test case 4: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "false". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"} + ): + assert BigtableClient._use_client_cert_effective() is False + + # Test case 5: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "True". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "True"}): + assert BigtableClient._use_client_cert_effective() is True + + # Test case 6: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "False". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "False"} + ): + assert BigtableClient._use_client_cert_effective() is False + + # Test case 7: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "TRUE". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "TRUE"}): + assert BigtableClient._use_client_cert_effective() is True + + # Test case 8: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "FALSE". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "FALSE"} + ): + assert BigtableClient._use_client_cert_effective() is False + + # Test case 9: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not set. + # In this case, the method should return False, which is the default value. + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict(os.environ, clear=True): + assert BigtableClient._use_client_cert_effective() is False + + # Test case 10: Test when `should_use_client_cert` is unavailable and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to an invalid value. + # The method should raise a ValueError as the environment variable must be either + # "true" or "false". + if not hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "unsupported"} + ): + with pytest.raises(ValueError): + BigtableClient._use_client_cert_effective() + + # Test case 11: Test when `should_use_client_cert` is available and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to an invalid value. + # The method should return False as the environment variable is set to an invalid value. + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "unsupported"} + ): + assert BigtableClient._use_client_cert_effective() is False + + # Test case 12: Test when `should_use_client_cert` is available and the + # `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is unset. Also, + # the GOOGLE_API_CONFIG environment variable is unset. + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": ""}): + with mock.patch.dict(os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": ""}): + assert BigtableClient._use_client_cert_effective() is False + + def test__get_client_cert_source(): mock_provided_cert_source = mock.Mock() mock_default_cert_source = mock.Mock() @@ -539,17 +645,6 @@ def test_bigtable_client_client_options(client_class, transport_class, transport == "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" ) - # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value. - with mock.patch.dict( - os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} - ): - with pytest.raises(ValueError) as excinfo: - client = client_class(transport=transport_name) - assert ( - str(excinfo.value) - == "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) - # Check the case quota_project_id is provided options = client_options.ClientOptions(quota_project_id="octopus") with mock.patch.object(transport_class, "__init__") as patched: @@ -761,6 +856,119 @@ def test_bigtable_client_get_mtls_endpoint_and_cert_source(client_class): assert api_endpoint == mock_api_endpoint assert cert_source is None + # Test the case GOOGLE_API_USE_CLIENT_CERTIFICATE is "Unsupported". + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} + ): + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + mock_client_cert_source = mock.Mock() + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, + api_endpoint=mock_api_endpoint, + ) + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source( + options + ) + assert api_endpoint == mock_api_endpoint + assert cert_source is None + + # Test cases for mTLS enablement when GOOGLE_API_USE_CLIENT_CERTIFICATE is unset. + test_cases = [ + ( + # With workloads present in config, mTLS is enabled. + { + "version": 1, + "cert_configs": { + "workload": { + "cert_path": "path/to/cert/file", + "key_path": "path/to/key/file", + } + }, + }, + mock_client_cert_source, + ), + ( + # With workloads not present in config, mTLS is disabled. + { + "version": 1, + "cert_configs": {}, + }, + None, + ), + ] + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + for config_data, expected_cert_source in test_cases: + env = os.environ.copy() + env.pop("GOOGLE_API_USE_CLIENT_CERTIFICATE", None) + with mock.patch.dict(os.environ, env, clear=True): + config_filename = "mock_certificate_config.json" + config_file_content = json.dumps(config_data) + m = mock.mock_open(read_data=config_file_content) + with mock.patch("builtins.open", m): + with mock.patch.dict( + os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} + ): + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, + api_endpoint=mock_api_endpoint, + ) + ( + api_endpoint, + cert_source, + ) = client_class.get_mtls_endpoint_and_cert_source(options) + assert api_endpoint == mock_api_endpoint + assert cert_source is expected_cert_source + + # Test cases for mTLS enablement when GOOGLE_API_USE_CLIENT_CERTIFICATE is unset(empty). + test_cases = [ + ( + # With workloads present in config, mTLS is enabled. + { + "version": 1, + "cert_configs": { + "workload": { + "cert_path": "path/to/cert/file", + "key_path": "path/to/key/file", + } + }, + }, + mock_client_cert_source, + ), + ( + # With workloads not present in config, mTLS is disabled. + { + "version": 1, + "cert_configs": {}, + }, + None, + ), + ] + if hasattr(google.auth.transport.mtls, "should_use_client_cert"): + for config_data, expected_cert_source in test_cases: + env = os.environ.copy() + env.pop("GOOGLE_API_USE_CLIENT_CERTIFICATE", "") + with mock.patch.dict(os.environ, env, clear=True): + config_filename = "mock_certificate_config.json" + config_file_content = json.dumps(config_data) + m = mock.mock_open(read_data=config_file_content) + with mock.patch("builtins.open", m): + with mock.patch.dict( + os.environ, {"GOOGLE_API_CERTIFICATE_CONFIG": config_filename} + ): + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, + api_endpoint=mock_api_endpoint, + ) + ( + api_endpoint, + cert_source, + ) = client_class.get_mtls_endpoint_and_cert_source(options) + assert api_endpoint == mock_api_endpoint + assert cert_source is expected_cert_source + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "never". with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() @@ -811,18 +1019,6 @@ def test_bigtable_client_get_mtls_endpoint_and_cert_source(client_class): == "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" ) - # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value. - with mock.patch.dict( - os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} - ): - with pytest.raises(ValueError) as excinfo: - client_class.get_mtls_endpoint_and_cert_source() - - assert ( - str(excinfo.value) - == "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) - @pytest.mark.parametrize("client_class", [BigtableClient, BigtableAsyncClient]) @mock.patch.object( @@ -11756,6 +11952,7 @@ def test_bigtable_grpc_asyncio_transport_channel(): # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are # removed from grpc/grpc_asyncio transport constructor. +@pytest.mark.filterwarnings("ignore::FutureWarning") @pytest.mark.parametrize( "transport_class", [transports.BigtableGrpcTransport, transports.BigtableGrpcAsyncIOTransport], From 6c2ea4ccb6de8dac0ba0287e1a9fce8bfdc8fe6f Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 16 Dec 2025 14:09:17 -0800 Subject: [PATCH 10/10] chore: librarian release pull request: 20251216T130719Z (#1251) PR created by the Librarian CLI to initialize a release. Merging this PR will auto trigger a release. Librarian Version: v0.7.0 Language Image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:b8058df4c45e9a6e07f6b4d65b458d0d059241dd34c814f151c8bf6b89211209
google-cloud-bigtable: 2.35.0 ## [2.35.0](https://github.com/googleapis/python-bigtable/compare/v2.34.0...v2.35.0) (2025-12-16) ### Features * add basic interceptor to client (#1206) ([6561cfac](https://github.com/googleapis/python-bigtable/commit/6561cfac)) * Add encodings for STRUCT and the Timestamp type ([72dfdc44](https://github.com/googleapis/python-bigtable/commit/72dfdc44)) * add PeerInfo proto in Bigtable API ([72dfdc44](https://github.com/googleapis/python-bigtable/commit/72dfdc44)) * Add Type API updates needed to support structured keys in materialized views ([72dfdc44](https://github.com/googleapis/python-bigtable/commit/72dfdc44)) * support mTLS certificates when available (#1249) ([ca20219c](https://github.com/googleapis/python-bigtable/commit/ca20219c)) ### Bug Fixes * re-export AddToCell for consistency (#1241) ([2a5baf11](https://github.com/googleapis/python-bigtable/commit/2a5baf11)) * async client uses fixed grace period (#1236) ([544db1cd](https://github.com/googleapis/python-bigtable/commit/544db1cd)) * Deprecate credentials_file argument ([72dfdc44](https://github.com/googleapis/python-bigtable/commit/72dfdc44)) * Add ReadRows/SampleRowKeys bindings for materialized views ([72dfdc44](https://github.com/googleapis/python-bigtable/commit/72dfdc44)) * retry cancelled errors (#1235) ([e3fd5d86](https://github.com/googleapis/python-bigtable/commit/e3fd5d86))
--- .librarian/state.yaml | 2 +- CHANGELOG.md | 20 +++++++++++++++++++ google/cloud/bigtable/gapic_version.py | 2 +- google/cloud/bigtable_admin/gapic_version.py | 2 +- .../cloud/bigtable_admin_v2/gapic_version.py | 2 +- google/cloud/bigtable_v2/gapic_version.py | 2 +- ...pet_metadata_google.bigtable.admin.v2.json | 2 +- 7 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.librarian/state.yaml b/.librarian/state.yaml index 56a3154ff..aca1e2fca 100644 --- a/.librarian/state.yaml +++ b/.librarian/state.yaml @@ -1,7 +1,7 @@ image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:b8058df4c45e9a6e07f6b4d65b458d0d059241dd34c814f151c8bf6b89211209 libraries: - id: google-cloud-bigtable - version: 2.34.0 + version: 2.35.0 last_generated_commit: a17b84add8318f780fcc8a027815d5fee644b9f7 apis: - path: google/bigtable/v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a0251dc1..cbb707694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ [1]: https://pypi.org/project/google-cloud-bigtable/#history +## [2.35.0](https://github.com/googleapis/python-bigtable/compare/v2.34.0...v2.35.0) (2025-12-16) + + +### Features + +* support mTLS certificates when available (#1249) ([ca20219cf45305de25dfb715f69dd63bce9981b7](https://github.com/googleapis/python-bigtable/commit/ca20219cf45305de25dfb715f69dd63bce9981b7)) +* add basic interceptor to client (#1206) ([6561cfac605ba7c5b3f750c3bdca9108e517ba77](https://github.com/googleapis/python-bigtable/commit/6561cfac605ba7c5b3f750c3bdca9108e517ba77)) +* add PeerInfo proto in Bigtable API ([72dfdc440c22db0f4c372e6f11a9f7dc83fed350](https://github.com/googleapis/python-bigtable/commit/72dfdc440c22db0f4c372e6f11a9f7dc83fed350)) +* Add Type API updates needed to support structured keys in materialized views ([72dfdc440c22db0f4c372e6f11a9f7dc83fed350](https://github.com/googleapis/python-bigtable/commit/72dfdc440c22db0f4c372e6f11a9f7dc83fed350)) +* Add encodings for STRUCT and the Timestamp type ([72dfdc440c22db0f4c372e6f11a9f7dc83fed350](https://github.com/googleapis/python-bigtable/commit/72dfdc440c22db0f4c372e6f11a9f7dc83fed350)) + + +### Bug Fixes + +* async client uses fixed grace period (#1236) ([544db1cd7af876298b8637f495b6c7b2a0bcf16c](https://github.com/googleapis/python-bigtable/commit/544db1cd7af876298b8637f495b6c7b2a0bcf16c)) +* re-export AddToCell for consistency (#1241) ([2a5baf11d30dc383a7b48d5f43b6cbb6160782e3](https://github.com/googleapis/python-bigtable/commit/2a5baf11d30dc383a7b48d5f43b6cbb6160782e3)) +* retry cancelled errors (#1235) ([e3fd5d8668303db4ed35e9bf6be48b46954f9d67](https://github.com/googleapis/python-bigtable/commit/e3fd5d8668303db4ed35e9bf6be48b46954f9d67)) +* Add ReadRows/SampleRowKeys bindings for materialized views ([72dfdc440c22db0f4c372e6f11a9f7dc83fed350](https://github.com/googleapis/python-bigtable/commit/72dfdc440c22db0f4c372e6f11a9f7dc83fed350)) +* Deprecate credentials_file argument ([72dfdc440c22db0f4c372e6f11a9f7dc83fed350](https://github.com/googleapis/python-bigtable/commit/72dfdc440c22db0f4c372e6f11a9f7dc83fed350)) + ## [2.34.0](https://github.com/googleapis/python-bigtable/compare/v2.33.0...v2.34.0) (2025-10-16) diff --git a/google/cloud/bigtable/gapic_version.py b/google/cloud/bigtable/gapic_version.py index 4800b0559..a105a8349 100644 --- a/google/cloud/bigtable/gapic_version.py +++ b/google/cloud/bigtable/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "2.34.0" # {x-release-please-version} +__version__ = "2.35.0" # {x-release-please-version} diff --git a/google/cloud/bigtable_admin/gapic_version.py b/google/cloud/bigtable_admin/gapic_version.py index b31b170e1..6d72a226d 100644 --- a/google/cloud/bigtable_admin/gapic_version.py +++ b/google/cloud/bigtable_admin/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "2.34.0" # {x-release-please-version} +__version__ = "2.35.0" # {x-release-please-version} diff --git a/google/cloud/bigtable_admin_v2/gapic_version.py b/google/cloud/bigtable_admin_v2/gapic_version.py index b31b170e1..6d72a226d 100644 --- a/google/cloud/bigtable_admin_v2/gapic_version.py +++ b/google/cloud/bigtable_admin_v2/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "2.34.0" # {x-release-please-version} +__version__ = "2.35.0" # {x-release-please-version} diff --git a/google/cloud/bigtable_v2/gapic_version.py b/google/cloud/bigtable_v2/gapic_version.py index b31b170e1..6d72a226d 100644 --- a/google/cloud/bigtable_v2/gapic_version.py +++ b/google/cloud/bigtable_v2/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "2.34.0" # {x-release-please-version} +__version__ = "2.35.0" # {x-release-please-version} diff --git a/samples/generated_samples/snippet_metadata_google.bigtable.admin.v2.json b/samples/generated_samples/snippet_metadata_google.bigtable.admin.v2.json index 8b4f59fbd..42db3b70b 100644 --- a/samples/generated_samples/snippet_metadata_google.bigtable.admin.v2.json +++ b/samples/generated_samples/snippet_metadata_google.bigtable.admin.v2.json @@ -8,7 +8,7 @@ ], "language": "PYTHON", "name": "google-cloud-bigtable-admin", - "version": "2.34.0" + "version": "2.35.0" }, "snippets": [ {