diff --git a/.coveragerc b/.coveragerc index c4d3671..d1bb5ee 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,6 +5,7 @@ branch = True show_missing = True omit = google/cloud/appengine_admin/__init__.py + google/cloud/appengine_admin/gapic_version.py exclude_lines = # Re-enable the standard pragma pragma: NO COVER diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 889f77d..5fc5daa 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,4 +13,4 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:c43f1d918bcf817d337aa29ff833439494a158a0831508fda4ec75dc4c0d0320 + digest: sha256:8555f0e37e6261408f792bfd6635102d2da5ad73f8f09bcb24f25e6afb5fac97 diff --git a/.kokoro/requirements.in b/.kokoro/requirements.in index cbd7e77..882178c 100644 --- a/.kokoro/requirements.in +++ b/.kokoro/requirements.in @@ -1,5 +1,5 @@ gcp-docuploader -gcp-releasetool +gcp-releasetool>=1.10.5 # required for compatibility with cryptography>=39.x importlib-metadata typing-extensions twine diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index 05dc467..fa99c12 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -113,33 +113,28 @@ commonmark==0.9.1 \ --hash=sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60 \ --hash=sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9 # via rich -cryptography==38.0.3 \ - --hash=sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d \ - --hash=sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd \ - --hash=sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146 \ - --hash=sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7 \ - --hash=sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436 \ - --hash=sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0 \ - --hash=sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828 \ - --hash=sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b \ - --hash=sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55 \ - --hash=sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36 \ - --hash=sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50 \ - --hash=sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2 \ - --hash=sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a \ - --hash=sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8 \ - --hash=sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0 \ - --hash=sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548 \ - --hash=sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320 \ - --hash=sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748 \ - --hash=sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249 \ - --hash=sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959 \ - --hash=sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f \ - --hash=sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0 \ - --hash=sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd \ - --hash=sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220 \ - --hash=sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c \ - --hash=sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722 +cryptography==39.0.1 \ + --hash=sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4 \ + --hash=sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f \ + --hash=sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502 \ + --hash=sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41 \ + --hash=sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965 \ + --hash=sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e \ + --hash=sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc \ + --hash=sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad \ + --hash=sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505 \ + --hash=sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388 \ + --hash=sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6 \ + --hash=sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2 \ + --hash=sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac \ + --hash=sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695 \ + --hash=sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6 \ + --hash=sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336 \ + --hash=sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0 \ + --hash=sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c \ + --hash=sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106 \ + --hash=sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a \ + --hash=sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8 # via # gcp-releasetool # secretstorage @@ -159,9 +154,9 @@ gcp-docuploader==0.6.4 \ --hash=sha256:01486419e24633af78fd0167db74a2763974765ee8078ca6eb6964d0ebd388af \ --hash=sha256:70861190c123d907b3b067da896265ead2eeb9263969d6955c9e0bb091b5ccbf # via -r requirements.in -gcp-releasetool==1.10.0 \ - --hash=sha256:72a38ca91b59c24f7e699e9227c90cbe4dd71b789383cb0164b088abae294c83 \ - --hash=sha256:8c7c99320208383d4bb2b808c6880eb7a81424afe7cdba3c8d84b25f4f0e097d +gcp-releasetool==1.10.5 \ + --hash=sha256:174b7b102d704b254f2a26a3eda2c684fd3543320ec239baf771542a2e58e109 \ + --hash=sha256:e29d29927fe2ca493105a82958c6873bb2b90d503acac56be2c229e74de0eec9 # via -r requirements.in google-api-core==2.10.2 \ --hash=sha256:10c06f7739fe57781f87523375e8e1a3a4674bf6392cd6131a3222182b971320 \ diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7840fde..4fcfdf7 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.8.1" + ".": "1.9.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c9c677..6011748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.9.0](https://github.com/googleapis/python-appengine-admin/compare/v1.8.1...v1.9.0) (2023-02-27) + + +### Features + +* Enable "rest" transport in Python for services supporting numeric enums ([#224](https://github.com/googleapis/python-appengine-admin/issues/224)) ([819631a](https://github.com/googleapis/python-appengine-admin/commit/819631abc6b95d3d40d1772f74dac62300f4616f)) + ## [1.8.1](https://github.com/googleapis/python-appengine-admin/compare/v1.8.0...v1.8.1) (2023-01-20) diff --git a/google/cloud/appengine_admin/gapic_version.py b/google/cloud/appengine_admin/gapic_version.py index 90e0293..163d151 100644 --- a/google/cloud/appengine_admin/gapic_version.py +++ b/google/cloud/appengine_admin/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.8.1" # {x-release-please-version} +__version__ = "1.9.0" # {x-release-please-version} diff --git a/google/cloud/appengine_admin_v1/__init__.py b/google/cloud/appengine_admin_v1/__init__.py index 38164c2..1b5d78c 100644 --- a/google/cloud/appengine_admin_v1/__init__.py +++ b/google/cloud/appengine_admin_v1/__init__.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from google.cloud.appengine_admin import gapic_version as package_version +from google.cloud.appengine_admin_v1 import gapic_version as package_version __version__ = package_version.__version__ diff --git a/google/cloud/appengine_admin_v1/gapic_metadata.json b/google/cloud/appengine_admin_v1/gapic_metadata.json index 0bb6583..d99b434 100644 --- a/google/cloud/appengine_admin_v1/gapic_metadata.json +++ b/google/cloud/appengine_admin_v1/gapic_metadata.json @@ -56,6 +56,31 @@ ] } } + }, + "rest": { + "libraryClient": "ApplicationsClient", + "rpcs": { + "CreateApplication": { + "methods": [ + "create_application" + ] + }, + "GetApplication": { + "methods": [ + "get_application" + ] + }, + "RepairApplication": { + "methods": [ + "repair_application" + ] + }, + "UpdateApplication": { + "methods": [ + "update_application" + ] + } + } } } }, @@ -120,6 +145,36 @@ ] } } + }, + "rest": { + "libraryClient": "AuthorizedCertificatesClient", + "rpcs": { + "CreateAuthorizedCertificate": { + "methods": [ + "create_authorized_certificate" + ] + }, + "DeleteAuthorizedCertificate": { + "methods": [ + "delete_authorized_certificate" + ] + }, + "GetAuthorizedCertificate": { + "methods": [ + "get_authorized_certificate" + ] + }, + "ListAuthorizedCertificates": { + "methods": [ + "list_authorized_certificates" + ] + }, + "UpdateAuthorizedCertificate": { + "methods": [ + "update_authorized_certificate" + ] + } + } } } }, @@ -144,6 +199,16 @@ ] } } + }, + "rest": { + "libraryClient": "AuthorizedDomainsClient", + "rpcs": { + "ListAuthorizedDomains": { + "methods": [ + "list_authorized_domains" + ] + } + } } } }, @@ -208,6 +273,36 @@ ] } } + }, + "rest": { + "libraryClient": "DomainMappingsClient", + "rpcs": { + "CreateDomainMapping": { + "methods": [ + "create_domain_mapping" + ] + }, + "DeleteDomainMapping": { + "methods": [ + "delete_domain_mapping" + ] + }, + "GetDomainMapping": { + "methods": [ + "get_domain_mapping" + ] + }, + "ListDomainMappings": { + "methods": [ + "list_domain_mappings" + ] + }, + "UpdateDomainMapping": { + "methods": [ + "update_domain_mapping" + ] + } + } } } }, @@ -282,6 +377,41 @@ ] } } + }, + "rest": { + "libraryClient": "FirewallClient", + "rpcs": { + "BatchUpdateIngressRules": { + "methods": [ + "batch_update_ingress_rules" + ] + }, + "CreateIngressRule": { + "methods": [ + "create_ingress_rule" + ] + }, + "DeleteIngressRule": { + "methods": [ + "delete_ingress_rule" + ] + }, + "GetIngressRule": { + "methods": [ + "get_ingress_rule" + ] + }, + "ListIngressRules": { + "methods": [ + "list_ingress_rules" + ] + }, + "UpdateIngressRule": { + "methods": [ + "update_ingress_rule" + ] + } + } } } }, @@ -336,6 +466,31 @@ ] } } + }, + "rest": { + "libraryClient": "InstancesClient", + "rpcs": { + "DebugInstance": { + "methods": [ + "debug_instance" + ] + }, + "DeleteInstance": { + "methods": [ + "delete_instance" + ] + }, + "GetInstance": { + "methods": [ + "get_instance" + ] + }, + "ListInstances": { + "methods": [ + "list_instances" + ] + } + } } } }, @@ -390,6 +545,31 @@ ] } } + }, + "rest": { + "libraryClient": "ServicesClient", + "rpcs": { + "DeleteService": { + "methods": [ + "delete_service" + ] + }, + "GetService": { + "methods": [ + "get_service" + ] + }, + "ListServices": { + "methods": [ + "list_services" + ] + }, + "UpdateService": { + "methods": [ + "update_service" + ] + } + } } } }, @@ -454,6 +634,36 @@ ] } } + }, + "rest": { + "libraryClient": "VersionsClient", + "rpcs": { + "CreateVersion": { + "methods": [ + "create_version" + ] + }, + "DeleteVersion": { + "methods": [ + "delete_version" + ] + }, + "GetVersion": { + "methods": [ + "get_version" + ] + }, + "ListVersions": { + "methods": [ + "list_versions" + ] + }, + "UpdateVersion": { + "methods": [ + "update_version" + ] + } + } } } } diff --git a/google/cloud/appengine_admin_v1/gapic_version.py b/google/cloud/appengine_admin_v1/gapic_version.py index 90e0293..163d151 100644 --- a/google/cloud/appengine_admin_v1/gapic_version.py +++ b/google/cloud/appengine_admin_v1/gapic_version.py @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -__version__ = "1.8.1" # {x-release-please-version} +__version__ = "1.9.0" # {x-release-please-version} diff --git a/google/cloud/appengine_admin_v1/services/applications/client.py b/google/cloud/appengine_admin_v1/services/applications/client.py index 5742ba5..8ae55a0 100644 --- a/google/cloud/appengine_admin_v1/services/applications/client.py +++ b/google/cloud/appengine_admin_v1/services/applications/client.py @@ -56,6 +56,7 @@ from .transports.base import DEFAULT_CLIENT_INFO, ApplicationsTransport from .transports.grpc import ApplicationsGrpcTransport from .transports.grpc_asyncio import ApplicationsGrpcAsyncIOTransport +from .transports.rest import ApplicationsRestTransport class ApplicationsClientMeta(type): @@ -69,6 +70,7 @@ class ApplicationsClientMeta(type): _transport_registry = OrderedDict() # type: Dict[str, Type[ApplicationsTransport]] _transport_registry["grpc"] = ApplicationsGrpcTransport _transport_registry["grpc_asyncio"] = ApplicationsGrpcAsyncIOTransport + _transport_registry["rest"] = ApplicationsRestTransport def get_transport_class( cls, diff --git a/google/cloud/appengine_admin_v1/services/applications/transports/__init__.py b/google/cloud/appengine_admin_v1/services/applications/transports/__init__.py index d911b25..827f1d5 100644 --- a/google/cloud/appengine_admin_v1/services/applications/transports/__init__.py +++ b/google/cloud/appengine_admin_v1/services/applications/transports/__init__.py @@ -19,14 +19,18 @@ from .base import ApplicationsTransport from .grpc import ApplicationsGrpcTransport from .grpc_asyncio import ApplicationsGrpcAsyncIOTransport +from .rest import ApplicationsRestInterceptor, ApplicationsRestTransport # Compile a registry of transports. _transport_registry = OrderedDict() # type: Dict[str, Type[ApplicationsTransport]] _transport_registry["grpc"] = ApplicationsGrpcTransport _transport_registry["grpc_asyncio"] = ApplicationsGrpcAsyncIOTransport +_transport_registry["rest"] = ApplicationsRestTransport __all__ = ( "ApplicationsTransport", "ApplicationsGrpcTransport", "ApplicationsGrpcAsyncIOTransport", + "ApplicationsRestTransport", + "ApplicationsRestInterceptor", ) diff --git a/google/cloud/appengine_admin_v1/services/applications/transports/rest.py b/google/cloud/appengine_admin_v1/services/applications/transports/rest.py new file mode 100644 index 0000000..3259f25 --- /dev/null +++ b/google/cloud/appengine_admin_v1/services/applications/transports/rest.py @@ -0,0 +1,723 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import dataclasses +import json # type: ignore +import re +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +import warnings + +from google.api_core import ( + gapic_v1, + operations_v1, + path_template, + rest_helpers, + rest_streaming, +) +from google.api_core import exceptions as core_exceptions +from google.api_core import retry as retries +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.auth.transport.requests import AuthorizedSession # type: ignore +from google.protobuf import json_format +import grpc # type: ignore +from requests import __version__ as requests_version + +try: + OptionalRetry = Union[retries.Retry, gapic_v1.method._MethodDefault] +except AttributeError: # pragma: NO COVER + OptionalRetry = Union[retries.Retry, object] # type: ignore + + +from google.longrunning import operations_pb2 # type: ignore + +from google.cloud.appengine_admin_v1.types import appengine, application + +from .base import ApplicationsTransport +from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, + grpc_version=None, + rest_version=requests_version, +) + + +class ApplicationsRestInterceptor: + """Interceptor for Applications. + + Interceptors are used to manipulate requests, request metadata, and responses + in arbitrary ways. + Example use cases include: + * Logging + * Verifying requests according to service or custom semantics + * Stripping extraneous information from responses + + These use cases and more can be enabled by injecting an + instance of a custom subclass when constructing the ApplicationsRestTransport. + + .. code-block:: python + class MyCustomApplicationsInterceptor(ApplicationsRestInterceptor): + def pre_create_application(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_create_application(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_get_application(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_get_application(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_repair_application(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_repair_application(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_update_application(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_update_application(self, response): + logging.log(f"Received response: {response}") + return response + + transport = ApplicationsRestTransport(interceptor=MyCustomApplicationsInterceptor()) + client = ApplicationsClient(transport=transport) + + + """ + + def pre_create_application( + self, + request: appengine.CreateApplicationRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.CreateApplicationRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for create_application + + Override in a subclass to manipulate the request or metadata + before they are sent to the Applications server. + """ + return request, metadata + + def post_create_application( + self, response: operations_pb2.Operation + ) -> operations_pb2.Operation: + """Post-rpc interceptor for create_application + + Override in a subclass to manipulate the response + after it is returned by the Applications server but before + it is returned to user code. + """ + return response + + def pre_get_application( + self, + request: appengine.GetApplicationRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.GetApplicationRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for get_application + + Override in a subclass to manipulate the request or metadata + before they are sent to the Applications server. + """ + return request, metadata + + def post_get_application( + self, response: application.Application + ) -> application.Application: + """Post-rpc interceptor for get_application + + Override in a subclass to manipulate the response + after it is returned by the Applications server but before + it is returned to user code. + """ + return response + + def pre_repair_application( + self, + request: appengine.RepairApplicationRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.RepairApplicationRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for repair_application + + Override in a subclass to manipulate the request or metadata + before they are sent to the Applications server. + """ + return request, metadata + + def post_repair_application( + self, response: operations_pb2.Operation + ) -> operations_pb2.Operation: + """Post-rpc interceptor for repair_application + + Override in a subclass to manipulate the response + after it is returned by the Applications server but before + it is returned to user code. + """ + return response + + def pre_update_application( + self, + request: appengine.UpdateApplicationRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.UpdateApplicationRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for update_application + + Override in a subclass to manipulate the request or metadata + before they are sent to the Applications server. + """ + return request, metadata + + def post_update_application( + self, response: operations_pb2.Operation + ) -> operations_pb2.Operation: + """Post-rpc interceptor for update_application + + Override in a subclass to manipulate the response + after it is returned by the Applications server but before + it is returned to user code. + """ + return response + + +@dataclasses.dataclass +class ApplicationsRestStub: + _session: AuthorizedSession + _host: str + _interceptor: ApplicationsRestInterceptor + + +class ApplicationsRestTransport(ApplicationsTransport): + """REST backend transport for Applications. + + Manages App Engine applications. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends JSON representations of protocol buffers over HTTP/1.1 + + """ + + def __init__( + self, + *, + host: str = "appengine.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + client_cert_source_for_mtls: Optional[Callable[[], Tuple[bytes, bytes]]] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + url_scheme: str = "https", + interceptor: Optional[ApplicationsRestInterceptor] = None, + api_audience: Optional[str] = None, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to. + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + 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 + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. + 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 + certificate to configure mutual TLS HTTP channel. It is ignored + if ``channel`` is provided. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you are developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + url_scheme: the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. + """ + # Run the base constructor + # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. + # TODO: When custom host (api_endpoint) is set, `scopes` must *also* be set on the + # credentials object + maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) + if maybe_url_match is None: + raise ValueError( + f"Unexpected hostname structure: {host}" + ) # pragma: NO COVER + + url_match_items = maybe_url_match.groupdict() + + host = f"{url_scheme}://{host}" if not url_match_items["scheme"] else host + + super().__init__( + host=host, + credentials=credentials, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + api_audience=api_audience, + ) + self._session = AuthorizedSession( + self._credentials, default_host=self.DEFAULT_HOST + ) + self._operations_client: Optional[operations_v1.AbstractOperationsClient] = None + if client_cert_source_for_mtls: + self._session.configure_mtls_channel(client_cert_source_for_mtls) + self._interceptor = interceptor or ApplicationsRestInterceptor() + self._prep_wrapped_messages(client_info) + + @property + def operations_client(self) -> operations_v1.AbstractOperationsClient: + """Create the client designed to process long-running operations. + + This property caches on the instance; repeated calls return the same + client. + """ + # Only create a new client if we do not already have one. + if self._operations_client is None: + http_options: Dict[str, List[Dict[str, str]]] = { + "google.longrunning.Operations.GetOperation": [ + { + "method": "get", + "uri": "/v1/{name=apps/*/operations/*}", + }, + ], + "google.longrunning.Operations.ListOperations": [ + { + "method": "get", + "uri": "/v1/{name=apps/*}/operations", + }, + ], + } + + rest_transport = operations_v1.OperationsRestTransport( + host=self._host, + # use the credentials which are saved + credentials=self._credentials, + scopes=self._scopes, + http_options=http_options, + path_prefix="v1", + ) + + self._operations_client = operations_v1.AbstractOperationsClient( + transport=rest_transport + ) + + # Return the client from cache. + return self._operations_client + + class _CreateApplication(ApplicationsRestStub): + def __hash__(self): + return hash("CreateApplication") + + def __call__( + self, + request: appengine.CreateApplicationRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Call the create application method over HTTP. + + Args: + request (~.appengine.CreateApplicationRequest): + The request object. Request message for ``Applications.CreateApplication``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.Operation: + This resource represents a + long-running operation that is the + result of a network API call. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "post", + "uri": "/v1/apps", + "body": "application", + }, + ] + request, metadata = self._interceptor.pre_create_application( + request, metadata + ) + pb_request = appengine.CreateApplicationRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + # Jsonify the request body + + body = json_format.MessageToJson( + transcoded_request["body"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = operations_pb2.Operation() + json_format.Parse(response.content, resp, ignore_unknown_fields=True) + resp = self._interceptor.post_create_application(resp) + return resp + + class _GetApplication(ApplicationsRestStub): + def __hash__(self): + return hash("GetApplication") + + def __call__( + self, + request: appengine.GetApplicationRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> application.Application: + r"""Call the get application method over HTTP. + + Args: + request (~.appengine.GetApplicationRequest): + The request object. Request message for ``Applications.GetApplication``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.application.Application: + An Application resource contains the + top-level configuration of an App Engine + application. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "get", + "uri": "/v1/{name=apps/*}", + }, + ] + request, metadata = self._interceptor.pre_get_application(request, metadata) + pb_request = appengine.GetApplicationRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = application.Application() + pb_resp = application.Application.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_get_application(resp) + return resp + + class _RepairApplication(ApplicationsRestStub): + def __hash__(self): + return hash("RepairApplication") + + def __call__( + self, + request: appengine.RepairApplicationRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Call the repair application method over HTTP. + + Args: + request (~.appengine.RepairApplicationRequest): + The request object. Request message for + 'Applications.RepairApplication'. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.Operation: + This resource represents a + long-running operation that is the + result of a network API call. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "post", + "uri": "/v1/{name=apps/*}:repair", + "body": "*", + }, + ] + request, metadata = self._interceptor.pre_repair_application( + request, metadata + ) + pb_request = appengine.RepairApplicationRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + # Jsonify the request body + + body = json_format.MessageToJson( + transcoded_request["body"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = operations_pb2.Operation() + json_format.Parse(response.content, resp, ignore_unknown_fields=True) + resp = self._interceptor.post_repair_application(resp) + return resp + + class _UpdateApplication(ApplicationsRestStub): + def __hash__(self): + return hash("UpdateApplication") + + def __call__( + self, + request: appengine.UpdateApplicationRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Call the update application method over HTTP. + + Args: + request (~.appengine.UpdateApplicationRequest): + The request object. Request message for ``Applications.UpdateApplication``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.Operation: + This resource represents a + long-running operation that is the + result of a network API call. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "patch", + "uri": "/v1/{name=apps/*}", + "body": "application", + }, + ] + request, metadata = self._interceptor.pre_update_application( + request, metadata + ) + pb_request = appengine.UpdateApplicationRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + # Jsonify the request body + + body = json_format.MessageToJson( + transcoded_request["body"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = operations_pb2.Operation() + json_format.Parse(response.content, resp, ignore_unknown_fields=True) + resp = self._interceptor.post_update_application(resp) + return resp + + @property + def create_application( + self, + ) -> Callable[[appengine.CreateApplicationRequest], operations_pb2.Operation]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._CreateApplication(self._session, self._host, self._interceptor) # type: ignore + + @property + def get_application( + self, + ) -> Callable[[appengine.GetApplicationRequest], application.Application]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._GetApplication(self._session, self._host, self._interceptor) # type: ignore + + @property + def repair_application( + self, + ) -> Callable[[appengine.RepairApplicationRequest], operations_pb2.Operation]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._RepairApplication(self._session, self._host, self._interceptor) # type: ignore + + @property + def update_application( + self, + ) -> Callable[[appengine.UpdateApplicationRequest], operations_pb2.Operation]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._UpdateApplication(self._session, self._host, self._interceptor) # type: ignore + + @property + def kind(self) -> str: + return "rest" + + def close(self): + self._session.close() + + +__all__ = ("ApplicationsRestTransport",) diff --git a/google/cloud/appengine_admin_v1/services/authorized_certificates/client.py b/google/cloud/appengine_admin_v1/services/authorized_certificates/client.py index be433ac..d6a9f17 100644 --- a/google/cloud/appengine_admin_v1/services/authorized_certificates/client.py +++ b/google/cloud/appengine_admin_v1/services/authorized_certificates/client.py @@ -54,6 +54,7 @@ from .transports.base import DEFAULT_CLIENT_INFO, AuthorizedCertificatesTransport from .transports.grpc import AuthorizedCertificatesGrpcTransport from .transports.grpc_asyncio import AuthorizedCertificatesGrpcAsyncIOTransport +from .transports.rest import AuthorizedCertificatesRestTransport class AuthorizedCertificatesClientMeta(type): @@ -69,6 +70,7 @@ class AuthorizedCertificatesClientMeta(type): ) # type: Dict[str, Type[AuthorizedCertificatesTransport]] _transport_registry["grpc"] = AuthorizedCertificatesGrpcTransport _transport_registry["grpc_asyncio"] = AuthorizedCertificatesGrpcAsyncIOTransport + _transport_registry["rest"] = AuthorizedCertificatesRestTransport def get_transport_class( cls, diff --git a/google/cloud/appengine_admin_v1/services/authorized_certificates/transports/__init__.py b/google/cloud/appengine_admin_v1/services/authorized_certificates/transports/__init__.py index 659030c..24f04d2 100644 --- a/google/cloud/appengine_admin_v1/services/authorized_certificates/transports/__init__.py +++ b/google/cloud/appengine_admin_v1/services/authorized_certificates/transports/__init__.py @@ -19,6 +19,10 @@ from .base import AuthorizedCertificatesTransport from .grpc import AuthorizedCertificatesGrpcTransport from .grpc_asyncio import AuthorizedCertificatesGrpcAsyncIOTransport +from .rest import ( + AuthorizedCertificatesRestInterceptor, + AuthorizedCertificatesRestTransport, +) # Compile a registry of transports. _transport_registry = ( @@ -26,9 +30,12 @@ ) # type: Dict[str, Type[AuthorizedCertificatesTransport]] _transport_registry["grpc"] = AuthorizedCertificatesGrpcTransport _transport_registry["grpc_asyncio"] = AuthorizedCertificatesGrpcAsyncIOTransport +_transport_registry["rest"] = AuthorizedCertificatesRestTransport __all__ = ( "AuthorizedCertificatesTransport", "AuthorizedCertificatesGrpcTransport", "AuthorizedCertificatesGrpcAsyncIOTransport", + "AuthorizedCertificatesRestTransport", + "AuthorizedCertificatesRestInterceptor", ) diff --git a/google/cloud/appengine_admin_v1/services/authorized_certificates/transports/rest.py b/google/cloud/appengine_admin_v1/services/authorized_certificates/transports/rest.py new file mode 100644 index 0000000..f7f162e --- /dev/null +++ b/google/cloud/appengine_admin_v1/services/authorized_certificates/transports/rest.py @@ -0,0 +1,795 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import dataclasses +import json # type: ignore +import re +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +import warnings + +from google.api_core import gapic_v1, path_template, rest_helpers, rest_streaming +from google.api_core import exceptions as core_exceptions +from google.api_core import retry as retries +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.auth.transport.requests import AuthorizedSession # type: ignore +from google.protobuf import json_format +import grpc # type: ignore +from requests import __version__ as requests_version + +try: + OptionalRetry = Union[retries.Retry, gapic_v1.method._MethodDefault] +except AttributeError: # pragma: NO COVER + OptionalRetry = Union[retries.Retry, object] # type: ignore + + +from google.protobuf import empty_pb2 # type: ignore + +from google.cloud.appengine_admin_v1.types import appengine, certificate + +from .base import AuthorizedCertificatesTransport +from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, + grpc_version=None, + rest_version=requests_version, +) + + +class AuthorizedCertificatesRestInterceptor: + """Interceptor for AuthorizedCertificates. + + Interceptors are used to manipulate requests, request metadata, and responses + in arbitrary ways. + Example use cases include: + * Logging + * Verifying requests according to service or custom semantics + * Stripping extraneous information from responses + + These use cases and more can be enabled by injecting an + instance of a custom subclass when constructing the AuthorizedCertificatesRestTransport. + + .. code-block:: python + class MyCustomAuthorizedCertificatesInterceptor(AuthorizedCertificatesRestInterceptor): + def pre_create_authorized_certificate(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_create_authorized_certificate(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_delete_authorized_certificate(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def pre_get_authorized_certificate(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_get_authorized_certificate(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_list_authorized_certificates(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_list_authorized_certificates(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_update_authorized_certificate(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_update_authorized_certificate(self, response): + logging.log(f"Received response: {response}") + return response + + transport = AuthorizedCertificatesRestTransport(interceptor=MyCustomAuthorizedCertificatesInterceptor()) + client = AuthorizedCertificatesClient(transport=transport) + + + """ + + def pre_create_authorized_certificate( + self, + request: appengine.CreateAuthorizedCertificateRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.CreateAuthorizedCertificateRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for create_authorized_certificate + + Override in a subclass to manipulate the request or metadata + before they are sent to the AuthorizedCertificates server. + """ + return request, metadata + + def post_create_authorized_certificate( + self, response: certificate.AuthorizedCertificate + ) -> certificate.AuthorizedCertificate: + """Post-rpc interceptor for create_authorized_certificate + + Override in a subclass to manipulate the response + after it is returned by the AuthorizedCertificates server but before + it is returned to user code. + """ + return response + + def pre_delete_authorized_certificate( + self, + request: appengine.DeleteAuthorizedCertificateRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.DeleteAuthorizedCertificateRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for delete_authorized_certificate + + Override in a subclass to manipulate the request or metadata + before they are sent to the AuthorizedCertificates server. + """ + return request, metadata + + def pre_get_authorized_certificate( + self, + request: appengine.GetAuthorizedCertificateRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.GetAuthorizedCertificateRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for get_authorized_certificate + + Override in a subclass to manipulate the request or metadata + before they are sent to the AuthorizedCertificates server. + """ + return request, metadata + + def post_get_authorized_certificate( + self, response: certificate.AuthorizedCertificate + ) -> certificate.AuthorizedCertificate: + """Post-rpc interceptor for get_authorized_certificate + + Override in a subclass to manipulate the response + after it is returned by the AuthorizedCertificates server but before + it is returned to user code. + """ + return response + + def pre_list_authorized_certificates( + self, + request: appengine.ListAuthorizedCertificatesRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.ListAuthorizedCertificatesRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for list_authorized_certificates + + Override in a subclass to manipulate the request or metadata + before they are sent to the AuthorizedCertificates server. + """ + return request, metadata + + def post_list_authorized_certificates( + self, response: appengine.ListAuthorizedCertificatesResponse + ) -> appengine.ListAuthorizedCertificatesResponse: + """Post-rpc interceptor for list_authorized_certificates + + Override in a subclass to manipulate the response + after it is returned by the AuthorizedCertificates server but before + it is returned to user code. + """ + return response + + def pre_update_authorized_certificate( + self, + request: appengine.UpdateAuthorizedCertificateRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.UpdateAuthorizedCertificateRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for update_authorized_certificate + + Override in a subclass to manipulate the request or metadata + before they are sent to the AuthorizedCertificates server. + """ + return request, metadata + + def post_update_authorized_certificate( + self, response: certificate.AuthorizedCertificate + ) -> certificate.AuthorizedCertificate: + """Post-rpc interceptor for update_authorized_certificate + + Override in a subclass to manipulate the response + after it is returned by the AuthorizedCertificates server but before + it is returned to user code. + """ + return response + + +@dataclasses.dataclass +class AuthorizedCertificatesRestStub: + _session: AuthorizedSession + _host: str + _interceptor: AuthorizedCertificatesRestInterceptor + + +class AuthorizedCertificatesRestTransport(AuthorizedCertificatesTransport): + """REST backend transport for AuthorizedCertificates. + + Manages SSL certificates a user is authorized to administer. + A user can administer any SSL certificates applicable to their + authorized domains. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends JSON representations of protocol buffers over HTTP/1.1 + + """ + + def __init__( + self, + *, + host: str = "appengine.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + client_cert_source_for_mtls: Optional[Callable[[], Tuple[bytes, bytes]]] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + url_scheme: str = "https", + interceptor: Optional[AuthorizedCertificatesRestInterceptor] = None, + api_audience: Optional[str] = None, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to. + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + 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 + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. + 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 + certificate to configure mutual TLS HTTP channel. It is ignored + if ``channel`` is provided. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you are developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + url_scheme: the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. + """ + # Run the base constructor + # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. + # TODO: When custom host (api_endpoint) is set, `scopes` must *also* be set on the + # credentials object + maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) + if maybe_url_match is None: + raise ValueError( + f"Unexpected hostname structure: {host}" + ) # pragma: NO COVER + + url_match_items = maybe_url_match.groupdict() + + host = f"{url_scheme}://{host}" if not url_match_items["scheme"] else host + + super().__init__( + host=host, + credentials=credentials, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + api_audience=api_audience, + ) + self._session = AuthorizedSession( + self._credentials, default_host=self.DEFAULT_HOST + ) + if client_cert_source_for_mtls: + self._session.configure_mtls_channel(client_cert_source_for_mtls) + self._interceptor = interceptor or AuthorizedCertificatesRestInterceptor() + self._prep_wrapped_messages(client_info) + + class _CreateAuthorizedCertificate(AuthorizedCertificatesRestStub): + def __hash__(self): + return hash("CreateAuthorizedCertificate") + + def __call__( + self, + request: appengine.CreateAuthorizedCertificateRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> certificate.AuthorizedCertificate: + r"""Call the create authorized + certificate method over HTTP. + + Args: + request (~.appengine.CreateAuthorizedCertificateRequest): + The request object. Request message for + ``AuthorizedCertificates.CreateAuthorizedCertificate``. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.certificate.AuthorizedCertificate: + An SSL certificate that a user has + been authorized to administer. A user is + authorized to administer any certificate + that applies to one of their authorized + domains. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "post", + "uri": "/v1/{parent=apps/*}/authorizedCertificates", + "body": "certificate", + }, + ] + request, metadata = self._interceptor.pre_create_authorized_certificate( + request, metadata + ) + pb_request = appengine.CreateAuthorizedCertificateRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + # Jsonify the request body + + body = json_format.MessageToJson( + transcoded_request["body"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = certificate.AuthorizedCertificate() + pb_resp = certificate.AuthorizedCertificate.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_create_authorized_certificate(resp) + return resp + + class _DeleteAuthorizedCertificate(AuthorizedCertificatesRestStub): + def __hash__(self): + return hash("DeleteAuthorizedCertificate") + + def __call__( + self, + request: appengine.DeleteAuthorizedCertificateRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ): + r"""Call the delete authorized + certificate method over HTTP. + + Args: + request (~.appengine.DeleteAuthorizedCertificateRequest): + The request object. Request message for + ``AuthorizedCertificates.DeleteAuthorizedCertificate``. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "delete", + "uri": "/v1/{name=apps/*/authorizedCertificates/*}", + }, + ] + request, metadata = self._interceptor.pre_delete_authorized_certificate( + request, metadata + ) + pb_request = appengine.DeleteAuthorizedCertificateRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + class _GetAuthorizedCertificate(AuthorizedCertificatesRestStub): + def __hash__(self): + return hash("GetAuthorizedCertificate") + + def __call__( + self, + request: appengine.GetAuthorizedCertificateRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> certificate.AuthorizedCertificate: + r"""Call the get authorized + certificate method over HTTP. + + Args: + request (~.appengine.GetAuthorizedCertificateRequest): + The request object. Request message for + ``AuthorizedCertificates.GetAuthorizedCertificate``. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.certificate.AuthorizedCertificate: + An SSL certificate that a user has + been authorized to administer. A user is + authorized to administer any certificate + that applies to one of their authorized + domains. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "get", + "uri": "/v1/{name=apps/*/authorizedCertificates/*}", + }, + ] + request, metadata = self._interceptor.pre_get_authorized_certificate( + request, metadata + ) + pb_request = appengine.GetAuthorizedCertificateRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = certificate.AuthorizedCertificate() + pb_resp = certificate.AuthorizedCertificate.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_get_authorized_certificate(resp) + return resp + + class _ListAuthorizedCertificates(AuthorizedCertificatesRestStub): + def __hash__(self): + return hash("ListAuthorizedCertificates") + + def __call__( + self, + request: appengine.ListAuthorizedCertificatesRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> appengine.ListAuthorizedCertificatesResponse: + r"""Call the list authorized + certificates method over HTTP. + + Args: + request (~.appengine.ListAuthorizedCertificatesRequest): + The request object. Request message for + ``AuthorizedCertificates.ListAuthorizedCertificates``. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.appengine.ListAuthorizedCertificatesResponse: + Response message for + ``AuthorizedCertificates.ListAuthorizedCertificates``. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "get", + "uri": "/v1/{parent=apps/*}/authorizedCertificates", + }, + ] + request, metadata = self._interceptor.pre_list_authorized_certificates( + request, metadata + ) + pb_request = appengine.ListAuthorizedCertificatesRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = appengine.ListAuthorizedCertificatesResponse() + pb_resp = appengine.ListAuthorizedCertificatesResponse.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_list_authorized_certificates(resp) + return resp + + class _UpdateAuthorizedCertificate(AuthorizedCertificatesRestStub): + def __hash__(self): + return hash("UpdateAuthorizedCertificate") + + def __call__( + self, + request: appengine.UpdateAuthorizedCertificateRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> certificate.AuthorizedCertificate: + r"""Call the update authorized + certificate method over HTTP. + + Args: + request (~.appengine.UpdateAuthorizedCertificateRequest): + The request object. Request message for + ``AuthorizedCertificates.UpdateAuthorizedCertificate``. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.certificate.AuthorizedCertificate: + An SSL certificate that a user has + been authorized to administer. A user is + authorized to administer any certificate + that applies to one of their authorized + domains. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "patch", + "uri": "/v1/{name=apps/*/authorizedCertificates/*}", + "body": "certificate", + }, + ] + request, metadata = self._interceptor.pre_update_authorized_certificate( + request, metadata + ) + pb_request = appengine.UpdateAuthorizedCertificateRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + # Jsonify the request body + + body = json_format.MessageToJson( + transcoded_request["body"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = certificate.AuthorizedCertificate() + pb_resp = certificate.AuthorizedCertificate.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_update_authorized_certificate(resp) + return resp + + @property + def create_authorized_certificate( + self, + ) -> Callable[ + [appengine.CreateAuthorizedCertificateRequest], + certificate.AuthorizedCertificate, + ]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._CreateAuthorizedCertificate(self._session, self._host, self._interceptor) # type: ignore + + @property + def delete_authorized_certificate( + self, + ) -> Callable[[appengine.DeleteAuthorizedCertificateRequest], empty_pb2.Empty]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._DeleteAuthorizedCertificate(self._session, self._host, self._interceptor) # type: ignore + + @property + def get_authorized_certificate( + self, + ) -> Callable[ + [appengine.GetAuthorizedCertificateRequest], certificate.AuthorizedCertificate + ]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._GetAuthorizedCertificate(self._session, self._host, self._interceptor) # type: ignore + + @property + def list_authorized_certificates( + self, + ) -> Callable[ + [appengine.ListAuthorizedCertificatesRequest], + appengine.ListAuthorizedCertificatesResponse, + ]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._ListAuthorizedCertificates(self._session, self._host, self._interceptor) # type: ignore + + @property + def update_authorized_certificate( + self, + ) -> Callable[ + [appengine.UpdateAuthorizedCertificateRequest], + certificate.AuthorizedCertificate, + ]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._UpdateAuthorizedCertificate(self._session, self._host, self._interceptor) # type: ignore + + @property + def kind(self) -> str: + return "rest" + + def close(self): + self._session.close() + + +__all__ = ("AuthorizedCertificatesRestTransport",) diff --git a/google/cloud/appengine_admin_v1/services/authorized_domains/client.py b/google/cloud/appengine_admin_v1/services/authorized_domains/client.py index d4d8c57..3a6b4ed 100644 --- a/google/cloud/appengine_admin_v1/services/authorized_domains/client.py +++ b/google/cloud/appengine_admin_v1/services/authorized_domains/client.py @@ -52,6 +52,7 @@ from .transports.base import DEFAULT_CLIENT_INFO, AuthorizedDomainsTransport from .transports.grpc import AuthorizedDomainsGrpcTransport from .transports.grpc_asyncio import AuthorizedDomainsGrpcAsyncIOTransport +from .transports.rest import AuthorizedDomainsRestTransport class AuthorizedDomainsClientMeta(type): @@ -67,6 +68,7 @@ class AuthorizedDomainsClientMeta(type): ) # type: Dict[str, Type[AuthorizedDomainsTransport]] _transport_registry["grpc"] = AuthorizedDomainsGrpcTransport _transport_registry["grpc_asyncio"] = AuthorizedDomainsGrpcAsyncIOTransport + _transport_registry["rest"] = AuthorizedDomainsRestTransport def get_transport_class( cls, diff --git a/google/cloud/appengine_admin_v1/services/authorized_domains/transports/__init__.py b/google/cloud/appengine_admin_v1/services/authorized_domains/transports/__init__.py index 5938ca9..f2fffb0 100644 --- a/google/cloud/appengine_admin_v1/services/authorized_domains/transports/__init__.py +++ b/google/cloud/appengine_admin_v1/services/authorized_domains/transports/__init__.py @@ -19,14 +19,18 @@ from .base import AuthorizedDomainsTransport from .grpc import AuthorizedDomainsGrpcTransport from .grpc_asyncio import AuthorizedDomainsGrpcAsyncIOTransport +from .rest import AuthorizedDomainsRestInterceptor, AuthorizedDomainsRestTransport # Compile a registry of transports. _transport_registry = OrderedDict() # type: Dict[str, Type[AuthorizedDomainsTransport]] _transport_registry["grpc"] = AuthorizedDomainsGrpcTransport _transport_registry["grpc_asyncio"] = AuthorizedDomainsGrpcAsyncIOTransport +_transport_registry["rest"] = AuthorizedDomainsRestTransport __all__ = ( "AuthorizedDomainsTransport", "AuthorizedDomainsGrpcTransport", "AuthorizedDomainsGrpcAsyncIOTransport", + "AuthorizedDomainsRestTransport", + "AuthorizedDomainsRestInterceptor", ) diff --git a/google/cloud/appengine_admin_v1/services/authorized_domains/transports/rest.py b/google/cloud/appengine_admin_v1/services/authorized_domains/transports/rest.py new file mode 100644 index 0000000..d08506a --- /dev/null +++ b/google/cloud/appengine_admin_v1/services/authorized_domains/transports/rest.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import dataclasses +import json # type: ignore +import re +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +import warnings + +from google.api_core import gapic_v1, path_template, rest_helpers, rest_streaming +from google.api_core import exceptions as core_exceptions +from google.api_core import retry as retries +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.auth.transport.requests import AuthorizedSession # type: ignore +from google.protobuf import json_format +import grpc # type: ignore +from requests import __version__ as requests_version + +try: + OptionalRetry = Union[retries.Retry, gapic_v1.method._MethodDefault] +except AttributeError: # pragma: NO COVER + OptionalRetry = Union[retries.Retry, object] # type: ignore + + +from google.cloud.appengine_admin_v1.types import appengine + +from .base import AuthorizedDomainsTransport +from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, + grpc_version=None, + rest_version=requests_version, +) + + +class AuthorizedDomainsRestInterceptor: + """Interceptor for AuthorizedDomains. + + Interceptors are used to manipulate requests, request metadata, and responses + in arbitrary ways. + Example use cases include: + * Logging + * Verifying requests according to service or custom semantics + * Stripping extraneous information from responses + + These use cases and more can be enabled by injecting an + instance of a custom subclass when constructing the AuthorizedDomainsRestTransport. + + .. code-block:: python + class MyCustomAuthorizedDomainsInterceptor(AuthorizedDomainsRestInterceptor): + def pre_list_authorized_domains(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_list_authorized_domains(self, response): + logging.log(f"Received response: {response}") + return response + + transport = AuthorizedDomainsRestTransport(interceptor=MyCustomAuthorizedDomainsInterceptor()) + client = AuthorizedDomainsClient(transport=transport) + + + """ + + def pre_list_authorized_domains( + self, + request: appengine.ListAuthorizedDomainsRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.ListAuthorizedDomainsRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for list_authorized_domains + + Override in a subclass to manipulate the request or metadata + before they are sent to the AuthorizedDomains server. + """ + return request, metadata + + def post_list_authorized_domains( + self, response: appengine.ListAuthorizedDomainsResponse + ) -> appengine.ListAuthorizedDomainsResponse: + """Post-rpc interceptor for list_authorized_domains + + Override in a subclass to manipulate the response + after it is returned by the AuthorizedDomains server but before + it is returned to user code. + """ + return response + + +@dataclasses.dataclass +class AuthorizedDomainsRestStub: + _session: AuthorizedSession + _host: str + _interceptor: AuthorizedDomainsRestInterceptor + + +class AuthorizedDomainsRestTransport(AuthorizedDomainsTransport): + """REST backend transport for AuthorizedDomains. + + Manages domains a user is authorized to administer. To authorize use + of a domain, verify ownership via `Webmaster + Central `__. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends JSON representations of protocol buffers over HTTP/1.1 + + """ + + def __init__( + self, + *, + host: str = "appengine.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + client_cert_source_for_mtls: Optional[Callable[[], Tuple[bytes, bytes]]] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + url_scheme: str = "https", + interceptor: Optional[AuthorizedDomainsRestInterceptor] = None, + api_audience: Optional[str] = None, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to. + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + 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 + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. + 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 + certificate to configure mutual TLS HTTP channel. It is ignored + if ``channel`` is provided. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you are developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + url_scheme: the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. + """ + # Run the base constructor + # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. + # TODO: When custom host (api_endpoint) is set, `scopes` must *also* be set on the + # credentials object + maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) + if maybe_url_match is None: + raise ValueError( + f"Unexpected hostname structure: {host}" + ) # pragma: NO COVER + + url_match_items = maybe_url_match.groupdict() + + host = f"{url_scheme}://{host}" if not url_match_items["scheme"] else host + + super().__init__( + host=host, + credentials=credentials, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + api_audience=api_audience, + ) + self._session = AuthorizedSession( + self._credentials, default_host=self.DEFAULT_HOST + ) + if client_cert_source_for_mtls: + self._session.configure_mtls_channel(client_cert_source_for_mtls) + self._interceptor = interceptor or AuthorizedDomainsRestInterceptor() + self._prep_wrapped_messages(client_info) + + class _ListAuthorizedDomains(AuthorizedDomainsRestStub): + def __hash__(self): + return hash("ListAuthorizedDomains") + + def __call__( + self, + request: appengine.ListAuthorizedDomainsRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> appengine.ListAuthorizedDomainsResponse: + r"""Call the list authorized domains method over HTTP. + + Args: + request (~.appengine.ListAuthorizedDomainsRequest): + The request object. Request message for + ``AuthorizedDomains.ListAuthorizedDomains``. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.appengine.ListAuthorizedDomainsResponse: + Response message for + ``AuthorizedDomains.ListAuthorizedDomains``. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "get", + "uri": "/v1/{parent=apps/*}/authorizedDomains", + }, + ] + request, metadata = self._interceptor.pre_list_authorized_domains( + request, metadata + ) + pb_request = appengine.ListAuthorizedDomainsRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = appengine.ListAuthorizedDomainsResponse() + pb_resp = appengine.ListAuthorizedDomainsResponse.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_list_authorized_domains(resp) + return resp + + @property + def list_authorized_domains( + self, + ) -> Callable[ + [appengine.ListAuthorizedDomainsRequest], + appengine.ListAuthorizedDomainsResponse, + ]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._ListAuthorizedDomains(self._session, self._host, self._interceptor) # type: ignore + + @property + def kind(self) -> str: + return "rest" + + def close(self): + self._session.close() + + +__all__ = ("AuthorizedDomainsRestTransport",) diff --git a/google/cloud/appengine_admin_v1/services/domain_mappings/client.py b/google/cloud/appengine_admin_v1/services/domain_mappings/client.py index a96a7a6..d2a2e17 100644 --- a/google/cloud/appengine_admin_v1/services/domain_mappings/client.py +++ b/google/cloud/appengine_admin_v1/services/domain_mappings/client.py @@ -57,6 +57,7 @@ from .transports.base import DEFAULT_CLIENT_INFO, DomainMappingsTransport from .transports.grpc import DomainMappingsGrpcTransport from .transports.grpc_asyncio import DomainMappingsGrpcAsyncIOTransport +from .transports.rest import DomainMappingsRestTransport class DomainMappingsClientMeta(type): @@ -72,6 +73,7 @@ class DomainMappingsClientMeta(type): ) # type: Dict[str, Type[DomainMappingsTransport]] _transport_registry["grpc"] = DomainMappingsGrpcTransport _transport_registry["grpc_asyncio"] = DomainMappingsGrpcAsyncIOTransport + _transport_registry["rest"] = DomainMappingsRestTransport def get_transport_class( cls, diff --git a/google/cloud/appengine_admin_v1/services/domain_mappings/transports/__init__.py b/google/cloud/appengine_admin_v1/services/domain_mappings/transports/__init__.py index 1883c69..a17f0ae 100644 --- a/google/cloud/appengine_admin_v1/services/domain_mappings/transports/__init__.py +++ b/google/cloud/appengine_admin_v1/services/domain_mappings/transports/__init__.py @@ -19,14 +19,18 @@ from .base import DomainMappingsTransport from .grpc import DomainMappingsGrpcTransport from .grpc_asyncio import DomainMappingsGrpcAsyncIOTransport +from .rest import DomainMappingsRestInterceptor, DomainMappingsRestTransport # Compile a registry of transports. _transport_registry = OrderedDict() # type: Dict[str, Type[DomainMappingsTransport]] _transport_registry["grpc"] = DomainMappingsGrpcTransport _transport_registry["grpc_asyncio"] = DomainMappingsGrpcAsyncIOTransport +_transport_registry["rest"] = DomainMappingsRestTransport __all__ = ( "DomainMappingsTransport", "DomainMappingsGrpcTransport", "DomainMappingsGrpcAsyncIOTransport", + "DomainMappingsRestTransport", + "DomainMappingsRestInterceptor", ) diff --git a/google/cloud/appengine_admin_v1/services/domain_mappings/transports/rest.py b/google/cloud/appengine_admin_v1/services/domain_mappings/transports/rest.py new file mode 100644 index 0000000..544e4fb --- /dev/null +++ b/google/cloud/appengine_admin_v1/services/domain_mappings/transports/rest.py @@ -0,0 +1,841 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import dataclasses +import json # type: ignore +import re +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +import warnings + +from google.api_core import ( + gapic_v1, + operations_v1, + path_template, + rest_helpers, + rest_streaming, +) +from google.api_core import exceptions as core_exceptions +from google.api_core import retry as retries +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.auth.transport.requests import AuthorizedSession # type: ignore +from google.protobuf import json_format +import grpc # type: ignore +from requests import __version__ as requests_version + +try: + OptionalRetry = Union[retries.Retry, gapic_v1.method._MethodDefault] +except AttributeError: # pragma: NO COVER + OptionalRetry = Union[retries.Retry, object] # type: ignore + + +from google.longrunning import operations_pb2 # type: ignore + +from google.cloud.appengine_admin_v1.types import appengine, domain_mapping + +from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO +from .base import DomainMappingsTransport + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, + grpc_version=None, + rest_version=requests_version, +) + + +class DomainMappingsRestInterceptor: + """Interceptor for DomainMappings. + + Interceptors are used to manipulate requests, request metadata, and responses + in arbitrary ways. + Example use cases include: + * Logging + * Verifying requests according to service or custom semantics + * Stripping extraneous information from responses + + These use cases and more can be enabled by injecting an + instance of a custom subclass when constructing the DomainMappingsRestTransport. + + .. code-block:: python + class MyCustomDomainMappingsInterceptor(DomainMappingsRestInterceptor): + def pre_create_domain_mapping(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_create_domain_mapping(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_delete_domain_mapping(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_delete_domain_mapping(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_get_domain_mapping(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_get_domain_mapping(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_list_domain_mappings(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_list_domain_mappings(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_update_domain_mapping(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_update_domain_mapping(self, response): + logging.log(f"Received response: {response}") + return response + + transport = DomainMappingsRestTransport(interceptor=MyCustomDomainMappingsInterceptor()) + client = DomainMappingsClient(transport=transport) + + + """ + + def pre_create_domain_mapping( + self, + request: appengine.CreateDomainMappingRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.CreateDomainMappingRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for create_domain_mapping + + Override in a subclass to manipulate the request or metadata + before they are sent to the DomainMappings server. + """ + return request, metadata + + def post_create_domain_mapping( + self, response: operations_pb2.Operation + ) -> operations_pb2.Operation: + """Post-rpc interceptor for create_domain_mapping + + Override in a subclass to manipulate the response + after it is returned by the DomainMappings server but before + it is returned to user code. + """ + return response + + def pre_delete_domain_mapping( + self, + request: appengine.DeleteDomainMappingRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.DeleteDomainMappingRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for delete_domain_mapping + + Override in a subclass to manipulate the request or metadata + before they are sent to the DomainMappings server. + """ + return request, metadata + + def post_delete_domain_mapping( + self, response: operations_pb2.Operation + ) -> operations_pb2.Operation: + """Post-rpc interceptor for delete_domain_mapping + + Override in a subclass to manipulate the response + after it is returned by the DomainMappings server but before + it is returned to user code. + """ + return response + + def pre_get_domain_mapping( + self, + request: appengine.GetDomainMappingRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.GetDomainMappingRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for get_domain_mapping + + Override in a subclass to manipulate the request or metadata + before they are sent to the DomainMappings server. + """ + return request, metadata + + def post_get_domain_mapping( + self, response: domain_mapping.DomainMapping + ) -> domain_mapping.DomainMapping: + """Post-rpc interceptor for get_domain_mapping + + Override in a subclass to manipulate the response + after it is returned by the DomainMappings server but before + it is returned to user code. + """ + return response + + def pre_list_domain_mappings( + self, + request: appengine.ListDomainMappingsRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.ListDomainMappingsRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for list_domain_mappings + + Override in a subclass to manipulate the request or metadata + before they are sent to the DomainMappings server. + """ + return request, metadata + + def post_list_domain_mappings( + self, response: appengine.ListDomainMappingsResponse + ) -> appengine.ListDomainMappingsResponse: + """Post-rpc interceptor for list_domain_mappings + + Override in a subclass to manipulate the response + after it is returned by the DomainMappings server but before + it is returned to user code. + """ + return response + + def pre_update_domain_mapping( + self, + request: appengine.UpdateDomainMappingRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.UpdateDomainMappingRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for update_domain_mapping + + Override in a subclass to manipulate the request or metadata + before they are sent to the DomainMappings server. + """ + return request, metadata + + def post_update_domain_mapping( + self, response: operations_pb2.Operation + ) -> operations_pb2.Operation: + """Post-rpc interceptor for update_domain_mapping + + Override in a subclass to manipulate the response + after it is returned by the DomainMappings server but before + it is returned to user code. + """ + return response + + +@dataclasses.dataclass +class DomainMappingsRestStub: + _session: AuthorizedSession + _host: str + _interceptor: DomainMappingsRestInterceptor + + +class DomainMappingsRestTransport(DomainMappingsTransport): + """REST backend transport for DomainMappings. + + Manages domains serving an application. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends JSON representations of protocol buffers over HTTP/1.1 + + """ + + def __init__( + self, + *, + host: str = "appengine.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + client_cert_source_for_mtls: Optional[Callable[[], Tuple[bytes, bytes]]] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + url_scheme: str = "https", + interceptor: Optional[DomainMappingsRestInterceptor] = None, + api_audience: Optional[str] = None, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to. + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + 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 + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. + 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 + certificate to configure mutual TLS HTTP channel. It is ignored + if ``channel`` is provided. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you are developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + url_scheme: the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. + """ + # Run the base constructor + # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. + # TODO: When custom host (api_endpoint) is set, `scopes` must *also* be set on the + # credentials object + maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) + if maybe_url_match is None: + raise ValueError( + f"Unexpected hostname structure: {host}" + ) # pragma: NO COVER + + url_match_items = maybe_url_match.groupdict() + + host = f"{url_scheme}://{host}" if not url_match_items["scheme"] else host + + super().__init__( + host=host, + credentials=credentials, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + api_audience=api_audience, + ) + self._session = AuthorizedSession( + self._credentials, default_host=self.DEFAULT_HOST + ) + self._operations_client: Optional[operations_v1.AbstractOperationsClient] = None + if client_cert_source_for_mtls: + self._session.configure_mtls_channel(client_cert_source_for_mtls) + self._interceptor = interceptor or DomainMappingsRestInterceptor() + self._prep_wrapped_messages(client_info) + + @property + def operations_client(self) -> operations_v1.AbstractOperationsClient: + """Create the client designed to process long-running operations. + + This property caches on the instance; repeated calls return the same + client. + """ + # Only create a new client if we do not already have one. + if self._operations_client is None: + http_options: Dict[str, List[Dict[str, str]]] = { + "google.longrunning.Operations.GetOperation": [ + { + "method": "get", + "uri": "/v1/{name=apps/*/operations/*}", + }, + ], + "google.longrunning.Operations.ListOperations": [ + { + "method": "get", + "uri": "/v1/{name=apps/*}/operations", + }, + ], + } + + rest_transport = operations_v1.OperationsRestTransport( + host=self._host, + # use the credentials which are saved + credentials=self._credentials, + scopes=self._scopes, + http_options=http_options, + path_prefix="v1", + ) + + self._operations_client = operations_v1.AbstractOperationsClient( + transport=rest_transport + ) + + # Return the client from cache. + return self._operations_client + + class _CreateDomainMapping(DomainMappingsRestStub): + def __hash__(self): + return hash("CreateDomainMapping") + + def __call__( + self, + request: appengine.CreateDomainMappingRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Call the create domain mapping method over HTTP. + + Args: + request (~.appengine.CreateDomainMappingRequest): + The request object. Request message for + ``DomainMappings.CreateDomainMapping``. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.Operation: + This resource represents a + long-running operation that is the + result of a network API call. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "post", + "uri": "/v1/{parent=apps/*}/domainMappings", + "body": "domain_mapping", + }, + ] + request, metadata = self._interceptor.pre_create_domain_mapping( + request, metadata + ) + pb_request = appengine.CreateDomainMappingRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + # Jsonify the request body + + body = json_format.MessageToJson( + transcoded_request["body"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = operations_pb2.Operation() + json_format.Parse(response.content, resp, ignore_unknown_fields=True) + resp = self._interceptor.post_create_domain_mapping(resp) + return resp + + class _DeleteDomainMapping(DomainMappingsRestStub): + def __hash__(self): + return hash("DeleteDomainMapping") + + def __call__( + self, + request: appengine.DeleteDomainMappingRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Call the delete domain mapping method over HTTP. + + Args: + request (~.appengine.DeleteDomainMappingRequest): + The request object. Request message for + ``DomainMappings.DeleteDomainMapping``. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.Operation: + This resource represents a + long-running operation that is the + result of a network API call. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "delete", + "uri": "/v1/{name=apps/*/domainMappings/*}", + }, + ] + request, metadata = self._interceptor.pre_delete_domain_mapping( + request, metadata + ) + pb_request = appengine.DeleteDomainMappingRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = operations_pb2.Operation() + json_format.Parse(response.content, resp, ignore_unknown_fields=True) + resp = self._interceptor.post_delete_domain_mapping(resp) + return resp + + class _GetDomainMapping(DomainMappingsRestStub): + def __hash__(self): + return hash("GetDomainMapping") + + def __call__( + self, + request: appengine.GetDomainMappingRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> domain_mapping.DomainMapping: + r"""Call the get domain mapping method over HTTP. + + Args: + request (~.appengine.GetDomainMappingRequest): + The request object. Request message for ``DomainMappings.GetDomainMapping``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.domain_mapping.DomainMapping: + A domain serving an App Engine + application. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "get", + "uri": "/v1/{name=apps/*/domainMappings/*}", + }, + ] + request, metadata = self._interceptor.pre_get_domain_mapping( + request, metadata + ) + pb_request = appengine.GetDomainMappingRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = domain_mapping.DomainMapping() + pb_resp = domain_mapping.DomainMapping.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_get_domain_mapping(resp) + return resp + + class _ListDomainMappings(DomainMappingsRestStub): + def __hash__(self): + return hash("ListDomainMappings") + + def __call__( + self, + request: appengine.ListDomainMappingsRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> appengine.ListDomainMappingsResponse: + r"""Call the list domain mappings method over HTTP. + + Args: + request (~.appengine.ListDomainMappingsRequest): + The request object. Request message for + ``DomainMappings.ListDomainMappings``. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.appengine.ListDomainMappingsResponse: + Response message for + ``DomainMappings.ListDomainMappings``. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "get", + "uri": "/v1/{parent=apps/*}/domainMappings", + }, + ] + request, metadata = self._interceptor.pre_list_domain_mappings( + request, metadata + ) + pb_request = appengine.ListDomainMappingsRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = appengine.ListDomainMappingsResponse() + pb_resp = appengine.ListDomainMappingsResponse.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_list_domain_mappings(resp) + return resp + + class _UpdateDomainMapping(DomainMappingsRestStub): + def __hash__(self): + return hash("UpdateDomainMapping") + + def __call__( + self, + request: appengine.UpdateDomainMappingRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Call the update domain mapping method over HTTP. + + Args: + request (~.appengine.UpdateDomainMappingRequest): + The request object. Request message for + ``DomainMappings.UpdateDomainMapping``. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.Operation: + This resource represents a + long-running operation that is the + result of a network API call. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "patch", + "uri": "/v1/{name=apps/*/domainMappings/*}", + "body": "domain_mapping", + }, + ] + request, metadata = self._interceptor.pre_update_domain_mapping( + request, metadata + ) + pb_request = appengine.UpdateDomainMappingRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + # Jsonify the request body + + body = json_format.MessageToJson( + transcoded_request["body"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = operations_pb2.Operation() + json_format.Parse(response.content, resp, ignore_unknown_fields=True) + resp = self._interceptor.post_update_domain_mapping(resp) + return resp + + @property + def create_domain_mapping( + self, + ) -> Callable[[appengine.CreateDomainMappingRequest], operations_pb2.Operation]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._CreateDomainMapping(self._session, self._host, self._interceptor) # type: ignore + + @property + def delete_domain_mapping( + self, + ) -> Callable[[appengine.DeleteDomainMappingRequest], operations_pb2.Operation]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._DeleteDomainMapping(self._session, self._host, self._interceptor) # type: ignore + + @property + def get_domain_mapping( + self, + ) -> Callable[[appengine.GetDomainMappingRequest], domain_mapping.DomainMapping]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._GetDomainMapping(self._session, self._host, self._interceptor) # type: ignore + + @property + def list_domain_mappings( + self, + ) -> Callable[ + [appengine.ListDomainMappingsRequest], appengine.ListDomainMappingsResponse + ]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._ListDomainMappings(self._session, self._host, self._interceptor) # type: ignore + + @property + def update_domain_mapping( + self, + ) -> Callable[[appengine.UpdateDomainMappingRequest], operations_pb2.Operation]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._UpdateDomainMapping(self._session, self._host, self._interceptor) # type: ignore + + @property + def kind(self) -> str: + return "rest" + + def close(self): + self._session.close() + + +__all__ = ("DomainMappingsRestTransport",) diff --git a/google/cloud/appengine_admin_v1/services/firewall/client.py b/google/cloud/appengine_admin_v1/services/firewall/client.py index 2788d82..2bc056a 100644 --- a/google/cloud/appengine_admin_v1/services/firewall/client.py +++ b/google/cloud/appengine_admin_v1/services/firewall/client.py @@ -52,6 +52,7 @@ from .transports.base import DEFAULT_CLIENT_INFO, FirewallTransport from .transports.grpc import FirewallGrpcTransport from .transports.grpc_asyncio import FirewallGrpcAsyncIOTransport +from .transports.rest import FirewallRestTransport class FirewallClientMeta(type): @@ -65,6 +66,7 @@ class FirewallClientMeta(type): _transport_registry = OrderedDict() # type: Dict[str, Type[FirewallTransport]] _transport_registry["grpc"] = FirewallGrpcTransport _transport_registry["grpc_asyncio"] = FirewallGrpcAsyncIOTransport + _transport_registry["rest"] = FirewallRestTransport def get_transport_class( cls, diff --git a/google/cloud/appengine_admin_v1/services/firewall/transports/__init__.py b/google/cloud/appengine_admin_v1/services/firewall/transports/__init__.py index 382119f..fa9b990 100644 --- a/google/cloud/appengine_admin_v1/services/firewall/transports/__init__.py +++ b/google/cloud/appengine_admin_v1/services/firewall/transports/__init__.py @@ -19,14 +19,18 @@ from .base import FirewallTransport from .grpc import FirewallGrpcTransport from .grpc_asyncio import FirewallGrpcAsyncIOTransport +from .rest import FirewallRestInterceptor, FirewallRestTransport # Compile a registry of transports. _transport_registry = OrderedDict() # type: Dict[str, Type[FirewallTransport]] _transport_registry["grpc"] = FirewallGrpcTransport _transport_registry["grpc_asyncio"] = FirewallGrpcAsyncIOTransport +_transport_registry["rest"] = FirewallRestTransport __all__ = ( "FirewallTransport", "FirewallGrpcTransport", "FirewallGrpcAsyncIOTransport", + "FirewallRestTransport", + "FirewallRestInterceptor", ) diff --git a/google/cloud/appengine_admin_v1/services/firewall/transports/rest.py b/google/cloud/appengine_admin_v1/services/firewall/transports/rest.py new file mode 100644 index 0000000..047c980 --- /dev/null +++ b/google/cloud/appengine_admin_v1/services/firewall/transports/rest.py @@ -0,0 +1,905 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import dataclasses +import json # type: ignore +import re +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +import warnings + +from google.api_core import gapic_v1, path_template, rest_helpers, rest_streaming +from google.api_core import exceptions as core_exceptions +from google.api_core import retry as retries +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.auth.transport.requests import AuthorizedSession # type: ignore +from google.protobuf import json_format +import grpc # type: ignore +from requests import __version__ as requests_version + +try: + OptionalRetry = Union[retries.Retry, gapic_v1.method._MethodDefault] +except AttributeError: # pragma: NO COVER + OptionalRetry = Union[retries.Retry, object] # type: ignore + + +from google.protobuf import empty_pb2 # type: ignore + +from google.cloud.appengine_admin_v1.types import appengine, firewall + +from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO +from .base import FirewallTransport + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, + grpc_version=None, + rest_version=requests_version, +) + + +class FirewallRestInterceptor: + """Interceptor for Firewall. + + Interceptors are used to manipulate requests, request metadata, and responses + in arbitrary ways. + Example use cases include: + * Logging + * Verifying requests according to service or custom semantics + * Stripping extraneous information from responses + + These use cases and more can be enabled by injecting an + instance of a custom subclass when constructing the FirewallRestTransport. + + .. code-block:: python + class MyCustomFirewallInterceptor(FirewallRestInterceptor): + def pre_batch_update_ingress_rules(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_batch_update_ingress_rules(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_create_ingress_rule(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_create_ingress_rule(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_delete_ingress_rule(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def pre_get_ingress_rule(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_get_ingress_rule(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_list_ingress_rules(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_list_ingress_rules(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_update_ingress_rule(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_update_ingress_rule(self, response): + logging.log(f"Received response: {response}") + return response + + transport = FirewallRestTransport(interceptor=MyCustomFirewallInterceptor()) + client = FirewallClient(transport=transport) + + + """ + + def pre_batch_update_ingress_rules( + self, + request: appengine.BatchUpdateIngressRulesRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.BatchUpdateIngressRulesRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for batch_update_ingress_rules + + Override in a subclass to manipulate the request or metadata + before they are sent to the Firewall server. + """ + return request, metadata + + def post_batch_update_ingress_rules( + self, response: appengine.BatchUpdateIngressRulesResponse + ) -> appengine.BatchUpdateIngressRulesResponse: + """Post-rpc interceptor for batch_update_ingress_rules + + Override in a subclass to manipulate the response + after it is returned by the Firewall server but before + it is returned to user code. + """ + return response + + def pre_create_ingress_rule( + self, + request: appengine.CreateIngressRuleRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.CreateIngressRuleRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for create_ingress_rule + + Override in a subclass to manipulate the request or metadata + before they are sent to the Firewall server. + """ + return request, metadata + + def post_create_ingress_rule( + self, response: firewall.FirewallRule + ) -> firewall.FirewallRule: + """Post-rpc interceptor for create_ingress_rule + + Override in a subclass to manipulate the response + after it is returned by the Firewall server but before + it is returned to user code. + """ + return response + + def pre_delete_ingress_rule( + self, + request: appengine.DeleteIngressRuleRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.DeleteIngressRuleRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for delete_ingress_rule + + Override in a subclass to manipulate the request or metadata + before they are sent to the Firewall server. + """ + return request, metadata + + def pre_get_ingress_rule( + self, + request: appengine.GetIngressRuleRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.GetIngressRuleRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for get_ingress_rule + + Override in a subclass to manipulate the request or metadata + before they are sent to the Firewall server. + """ + return request, metadata + + def post_get_ingress_rule( + self, response: firewall.FirewallRule + ) -> firewall.FirewallRule: + """Post-rpc interceptor for get_ingress_rule + + Override in a subclass to manipulate the response + after it is returned by the Firewall server but before + it is returned to user code. + """ + return response + + def pre_list_ingress_rules( + self, + request: appengine.ListIngressRulesRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.ListIngressRulesRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for list_ingress_rules + + Override in a subclass to manipulate the request or metadata + before they are sent to the Firewall server. + """ + return request, metadata + + def post_list_ingress_rules( + self, response: appengine.ListIngressRulesResponse + ) -> appengine.ListIngressRulesResponse: + """Post-rpc interceptor for list_ingress_rules + + Override in a subclass to manipulate the response + after it is returned by the Firewall server but before + it is returned to user code. + """ + return response + + def pre_update_ingress_rule( + self, + request: appengine.UpdateIngressRuleRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.UpdateIngressRuleRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for update_ingress_rule + + Override in a subclass to manipulate the request or metadata + before they are sent to the Firewall server. + """ + return request, metadata + + def post_update_ingress_rule( + self, response: firewall.FirewallRule + ) -> firewall.FirewallRule: + """Post-rpc interceptor for update_ingress_rule + + Override in a subclass to manipulate the response + after it is returned by the Firewall server but before + it is returned to user code. + """ + return response + + +@dataclasses.dataclass +class FirewallRestStub: + _session: AuthorizedSession + _host: str + _interceptor: FirewallRestInterceptor + + +class FirewallRestTransport(FirewallTransport): + """REST backend transport for Firewall. + + Firewall resources are used to define a collection of access + control rules for an Application. Each rule is defined with a + position which specifies the rule's order in the sequence of + rules, an IP range to be matched against requests, and an action + to take upon matching requests. + Every request is evaluated against the Firewall rules in + priority order. Processesing stops at the first rule which + matches the request's IP address. A final rule always specifies + an action that applies to all remaining IP addresses. The + default final rule for a newly-created application will be set + to "allow" if not otherwise specified by the user. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends JSON representations of protocol buffers over HTTP/1.1 + + """ + + def __init__( + self, + *, + host: str = "appengine.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + client_cert_source_for_mtls: Optional[Callable[[], Tuple[bytes, bytes]]] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + url_scheme: str = "https", + interceptor: Optional[FirewallRestInterceptor] = None, + api_audience: Optional[str] = None, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to. + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + 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 + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. + 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 + certificate to configure mutual TLS HTTP channel. It is ignored + if ``channel`` is provided. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you are developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + url_scheme: the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. + """ + # Run the base constructor + # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. + # TODO: When custom host (api_endpoint) is set, `scopes` must *also* be set on the + # credentials object + maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) + if maybe_url_match is None: + raise ValueError( + f"Unexpected hostname structure: {host}" + ) # pragma: NO COVER + + url_match_items = maybe_url_match.groupdict() + + host = f"{url_scheme}://{host}" if not url_match_items["scheme"] else host + + super().__init__( + host=host, + credentials=credentials, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + api_audience=api_audience, + ) + self._session = AuthorizedSession( + self._credentials, default_host=self.DEFAULT_HOST + ) + if client_cert_source_for_mtls: + self._session.configure_mtls_channel(client_cert_source_for_mtls) + self._interceptor = interceptor or FirewallRestInterceptor() + self._prep_wrapped_messages(client_info) + + class _BatchUpdateIngressRules(FirewallRestStub): + def __hash__(self): + return hash("BatchUpdateIngressRules") + + def __call__( + self, + request: appengine.BatchUpdateIngressRulesRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> appengine.BatchUpdateIngressRulesResponse: + r"""Call the batch update ingress + rules method over HTTP. + + Args: + request (~.appengine.BatchUpdateIngressRulesRequest): + The request object. Request message for + ``Firewall.BatchUpdateIngressRules``. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.appengine.BatchUpdateIngressRulesResponse: + Response message for ``Firewall.UpdateAllIngressRules``. + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "post", + "uri": "/v1/{name=apps/*/firewall/ingressRules}:batchUpdate", + "body": "*", + }, + ] + request, metadata = self._interceptor.pre_batch_update_ingress_rules( + request, metadata + ) + pb_request = appengine.BatchUpdateIngressRulesRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + # Jsonify the request body + + body = json_format.MessageToJson( + transcoded_request["body"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = appengine.BatchUpdateIngressRulesResponse() + pb_resp = appengine.BatchUpdateIngressRulesResponse.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_batch_update_ingress_rules(resp) + return resp + + class _CreateIngressRule(FirewallRestStub): + def __hash__(self): + return hash("CreateIngressRule") + + def __call__( + self, + request: appengine.CreateIngressRuleRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> firewall.FirewallRule: + r"""Call the create ingress rule method over HTTP. + + Args: + request (~.appengine.CreateIngressRuleRequest): + The request object. Request message for ``Firewall.CreateIngressRule``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.firewall.FirewallRule: + A single firewall rule that is + evaluated against incoming traffic and + provides an action to take on matched + requests. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "post", + "uri": "/v1/{parent=apps/*}/firewall/ingressRules", + "body": "rule", + }, + ] + request, metadata = self._interceptor.pre_create_ingress_rule( + request, metadata + ) + pb_request = appengine.CreateIngressRuleRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + # Jsonify the request body + + body = json_format.MessageToJson( + transcoded_request["body"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = firewall.FirewallRule() + pb_resp = firewall.FirewallRule.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_create_ingress_rule(resp) + return resp + + class _DeleteIngressRule(FirewallRestStub): + def __hash__(self): + return hash("DeleteIngressRule") + + def __call__( + self, + request: appengine.DeleteIngressRuleRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ): + r"""Call the delete ingress rule method over HTTP. + + Args: + request (~.appengine.DeleteIngressRuleRequest): + The request object. Request message for ``Firewall.DeleteIngressRule``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "delete", + "uri": "/v1/{name=apps/*/firewall/ingressRules/*}", + }, + ] + request, metadata = self._interceptor.pre_delete_ingress_rule( + request, metadata + ) + pb_request = appengine.DeleteIngressRuleRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + class _GetIngressRule(FirewallRestStub): + def __hash__(self): + return hash("GetIngressRule") + + def __call__( + self, + request: appengine.GetIngressRuleRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> firewall.FirewallRule: + r"""Call the get ingress rule method over HTTP. + + Args: + request (~.appengine.GetIngressRuleRequest): + The request object. Request message for ``Firewall.GetIngressRule``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.firewall.FirewallRule: + A single firewall rule that is + evaluated against incoming traffic and + provides an action to take on matched + requests. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "get", + "uri": "/v1/{name=apps/*/firewall/ingressRules/*}", + }, + ] + request, metadata = self._interceptor.pre_get_ingress_rule( + request, metadata + ) + pb_request = appengine.GetIngressRuleRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = firewall.FirewallRule() + pb_resp = firewall.FirewallRule.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_get_ingress_rule(resp) + return resp + + class _ListIngressRules(FirewallRestStub): + def __hash__(self): + return hash("ListIngressRules") + + def __call__( + self, + request: appengine.ListIngressRulesRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> appengine.ListIngressRulesResponse: + r"""Call the list ingress rules method over HTTP. + + Args: + request (~.appengine.ListIngressRulesRequest): + The request object. Request message for ``Firewall.ListIngressRules``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.appengine.ListIngressRulesResponse: + Response message for ``Firewall.ListIngressRules``. + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "get", + "uri": "/v1/{parent=apps/*}/firewall/ingressRules", + }, + ] + request, metadata = self._interceptor.pre_list_ingress_rules( + request, metadata + ) + pb_request = appengine.ListIngressRulesRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = appengine.ListIngressRulesResponse() + pb_resp = appengine.ListIngressRulesResponse.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_list_ingress_rules(resp) + return resp + + class _UpdateIngressRule(FirewallRestStub): + def __hash__(self): + return hash("UpdateIngressRule") + + def __call__( + self, + request: appengine.UpdateIngressRuleRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> firewall.FirewallRule: + r"""Call the update ingress rule method over HTTP. + + Args: + request (~.appengine.UpdateIngressRuleRequest): + The request object. Request message for ``Firewall.UpdateIngressRule``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.firewall.FirewallRule: + A single firewall rule that is + evaluated against incoming traffic and + provides an action to take on matched + requests. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "patch", + "uri": "/v1/{name=apps/*/firewall/ingressRules/*}", + "body": "rule", + }, + ] + request, metadata = self._interceptor.pre_update_ingress_rule( + request, metadata + ) + pb_request = appengine.UpdateIngressRuleRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + # Jsonify the request body + + body = json_format.MessageToJson( + transcoded_request["body"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = firewall.FirewallRule() + pb_resp = firewall.FirewallRule.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_update_ingress_rule(resp) + return resp + + @property + def batch_update_ingress_rules( + self, + ) -> Callable[ + [appengine.BatchUpdateIngressRulesRequest], + appengine.BatchUpdateIngressRulesResponse, + ]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._BatchUpdateIngressRules(self._session, self._host, self._interceptor) # type: ignore + + @property + def create_ingress_rule( + self, + ) -> Callable[[appengine.CreateIngressRuleRequest], firewall.FirewallRule]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._CreateIngressRule(self._session, self._host, self._interceptor) # type: ignore + + @property + def delete_ingress_rule( + self, + ) -> Callable[[appengine.DeleteIngressRuleRequest], empty_pb2.Empty]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._DeleteIngressRule(self._session, self._host, self._interceptor) # type: ignore + + @property + def get_ingress_rule( + self, + ) -> Callable[[appengine.GetIngressRuleRequest], firewall.FirewallRule]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._GetIngressRule(self._session, self._host, self._interceptor) # type: ignore + + @property + def list_ingress_rules( + self, + ) -> Callable[ + [appengine.ListIngressRulesRequest], appengine.ListIngressRulesResponse + ]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._ListIngressRules(self._session, self._host, self._interceptor) # type: ignore + + @property + def update_ingress_rule( + self, + ) -> Callable[[appengine.UpdateIngressRuleRequest], firewall.FirewallRule]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._UpdateIngressRule(self._session, self._host, self._interceptor) # type: ignore + + @property + def kind(self) -> str: + return "rest" + + def close(self): + self._session.close() + + +__all__ = ("FirewallRestTransport",) diff --git a/google/cloud/appengine_admin_v1/services/instances/client.py b/google/cloud/appengine_admin_v1/services/instances/client.py index ff8afbb..7cc1120 100644 --- a/google/cloud/appengine_admin_v1/services/instances/client.py +++ b/google/cloud/appengine_admin_v1/services/instances/client.py @@ -58,6 +58,7 @@ from .transports.base import DEFAULT_CLIENT_INFO, InstancesTransport from .transports.grpc import InstancesGrpcTransport from .transports.grpc_asyncio import InstancesGrpcAsyncIOTransport +from .transports.rest import InstancesRestTransport class InstancesClientMeta(type): @@ -71,6 +72,7 @@ class InstancesClientMeta(type): _transport_registry = OrderedDict() # type: Dict[str, Type[InstancesTransport]] _transport_registry["grpc"] = InstancesGrpcTransport _transport_registry["grpc_asyncio"] = InstancesGrpcAsyncIOTransport + _transport_registry["rest"] = InstancesRestTransport def get_transport_class( cls, diff --git a/google/cloud/appengine_admin_v1/services/instances/transports/__init__.py b/google/cloud/appengine_admin_v1/services/instances/transports/__init__.py index 5a5096b..eb0bbd8 100644 --- a/google/cloud/appengine_admin_v1/services/instances/transports/__init__.py +++ b/google/cloud/appengine_admin_v1/services/instances/transports/__init__.py @@ -19,14 +19,18 @@ from .base import InstancesTransport from .grpc import InstancesGrpcTransport from .grpc_asyncio import InstancesGrpcAsyncIOTransport +from .rest import InstancesRestInterceptor, InstancesRestTransport # Compile a registry of transports. _transport_registry = OrderedDict() # type: Dict[str, Type[InstancesTransport]] _transport_registry["grpc"] = InstancesGrpcTransport _transport_registry["grpc_asyncio"] = InstancesGrpcAsyncIOTransport +_transport_registry["rest"] = InstancesRestTransport __all__ = ( "InstancesTransport", "InstancesGrpcTransport", "InstancesGrpcAsyncIOTransport", + "InstancesRestTransport", + "InstancesRestInterceptor", ) diff --git a/google/cloud/appengine_admin_v1/services/instances/transports/rest.py b/google/cloud/appengine_admin_v1/services/instances/transports/rest.py new file mode 100644 index 0000000..dd1ecf4 --- /dev/null +++ b/google/cloud/appengine_admin_v1/services/instances/transports/rest.py @@ -0,0 +1,692 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import dataclasses +import json # type: ignore +import re +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +import warnings + +from google.api_core import ( + gapic_v1, + operations_v1, + path_template, + rest_helpers, + rest_streaming, +) +from google.api_core import exceptions as core_exceptions +from google.api_core import retry as retries +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.auth.transport.requests import AuthorizedSession # type: ignore +from google.protobuf import json_format +import grpc # type: ignore +from requests import __version__ as requests_version + +try: + OptionalRetry = Union[retries.Retry, gapic_v1.method._MethodDefault] +except AttributeError: # pragma: NO COVER + OptionalRetry = Union[retries.Retry, object] # type: ignore + + +from google.longrunning import operations_pb2 # type: ignore + +from google.cloud.appengine_admin_v1.types import appengine, instance + +from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO +from .base import InstancesTransport + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, + grpc_version=None, + rest_version=requests_version, +) + + +class InstancesRestInterceptor: + """Interceptor for Instances. + + Interceptors are used to manipulate requests, request metadata, and responses + in arbitrary ways. + Example use cases include: + * Logging + * Verifying requests according to service or custom semantics + * Stripping extraneous information from responses + + These use cases and more can be enabled by injecting an + instance of a custom subclass when constructing the InstancesRestTransport. + + .. code-block:: python + class MyCustomInstancesInterceptor(InstancesRestInterceptor): + def pre_debug_instance(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_debug_instance(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_delete_instance(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_delete_instance(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_get_instance(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_get_instance(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_list_instances(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_list_instances(self, response): + logging.log(f"Received response: {response}") + return response + + transport = InstancesRestTransport(interceptor=MyCustomInstancesInterceptor()) + client = InstancesClient(transport=transport) + + + """ + + def pre_debug_instance( + self, + request: appengine.DebugInstanceRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.DebugInstanceRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for debug_instance + + Override in a subclass to manipulate the request or metadata + before they are sent to the Instances server. + """ + return request, metadata + + def post_debug_instance( + self, response: operations_pb2.Operation + ) -> operations_pb2.Operation: + """Post-rpc interceptor for debug_instance + + Override in a subclass to manipulate the response + after it is returned by the Instances server but before + it is returned to user code. + """ + return response + + def pre_delete_instance( + self, + request: appengine.DeleteInstanceRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.DeleteInstanceRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for delete_instance + + Override in a subclass to manipulate the request or metadata + before they are sent to the Instances server. + """ + return request, metadata + + def post_delete_instance( + self, response: operations_pb2.Operation + ) -> operations_pb2.Operation: + """Post-rpc interceptor for delete_instance + + Override in a subclass to manipulate the response + after it is returned by the Instances server but before + it is returned to user code. + """ + return response + + def pre_get_instance( + self, request: appengine.GetInstanceRequest, metadata: Sequence[Tuple[str, str]] + ) -> Tuple[appengine.GetInstanceRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for get_instance + + Override in a subclass to manipulate the request or metadata + before they are sent to the Instances server. + """ + return request, metadata + + def post_get_instance(self, response: instance.Instance) -> instance.Instance: + """Post-rpc interceptor for get_instance + + Override in a subclass to manipulate the response + after it is returned by the Instances server but before + it is returned to user code. + """ + return response + + def pre_list_instances( + self, + request: appengine.ListInstancesRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.ListInstancesRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for list_instances + + Override in a subclass to manipulate the request or metadata + before they are sent to the Instances server. + """ + return request, metadata + + def post_list_instances( + self, response: appengine.ListInstancesResponse + ) -> appengine.ListInstancesResponse: + """Post-rpc interceptor for list_instances + + Override in a subclass to manipulate the response + after it is returned by the Instances server but before + it is returned to user code. + """ + return response + + +@dataclasses.dataclass +class InstancesRestStub: + _session: AuthorizedSession + _host: str + _interceptor: InstancesRestInterceptor + + +class InstancesRestTransport(InstancesTransport): + """REST backend transport for Instances. + + Manages instances of a version. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends JSON representations of protocol buffers over HTTP/1.1 + + """ + + def __init__( + self, + *, + host: str = "appengine.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + client_cert_source_for_mtls: Optional[Callable[[], Tuple[bytes, bytes]]] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + url_scheme: str = "https", + interceptor: Optional[InstancesRestInterceptor] = None, + api_audience: Optional[str] = None, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to. + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + 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 + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. + 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 + certificate to configure mutual TLS HTTP channel. It is ignored + if ``channel`` is provided. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you are developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + url_scheme: the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. + """ + # Run the base constructor + # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. + # TODO: When custom host (api_endpoint) is set, `scopes` must *also* be set on the + # credentials object + maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) + if maybe_url_match is None: + raise ValueError( + f"Unexpected hostname structure: {host}" + ) # pragma: NO COVER + + url_match_items = maybe_url_match.groupdict() + + host = f"{url_scheme}://{host}" if not url_match_items["scheme"] else host + + super().__init__( + host=host, + credentials=credentials, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + api_audience=api_audience, + ) + self._session = AuthorizedSession( + self._credentials, default_host=self.DEFAULT_HOST + ) + self._operations_client: Optional[operations_v1.AbstractOperationsClient] = None + if client_cert_source_for_mtls: + self._session.configure_mtls_channel(client_cert_source_for_mtls) + self._interceptor = interceptor or InstancesRestInterceptor() + self._prep_wrapped_messages(client_info) + + @property + def operations_client(self) -> operations_v1.AbstractOperationsClient: + """Create the client designed to process long-running operations. + + This property caches on the instance; repeated calls return the same + client. + """ + # Only create a new client if we do not already have one. + if self._operations_client is None: + http_options: Dict[str, List[Dict[str, str]]] = { + "google.longrunning.Operations.GetOperation": [ + { + "method": "get", + "uri": "/v1/{name=apps/*/operations/*}", + }, + ], + "google.longrunning.Operations.ListOperations": [ + { + "method": "get", + "uri": "/v1/{name=apps/*}/operations", + }, + ], + } + + rest_transport = operations_v1.OperationsRestTransport( + host=self._host, + # use the credentials which are saved + credentials=self._credentials, + scopes=self._scopes, + http_options=http_options, + path_prefix="v1", + ) + + self._operations_client = operations_v1.AbstractOperationsClient( + transport=rest_transport + ) + + # Return the client from cache. + return self._operations_client + + class _DebugInstance(InstancesRestStub): + def __hash__(self): + return hash("DebugInstance") + + def __call__( + self, + request: appengine.DebugInstanceRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Call the debug instance method over HTTP. + + Args: + request (~.appengine.DebugInstanceRequest): + The request object. Request message for ``Instances.DebugInstance``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.Operation: + This resource represents a + long-running operation that is the + result of a network API call. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "post", + "uri": "/v1/{name=apps/*/services/*/versions/*/instances/*}:debug", + "body": "*", + }, + ] + request, metadata = self._interceptor.pre_debug_instance(request, metadata) + pb_request = appengine.DebugInstanceRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + # Jsonify the request body + + body = json_format.MessageToJson( + transcoded_request["body"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = operations_pb2.Operation() + json_format.Parse(response.content, resp, ignore_unknown_fields=True) + resp = self._interceptor.post_debug_instance(resp) + return resp + + class _DeleteInstance(InstancesRestStub): + def __hash__(self): + return hash("DeleteInstance") + + def __call__( + self, + request: appengine.DeleteInstanceRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Call the delete instance method over HTTP. + + Args: + request (~.appengine.DeleteInstanceRequest): + The request object. Request message for ``Instances.DeleteInstance``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.Operation: + This resource represents a + long-running operation that is the + result of a network API call. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "delete", + "uri": "/v1/{name=apps/*/services/*/versions/*/instances/*}", + }, + ] + request, metadata = self._interceptor.pre_delete_instance(request, metadata) + pb_request = appengine.DeleteInstanceRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = operations_pb2.Operation() + json_format.Parse(response.content, resp, ignore_unknown_fields=True) + resp = self._interceptor.post_delete_instance(resp) + return resp + + class _GetInstance(InstancesRestStub): + def __hash__(self): + return hash("GetInstance") + + def __call__( + self, + request: appengine.GetInstanceRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> instance.Instance: + r"""Call the get instance method over HTTP. + + Args: + request (~.appengine.GetInstanceRequest): + The request object. Request message for ``Instances.GetInstance``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.instance.Instance: + An Instance resource is the computing + unit that App Engine uses to + automatically scale an application. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "get", + "uri": "/v1/{name=apps/*/services/*/versions/*/instances/*}", + }, + ] + request, metadata = self._interceptor.pre_get_instance(request, metadata) + pb_request = appengine.GetInstanceRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = instance.Instance() + pb_resp = instance.Instance.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_get_instance(resp) + return resp + + class _ListInstances(InstancesRestStub): + def __hash__(self): + return hash("ListInstances") + + def __call__( + self, + request: appengine.ListInstancesRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> appengine.ListInstancesResponse: + r"""Call the list instances method over HTTP. + + Args: + request (~.appengine.ListInstancesRequest): + The request object. Request message for ``Instances.ListInstances``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.appengine.ListInstancesResponse: + Response message for ``Instances.ListInstances``. + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "get", + "uri": "/v1/{parent=apps/*/services/*/versions/*}/instances", + }, + ] + request, metadata = self._interceptor.pre_list_instances(request, metadata) + pb_request = appengine.ListInstancesRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = appengine.ListInstancesResponse() + pb_resp = appengine.ListInstancesResponse.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_list_instances(resp) + return resp + + @property + def debug_instance( + self, + ) -> Callable[[appengine.DebugInstanceRequest], operations_pb2.Operation]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._DebugInstance(self._session, self._host, self._interceptor) # type: ignore + + @property + def delete_instance( + self, + ) -> Callable[[appengine.DeleteInstanceRequest], operations_pb2.Operation]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._DeleteInstance(self._session, self._host, self._interceptor) # type: ignore + + @property + def get_instance( + self, + ) -> Callable[[appengine.GetInstanceRequest], instance.Instance]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._GetInstance(self._session, self._host, self._interceptor) # type: ignore + + @property + def list_instances( + self, + ) -> Callable[[appengine.ListInstancesRequest], appengine.ListInstancesResponse]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._ListInstances(self._session, self._host, self._interceptor) # type: ignore + + @property + def kind(self) -> str: + return "rest" + + def close(self): + self._session.close() + + +__all__ = ("InstancesRestTransport",) diff --git a/google/cloud/appengine_admin_v1/services/services/client.py b/google/cloud/appengine_admin_v1/services/services/client.py index f1bb91a..24562ea 100644 --- a/google/cloud/appengine_admin_v1/services/services/client.py +++ b/google/cloud/appengine_admin_v1/services/services/client.py @@ -58,6 +58,7 @@ from .transports.base import DEFAULT_CLIENT_INFO, ServicesTransport from .transports.grpc import ServicesGrpcTransport from .transports.grpc_asyncio import ServicesGrpcAsyncIOTransport +from .transports.rest import ServicesRestTransport class ServicesClientMeta(type): @@ -71,6 +72,7 @@ class ServicesClientMeta(type): _transport_registry = OrderedDict() # type: Dict[str, Type[ServicesTransport]] _transport_registry["grpc"] = ServicesGrpcTransport _transport_registry["grpc_asyncio"] = ServicesGrpcAsyncIOTransport + _transport_registry["rest"] = ServicesRestTransport def get_transport_class( cls, diff --git a/google/cloud/appengine_admin_v1/services/services/transports/__init__.py b/google/cloud/appengine_admin_v1/services/services/transports/__init__.py index f362184..94ca3b8 100644 --- a/google/cloud/appengine_admin_v1/services/services/transports/__init__.py +++ b/google/cloud/appengine_admin_v1/services/services/transports/__init__.py @@ -19,14 +19,18 @@ from .base import ServicesTransport from .grpc import ServicesGrpcTransport from .grpc_asyncio import ServicesGrpcAsyncIOTransport +from .rest import ServicesRestInterceptor, ServicesRestTransport # Compile a registry of transports. _transport_registry = OrderedDict() # type: Dict[str, Type[ServicesTransport]] _transport_registry["grpc"] = ServicesGrpcTransport _transport_registry["grpc_asyncio"] = ServicesGrpcAsyncIOTransport +_transport_registry["rest"] = ServicesRestTransport __all__ = ( "ServicesTransport", "ServicesGrpcTransport", "ServicesGrpcAsyncIOTransport", + "ServicesRestTransport", + "ServicesRestInterceptor", ) diff --git a/google/cloud/appengine_admin_v1/services/services/transports/rest.py b/google/cloud/appengine_admin_v1/services/services/transports/rest.py new file mode 100644 index 0000000..d627e8a --- /dev/null +++ b/google/cloud/appengine_admin_v1/services/services/transports/rest.py @@ -0,0 +1,699 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import dataclasses +import json # type: ignore +import re +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +import warnings + +from google.api_core import ( + gapic_v1, + operations_v1, + path_template, + rest_helpers, + rest_streaming, +) +from google.api_core import exceptions as core_exceptions +from google.api_core import retry as retries +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.auth.transport.requests import AuthorizedSession # type: ignore +from google.protobuf import json_format +import grpc # type: ignore +from requests import __version__ as requests_version + +try: + OptionalRetry = Union[retries.Retry, gapic_v1.method._MethodDefault] +except AttributeError: # pragma: NO COVER + OptionalRetry = Union[retries.Retry, object] # type: ignore + + +from google.longrunning import operations_pb2 # type: ignore + +from google.cloud.appengine_admin_v1.types import appengine, service + +from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO +from .base import ServicesTransport + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, + grpc_version=None, + rest_version=requests_version, +) + + +class ServicesRestInterceptor: + """Interceptor for Services. + + Interceptors are used to manipulate requests, request metadata, and responses + in arbitrary ways. + Example use cases include: + * Logging + * Verifying requests according to service or custom semantics + * Stripping extraneous information from responses + + These use cases and more can be enabled by injecting an + instance of a custom subclass when constructing the ServicesRestTransport. + + .. code-block:: python + class MyCustomServicesInterceptor(ServicesRestInterceptor): + def pre_delete_service(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_delete_service(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_get_service(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_get_service(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_list_services(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_list_services(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_update_service(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_update_service(self, response): + logging.log(f"Received response: {response}") + return response + + transport = ServicesRestTransport(interceptor=MyCustomServicesInterceptor()) + client = ServicesClient(transport=transport) + + + """ + + def pre_delete_service( + self, + request: appengine.DeleteServiceRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.DeleteServiceRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for delete_service + + Override in a subclass to manipulate the request or metadata + before they are sent to the Services server. + """ + return request, metadata + + def post_delete_service( + self, response: operations_pb2.Operation + ) -> operations_pb2.Operation: + """Post-rpc interceptor for delete_service + + Override in a subclass to manipulate the response + after it is returned by the Services server but before + it is returned to user code. + """ + return response + + def pre_get_service( + self, request: appengine.GetServiceRequest, metadata: Sequence[Tuple[str, str]] + ) -> Tuple[appengine.GetServiceRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for get_service + + Override in a subclass to manipulate the request or metadata + before they are sent to the Services server. + """ + return request, metadata + + def post_get_service(self, response: service.Service) -> service.Service: + """Post-rpc interceptor for get_service + + Override in a subclass to manipulate the response + after it is returned by the Services server but before + it is returned to user code. + """ + return response + + def pre_list_services( + self, + request: appengine.ListServicesRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.ListServicesRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for list_services + + Override in a subclass to manipulate the request or metadata + before they are sent to the Services server. + """ + return request, metadata + + def post_list_services( + self, response: appengine.ListServicesResponse + ) -> appengine.ListServicesResponse: + """Post-rpc interceptor for list_services + + Override in a subclass to manipulate the response + after it is returned by the Services server but before + it is returned to user code. + """ + return response + + def pre_update_service( + self, + request: appengine.UpdateServiceRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.UpdateServiceRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for update_service + + Override in a subclass to manipulate the request or metadata + before they are sent to the Services server. + """ + return request, metadata + + def post_update_service( + self, response: operations_pb2.Operation + ) -> operations_pb2.Operation: + """Post-rpc interceptor for update_service + + Override in a subclass to manipulate the response + after it is returned by the Services server but before + it is returned to user code. + """ + return response + + +@dataclasses.dataclass +class ServicesRestStub: + _session: AuthorizedSession + _host: str + _interceptor: ServicesRestInterceptor + + +class ServicesRestTransport(ServicesTransport): + """REST backend transport for Services. + + Manages services of an application. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends JSON representations of protocol buffers over HTTP/1.1 + + """ + + def __init__( + self, + *, + host: str = "appengine.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + client_cert_source_for_mtls: Optional[Callable[[], Tuple[bytes, bytes]]] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + url_scheme: str = "https", + interceptor: Optional[ServicesRestInterceptor] = None, + api_audience: Optional[str] = None, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to. + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + 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 + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. + 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 + certificate to configure mutual TLS HTTP channel. It is ignored + if ``channel`` is provided. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you are developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + url_scheme: the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. + """ + # Run the base constructor + # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. + # TODO: When custom host (api_endpoint) is set, `scopes` must *also* be set on the + # credentials object + maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) + if maybe_url_match is None: + raise ValueError( + f"Unexpected hostname structure: {host}" + ) # pragma: NO COVER + + url_match_items = maybe_url_match.groupdict() + + host = f"{url_scheme}://{host}" if not url_match_items["scheme"] else host + + super().__init__( + host=host, + credentials=credentials, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + api_audience=api_audience, + ) + self._session = AuthorizedSession( + self._credentials, default_host=self.DEFAULT_HOST + ) + self._operations_client: Optional[operations_v1.AbstractOperationsClient] = None + if client_cert_source_for_mtls: + self._session.configure_mtls_channel(client_cert_source_for_mtls) + self._interceptor = interceptor or ServicesRestInterceptor() + self._prep_wrapped_messages(client_info) + + @property + def operations_client(self) -> operations_v1.AbstractOperationsClient: + """Create the client designed to process long-running operations. + + This property caches on the instance; repeated calls return the same + client. + """ + # Only create a new client if we do not already have one. + if self._operations_client is None: + http_options: Dict[str, List[Dict[str, str]]] = { + "google.longrunning.Operations.GetOperation": [ + { + "method": "get", + "uri": "/v1/{name=apps/*/operations/*}", + }, + ], + "google.longrunning.Operations.ListOperations": [ + { + "method": "get", + "uri": "/v1/{name=apps/*}/operations", + }, + ], + } + + rest_transport = operations_v1.OperationsRestTransport( + host=self._host, + # use the credentials which are saved + credentials=self._credentials, + scopes=self._scopes, + http_options=http_options, + path_prefix="v1", + ) + + self._operations_client = operations_v1.AbstractOperationsClient( + transport=rest_transport + ) + + # Return the client from cache. + return self._operations_client + + class _DeleteService(ServicesRestStub): + def __hash__(self): + return hash("DeleteService") + + def __call__( + self, + request: appengine.DeleteServiceRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Call the delete service method over HTTP. + + Args: + request (~.appengine.DeleteServiceRequest): + The request object. Request message for ``Services.DeleteService``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.Operation: + This resource represents a + long-running operation that is the + result of a network API call. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "delete", + "uri": "/v1/{name=apps/*/services/*}", + }, + ] + request, metadata = self._interceptor.pre_delete_service(request, metadata) + pb_request = appengine.DeleteServiceRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = operations_pb2.Operation() + json_format.Parse(response.content, resp, ignore_unknown_fields=True) + resp = self._interceptor.post_delete_service(resp) + return resp + + class _GetService(ServicesRestStub): + def __hash__(self): + return hash("GetService") + + def __call__( + self, + request: appengine.GetServiceRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> service.Service: + r"""Call the get service method over HTTP. + + Args: + request (~.appengine.GetServiceRequest): + The request object. Request message for ``Services.GetService``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.service.Service: + A Service resource is a logical + component of an application that can + share state and communicate in a secure + fashion with other services. For + example, an application that handles + customer requests might include separate + services to handle tasks such as backend + data analysis or API requests from + mobile devices. Each service has a + collection of versions that define a + specific set of code used to implement + the functionality of that service. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "get", + "uri": "/v1/{name=apps/*/services/*}", + }, + ] + request, metadata = self._interceptor.pre_get_service(request, metadata) + pb_request = appengine.GetServiceRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = service.Service() + pb_resp = service.Service.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_get_service(resp) + return resp + + class _ListServices(ServicesRestStub): + def __hash__(self): + return hash("ListServices") + + def __call__( + self, + request: appengine.ListServicesRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> appengine.ListServicesResponse: + r"""Call the list services method over HTTP. + + Args: + request (~.appengine.ListServicesRequest): + The request object. Request message for ``Services.ListServices``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.appengine.ListServicesResponse: + Response message for ``Services.ListServices``. + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "get", + "uri": "/v1/{parent=apps/*}/services", + }, + ] + request, metadata = self._interceptor.pre_list_services(request, metadata) + pb_request = appengine.ListServicesRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = appengine.ListServicesResponse() + pb_resp = appengine.ListServicesResponse.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_list_services(resp) + return resp + + class _UpdateService(ServicesRestStub): + def __hash__(self): + return hash("UpdateService") + + def __call__( + self, + request: appengine.UpdateServiceRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Call the update service method over HTTP. + + Args: + request (~.appengine.UpdateServiceRequest): + The request object. Request message for ``Services.UpdateService``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.Operation: + This resource represents a + long-running operation that is the + result of a network API call. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "patch", + "uri": "/v1/{name=apps/*/services/*}", + "body": "service", + }, + ] + request, metadata = self._interceptor.pre_update_service(request, metadata) + pb_request = appengine.UpdateServiceRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + # Jsonify the request body + + body = json_format.MessageToJson( + transcoded_request["body"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = operations_pb2.Operation() + json_format.Parse(response.content, resp, ignore_unknown_fields=True) + resp = self._interceptor.post_update_service(resp) + return resp + + @property + def delete_service( + self, + ) -> Callable[[appengine.DeleteServiceRequest], operations_pb2.Operation]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._DeleteService(self._session, self._host, self._interceptor) # type: ignore + + @property + def get_service(self) -> Callable[[appengine.GetServiceRequest], service.Service]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._GetService(self._session, self._host, self._interceptor) # type: ignore + + @property + def list_services( + self, + ) -> Callable[[appengine.ListServicesRequest], appengine.ListServicesResponse]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._ListServices(self._session, self._host, self._interceptor) # type: ignore + + @property + def update_service( + self, + ) -> Callable[[appengine.UpdateServiceRequest], operations_pb2.Operation]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._UpdateService(self._session, self._host, self._interceptor) # type: ignore + + @property + def kind(self) -> str: + return "rest" + + def close(self): + self._session.close() + + +__all__ = ("ServicesRestTransport",) diff --git a/google/cloud/appengine_admin_v1/services/versions/client.py b/google/cloud/appengine_admin_v1/services/versions/client.py index f4280cd..48d71ea 100644 --- a/google/cloud/appengine_admin_v1/services/versions/client.py +++ b/google/cloud/appengine_admin_v1/services/versions/client.py @@ -60,6 +60,7 @@ from .transports.base import DEFAULT_CLIENT_INFO, VersionsTransport from .transports.grpc import VersionsGrpcTransport from .transports.grpc_asyncio import VersionsGrpcAsyncIOTransport +from .transports.rest import VersionsRestTransport class VersionsClientMeta(type): @@ -73,6 +74,7 @@ class VersionsClientMeta(type): _transport_registry = OrderedDict() # type: Dict[str, Type[VersionsTransport]] _transport_registry["grpc"] = VersionsGrpcTransport _transport_registry["grpc_asyncio"] = VersionsGrpcAsyncIOTransport + _transport_registry["rest"] = VersionsRestTransport def get_transport_class( cls, diff --git a/google/cloud/appengine_admin_v1/services/versions/transports/__init__.py b/google/cloud/appengine_admin_v1/services/versions/transports/__init__.py index e31c216..e921ca1 100644 --- a/google/cloud/appengine_admin_v1/services/versions/transports/__init__.py +++ b/google/cloud/appengine_admin_v1/services/versions/transports/__init__.py @@ -19,14 +19,18 @@ from .base import VersionsTransport from .grpc import VersionsGrpcTransport from .grpc_asyncio import VersionsGrpcAsyncIOTransport +from .rest import VersionsRestInterceptor, VersionsRestTransport # Compile a registry of transports. _transport_registry = OrderedDict() # type: Dict[str, Type[VersionsTransport]] _transport_registry["grpc"] = VersionsGrpcTransport _transport_registry["grpc_asyncio"] = VersionsGrpcAsyncIOTransport +_transport_registry["rest"] = VersionsRestTransport __all__ = ( "VersionsTransport", "VersionsGrpcTransport", "VersionsGrpcAsyncIOTransport", + "VersionsRestTransport", + "VersionsRestInterceptor", ) diff --git a/google/cloud/appengine_admin_v1/services/versions/transports/rest.py b/google/cloud/appengine_admin_v1/services/versions/transports/rest.py new file mode 100644 index 0000000..0157db5 --- /dev/null +++ b/google/cloud/appengine_admin_v1/services/versions/transports/rest.py @@ -0,0 +1,814 @@ +# -*- coding: utf-8 -*- +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import dataclasses +import json # type: ignore +import re +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +import warnings + +from google.api_core import ( + gapic_v1, + operations_v1, + path_template, + rest_helpers, + rest_streaming, +) +from google.api_core import exceptions as core_exceptions +from google.api_core import retry as retries +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.auth.transport.requests import AuthorizedSession # type: ignore +from google.protobuf import json_format +import grpc # type: ignore +from requests import __version__ as requests_version + +try: + OptionalRetry = Union[retries.Retry, gapic_v1.method._MethodDefault] +except AttributeError: # pragma: NO COVER + OptionalRetry = Union[retries.Retry, object] # type: ignore + + +from google.longrunning import operations_pb2 # type: ignore + +from google.cloud.appengine_admin_v1.types import appengine, version + +from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO +from .base import VersionsTransport + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, + grpc_version=None, + rest_version=requests_version, +) + + +class VersionsRestInterceptor: + """Interceptor for Versions. + + Interceptors are used to manipulate requests, request metadata, and responses + in arbitrary ways. + Example use cases include: + * Logging + * Verifying requests according to service or custom semantics + * Stripping extraneous information from responses + + These use cases and more can be enabled by injecting an + instance of a custom subclass when constructing the VersionsRestTransport. + + .. code-block:: python + class MyCustomVersionsInterceptor(VersionsRestInterceptor): + def pre_create_version(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_create_version(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_delete_version(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_delete_version(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_get_version(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_get_version(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_list_versions(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_list_versions(self, response): + logging.log(f"Received response: {response}") + return response + + def pre_update_version(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + def post_update_version(self, response): + logging.log(f"Received response: {response}") + return response + + transport = VersionsRestTransport(interceptor=MyCustomVersionsInterceptor()) + client = VersionsClient(transport=transport) + + + """ + + def pre_create_version( + self, + request: appengine.CreateVersionRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.CreateVersionRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for create_version + + Override in a subclass to manipulate the request or metadata + before they are sent to the Versions server. + """ + return request, metadata + + def post_create_version( + self, response: operations_pb2.Operation + ) -> operations_pb2.Operation: + """Post-rpc interceptor for create_version + + Override in a subclass to manipulate the response + after it is returned by the Versions server but before + it is returned to user code. + """ + return response + + def pre_delete_version( + self, + request: appengine.DeleteVersionRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.DeleteVersionRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for delete_version + + Override in a subclass to manipulate the request or metadata + before they are sent to the Versions server. + """ + return request, metadata + + def post_delete_version( + self, response: operations_pb2.Operation + ) -> operations_pb2.Operation: + """Post-rpc interceptor for delete_version + + Override in a subclass to manipulate the response + after it is returned by the Versions server but before + it is returned to user code. + """ + return response + + def pre_get_version( + self, request: appengine.GetVersionRequest, metadata: Sequence[Tuple[str, str]] + ) -> Tuple[appengine.GetVersionRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for get_version + + Override in a subclass to manipulate the request or metadata + before they are sent to the Versions server. + """ + return request, metadata + + def post_get_version(self, response: version.Version) -> version.Version: + """Post-rpc interceptor for get_version + + Override in a subclass to manipulate the response + after it is returned by the Versions server but before + it is returned to user code. + """ + return response + + def pre_list_versions( + self, + request: appengine.ListVersionsRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.ListVersionsRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for list_versions + + Override in a subclass to manipulate the request or metadata + before they are sent to the Versions server. + """ + return request, metadata + + def post_list_versions( + self, response: appengine.ListVersionsResponse + ) -> appengine.ListVersionsResponse: + """Post-rpc interceptor for list_versions + + Override in a subclass to manipulate the response + after it is returned by the Versions server but before + it is returned to user code. + """ + return response + + def pre_update_version( + self, + request: appengine.UpdateVersionRequest, + metadata: Sequence[Tuple[str, str]], + ) -> Tuple[appengine.UpdateVersionRequest, Sequence[Tuple[str, str]]]: + """Pre-rpc interceptor for update_version + + Override in a subclass to manipulate the request or metadata + before they are sent to the Versions server. + """ + return request, metadata + + def post_update_version( + self, response: operations_pb2.Operation + ) -> operations_pb2.Operation: + """Post-rpc interceptor for update_version + + Override in a subclass to manipulate the response + after it is returned by the Versions server but before + it is returned to user code. + """ + return response + + +@dataclasses.dataclass +class VersionsRestStub: + _session: AuthorizedSession + _host: str + _interceptor: VersionsRestInterceptor + + +class VersionsRestTransport(VersionsTransport): + """REST backend transport for Versions. + + Manages versions of a service. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends JSON representations of protocol buffers over HTTP/1.1 + + """ + + def __init__( + self, + *, + host: str = "appengine.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + client_cert_source_for_mtls: Optional[Callable[[], Tuple[bytes, bytes]]] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + url_scheme: str = "https", + interceptor: Optional[VersionsRestInterceptor] = None, + api_audience: Optional[str] = None, + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to. + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + 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 + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. + 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 + certificate to configure mutual TLS HTTP channel. It is ignored + if ``channel`` is provided. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you are developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + url_scheme: the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. + """ + # Run the base constructor + # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. + # TODO: When custom host (api_endpoint) is set, `scopes` must *also* be set on the + # credentials object + maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) + if maybe_url_match is None: + raise ValueError( + f"Unexpected hostname structure: {host}" + ) # pragma: NO COVER + + url_match_items = maybe_url_match.groupdict() + + host = f"{url_scheme}://{host}" if not url_match_items["scheme"] else host + + super().__init__( + host=host, + credentials=credentials, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + api_audience=api_audience, + ) + self._session = AuthorizedSession( + self._credentials, default_host=self.DEFAULT_HOST + ) + self._operations_client: Optional[operations_v1.AbstractOperationsClient] = None + if client_cert_source_for_mtls: + self._session.configure_mtls_channel(client_cert_source_for_mtls) + self._interceptor = interceptor or VersionsRestInterceptor() + self._prep_wrapped_messages(client_info) + + @property + def operations_client(self) -> operations_v1.AbstractOperationsClient: + """Create the client designed to process long-running operations. + + This property caches on the instance; repeated calls return the same + client. + """ + # Only create a new client if we do not already have one. + if self._operations_client is None: + http_options: Dict[str, List[Dict[str, str]]] = { + "google.longrunning.Operations.GetOperation": [ + { + "method": "get", + "uri": "/v1/{name=apps/*/operations/*}", + }, + ], + "google.longrunning.Operations.ListOperations": [ + { + "method": "get", + "uri": "/v1/{name=apps/*}/operations", + }, + ], + } + + rest_transport = operations_v1.OperationsRestTransport( + host=self._host, + # use the credentials which are saved + credentials=self._credentials, + scopes=self._scopes, + http_options=http_options, + path_prefix="v1", + ) + + self._operations_client = operations_v1.AbstractOperationsClient( + transport=rest_transport + ) + + # Return the client from cache. + return self._operations_client + + class _CreateVersion(VersionsRestStub): + def __hash__(self): + return hash("CreateVersion") + + def __call__( + self, + request: appengine.CreateVersionRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Call the create version method over HTTP. + + Args: + request (~.appengine.CreateVersionRequest): + The request object. Request message for ``Versions.CreateVersion``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.Operation: + This resource represents a + long-running operation that is the + result of a network API call. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "post", + "uri": "/v1/{parent=apps/*/services/*}/versions", + "body": "version", + }, + ] + request, metadata = self._interceptor.pre_create_version(request, metadata) + pb_request = appengine.CreateVersionRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + # Jsonify the request body + + body = json_format.MessageToJson( + transcoded_request["body"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = operations_pb2.Operation() + json_format.Parse(response.content, resp, ignore_unknown_fields=True) + resp = self._interceptor.post_create_version(resp) + return resp + + class _DeleteVersion(VersionsRestStub): + def __hash__(self): + return hash("DeleteVersion") + + def __call__( + self, + request: appengine.DeleteVersionRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Call the delete version method over HTTP. + + Args: + request (~.appengine.DeleteVersionRequest): + The request object. Request message for ``Versions.DeleteVersion``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.Operation: + This resource represents a + long-running operation that is the + result of a network API call. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "delete", + "uri": "/v1/{name=apps/*/services/*/versions/*}", + }, + ] + request, metadata = self._interceptor.pre_delete_version(request, metadata) + pb_request = appengine.DeleteVersionRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = operations_pb2.Operation() + json_format.Parse(response.content, resp, ignore_unknown_fields=True) + resp = self._interceptor.post_delete_version(resp) + return resp + + class _GetVersion(VersionsRestStub): + def __hash__(self): + return hash("GetVersion") + + def __call__( + self, + request: appengine.GetVersionRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> version.Version: + r"""Call the get version method over HTTP. + + Args: + request (~.appengine.GetVersionRequest): + The request object. Request message for ``Versions.GetVersion``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.version.Version: + A Version resource is a specific set + of source code and configuration files + that are deployed into a service. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "get", + "uri": "/v1/{name=apps/*/services/*/versions/*}", + }, + ] + request, metadata = self._interceptor.pre_get_version(request, metadata) + pb_request = appengine.GetVersionRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = version.Version() + pb_resp = version.Version.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_get_version(resp) + return resp + + class _ListVersions(VersionsRestStub): + def __hash__(self): + return hash("ListVersions") + + def __call__( + self, + request: appengine.ListVersionsRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> appengine.ListVersionsResponse: + r"""Call the list versions method over HTTP. + + Args: + request (~.appengine.ListVersionsRequest): + The request object. Request message for ``Versions.ListVersions``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.appengine.ListVersionsResponse: + Response message for ``Versions.ListVersions``. + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "get", + "uri": "/v1/{parent=apps/*/services/*}/versions", + }, + ] + request, metadata = self._interceptor.pre_list_versions(request, metadata) + pb_request = appengine.ListVersionsRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = appengine.ListVersionsResponse() + pb_resp = appengine.ListVersionsResponse.pb(resp) + + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + resp = self._interceptor.post_list_versions(resp) + return resp + + class _UpdateVersion(VersionsRestStub): + def __hash__(self): + return hash("UpdateVersion") + + def __call__( + self, + request: appengine.UpdateVersionRequest, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Call the update version method over HTTP. + + Args: + request (~.appengine.UpdateVersionRequest): + The request object. Request message for ``Versions.UpdateVersion``. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.Operation: + This resource represents a + long-running operation that is the + result of a network API call. + + """ + + http_options: List[Dict[str, str]] = [ + { + "method": "patch", + "uri": "/v1/{name=apps/*/services/*/versions/*}", + "body": "version", + }, + ] + request, metadata = self._interceptor.pre_update_version(request, metadata) + pb_request = appengine.UpdateVersionRequest.pb(request) + transcoded_request = path_template.transcode(http_options, pb_request) + + # Jsonify the request body + + body = json_format.MessageToJson( + transcoded_request["body"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params = json.loads( + json_format.MessageToJson( + transcoded_request["query_params"], + including_default_value_fields=False, + use_integers_for_enums=True, + ) + ) + + query_params["$alt"] = "json;enum-encoding=int" + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + raise core_exceptions.from_http_response(response) + + # Return the response + resp = operations_pb2.Operation() + json_format.Parse(response.content, resp, ignore_unknown_fields=True) + resp = self._interceptor.post_update_version(resp) + return resp + + @property + def create_version( + self, + ) -> Callable[[appengine.CreateVersionRequest], operations_pb2.Operation]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._CreateVersion(self._session, self._host, self._interceptor) # type: ignore + + @property + def delete_version( + self, + ) -> Callable[[appengine.DeleteVersionRequest], operations_pb2.Operation]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._DeleteVersion(self._session, self._host, self._interceptor) # type: ignore + + @property + def get_version(self) -> Callable[[appengine.GetVersionRequest], version.Version]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._GetVersion(self._session, self._host, self._interceptor) # type: ignore + + @property + def list_versions( + self, + ) -> Callable[[appengine.ListVersionsRequest], appengine.ListVersionsResponse]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._ListVersions(self._session, self._host, self._interceptor) # type: ignore + + @property + def update_version( + self, + ) -> Callable[[appengine.UpdateVersionRequest], operations_pb2.Operation]: + # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. + # In C++ this would require a dynamic_cast + return self._UpdateVersion(self._session, self._host, self._interceptor) # type: ignore + + @property + def kind(self) -> str: + return "rest" + + def close(self): + self._session.close() + + +__all__ = ("VersionsRestTransport",) diff --git a/google/cloud/appengine_admin_v1/types/app_yaml.py b/google/cloud/appengine_admin_v1/types/app_yaml.py index 9384f6e..9387b98 100644 --- a/google/cloud/appengine_admin_v1/types/app_yaml.py +++ b/google/cloud/appengine_admin_v1/types/app_yaml.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from __future__ import annotations + from typing import MutableMapping, MutableSequence from google.protobuf import duration_pb2 # type: ignore diff --git a/google/cloud/appengine_admin_v1/types/appengine.py b/google/cloud/appengine_admin_v1/types/appengine.py index 28ceb53..f5ba7d5 100644 --- a/google/cloud/appengine_admin_v1/types/appengine.py +++ b/google/cloud/appengine_admin_v1/types/appengine.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from __future__ import annotations + from typing import MutableMapping, MutableSequence from google.protobuf import field_mask_pb2 # type: ignore diff --git a/google/cloud/appengine_admin_v1/types/application.py b/google/cloud/appengine_admin_v1/types/application.py index d50a6c0..f9d5c55 100644 --- a/google/cloud/appengine_admin_v1/types/application.py +++ b/google/cloud/appengine_admin_v1/types/application.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from __future__ import annotations + from typing import MutableMapping, MutableSequence from google.protobuf import duration_pb2 # type: ignore diff --git a/google/cloud/appengine_admin_v1/types/audit_data.py b/google/cloud/appengine_admin_v1/types/audit_data.py index 06c4a06..bc937a6 100644 --- a/google/cloud/appengine_admin_v1/types/audit_data.py +++ b/google/cloud/appengine_admin_v1/types/audit_data.py @@ -13,6 +13,8 @@ # 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 diff --git a/google/cloud/appengine_admin_v1/types/certificate.py b/google/cloud/appengine_admin_v1/types/certificate.py index e314a8a..db119c6 100644 --- a/google/cloud/appengine_admin_v1/types/certificate.py +++ b/google/cloud/appengine_admin_v1/types/certificate.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from __future__ import annotations + from typing import MutableMapping, MutableSequence from google.protobuf import timestamp_pb2 # type: ignore @@ -35,7 +37,7 @@ class ManagementStatus(proto.Enum): Values: MANAGEMENT_STATUS_UNSPECIFIED (0): - + No description available. OK (1): Certificate was successfully obtained and inserted into the serving system. diff --git a/google/cloud/appengine_admin_v1/types/deploy.py b/google/cloud/appengine_admin_v1/types/deploy.py index c17f74c..7200ff4 100644 --- a/google/cloud/appengine_admin_v1/types/deploy.py +++ b/google/cloud/appengine_admin_v1/types/deploy.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from __future__ import annotations + from typing import MutableMapping, MutableSequence from google.protobuf import duration_pb2 # type: ignore diff --git a/google/cloud/appengine_admin_v1/types/deployed_files.py b/google/cloud/appengine_admin_v1/types/deployed_files.py index e127548..c11a0ad 100644 --- a/google/cloud/appengine_admin_v1/types/deployed_files.py +++ b/google/cloud/appengine_admin_v1/types/deployed_files.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # - +import proto # type: ignore __protobuf__ = proto.module( package="google.appengine.v1", diff --git a/google/cloud/appengine_admin_v1/types/domain.py b/google/cloud/appengine_admin_v1/types/domain.py index 668a5dc..c0006a9 100644 --- a/google/cloud/appengine_admin_v1/types/domain.py +++ b/google/cloud/appengine_admin_v1/types/domain.py @@ -13,6 +13,8 @@ # 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 diff --git a/google/cloud/appengine_admin_v1/types/domain_mapping.py b/google/cloud/appengine_admin_v1/types/domain_mapping.py index 5007c5a..7af44dc 100644 --- a/google/cloud/appengine_admin_v1/types/domain_mapping.py +++ b/google/cloud/appengine_admin_v1/types/domain_mapping.py @@ -13,6 +13,8 @@ # 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 diff --git a/google/cloud/appengine_admin_v1/types/firewall.py b/google/cloud/appengine_admin_v1/types/firewall.py index cd8209a..d22a04a 100644 --- a/google/cloud/appengine_admin_v1/types/firewall.py +++ b/google/cloud/appengine_admin_v1/types/firewall.py @@ -13,6 +13,8 @@ # 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 @@ -65,7 +67,7 @@ class Action(proto.Enum): Values: UNSPECIFIED_ACTION (0): - + No description available. ALLOW (1): Matching requests are allowed. DENY (2): diff --git a/google/cloud/appengine_admin_v1/types/instance.py b/google/cloud/appengine_admin_v1/types/instance.py index db103a8..4ea61e1 100644 --- a/google/cloud/appengine_admin_v1/types/instance.py +++ b/google/cloud/appengine_admin_v1/types/instance.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from __future__ import annotations + from typing import MutableMapping, MutableSequence from google.protobuf import timestamp_pb2 # type: ignore @@ -96,11 +98,11 @@ class Availability(proto.Enum): Values: UNSPECIFIED (0): - + No description available. RESIDENT (1): - + No description available. DYNAMIC (2): - + No description available. """ UNSPECIFIED = 0 RESIDENT = 1 diff --git a/google/cloud/appengine_admin_v1/types/location.py b/google/cloud/appengine_admin_v1/types/location.py index ad3c5e2..84f925e 100644 --- a/google/cloud/appengine_admin_v1/types/location.py +++ b/google/cloud/appengine_admin_v1/types/location.py @@ -13,6 +13,8 @@ # 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 diff --git a/google/cloud/appengine_admin_v1/types/network_settings.py b/google/cloud/appengine_admin_v1/types/network_settings.py index f47ece3..47d9ad3 100644 --- a/google/cloud/appengine_admin_v1/types/network_settings.py +++ b/google/cloud/appengine_admin_v1/types/network_settings.py @@ -13,6 +13,8 @@ # 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 diff --git a/google/cloud/appengine_admin_v1/types/operation.py b/google/cloud/appengine_admin_v1/types/operation.py index 9a9a712..26baf33 100644 --- a/google/cloud/appengine_admin_v1/types/operation.py +++ b/google/cloud/appengine_admin_v1/types/operation.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from __future__ import annotations + from typing import MutableMapping, MutableSequence from google.protobuf import timestamp_pb2 # type: ignore diff --git a/google/cloud/appengine_admin_v1/types/service.py b/google/cloud/appengine_admin_v1/types/service.py index 1da091b..6cbcff4 100644 --- a/google/cloud/appengine_admin_v1/types/service.py +++ b/google/cloud/appengine_admin_v1/types/service.py @@ -13,6 +13,8 @@ # 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 diff --git a/google/cloud/appengine_admin_v1/types/version.py b/google/cloud/appengine_admin_v1/types/version.py index e1c5027..4841759 100644 --- a/google/cloud/appengine_admin_v1/types/version.py +++ b/google/cloud/appengine_admin_v1/types/version.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from __future__ import annotations + from typing import MutableMapping, MutableSequence from google.protobuf import duration_pb2 # type: ignore @@ -1036,7 +1038,7 @@ class EgressSetting(proto.Enum): Values: EGRESS_SETTING_UNSPECIFIED (0): - + No description available. ALL_TRAFFIC (1): Force the use of VPC Access for all egress traffic from the function. diff --git a/noxfile.py b/noxfile.py index e716318..95e58c5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -189,9 +189,9 @@ def unit(session): def install_systemtest_dependencies(session, *constraints): # Use pre-release gRPC for system tests. - # Exclude version 1.49.0rc1 which has a known issue. - # See https://github.com/grpc/grpc/pull/30642 - session.install("--pre", "grpcio!=1.49.0rc1") + # 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) @@ -346,9 +346,7 @@ def prerelease_deps(session): 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 - + SYSTEM_TEST_EXTRAS + SYSTEM_TEST_STANDARD_DEPENDENCIES + SYSTEM_TEST_EXTERNAL_DEPENDENCIES ) session.install(*system_deps_all) @@ -378,8 +376,8 @@ def prerelease_deps(session): # dependency of grpc "six", "googleapis-common-protos", - # Exclude version 1.49.0rc1 which has a known issue. See https://github.com/grpc/grpc/pull/30642 - "grpcio!=1.49.0rc1", + # Exclude version 1.52.0rc1 which has a known issue. See https://github.com/grpc/grpc/issues/32163 + "grpcio!=1.52.0rc1", "grpcio-status", "google-api-core", "proto-plus", diff --git a/samples/generated_samples/snippet_metadata_google.appengine.v1.json b/samples/generated_samples/snippet_metadata_google.appengine.v1.json index 4447546..ef36d09 100644 --- a/samples/generated_samples/snippet_metadata_google.appengine.v1.json +++ b/samples/generated_samples/snippet_metadata_google.appengine.v1.json @@ -8,7 +8,7 @@ ], "language": "PYTHON", "name": "google-cloud-appengine-admin", - "version": "1.8.1" + "version": "1.9.0" }, "snippets": [ { diff --git a/setup.py b/setup.py index 0a6d760..3652982 100644 --- a/setup.py +++ b/setup.py @@ -57,9 +57,7 @@ if package.startswith("google") ] -namespaces = ["google"] -if "google.cloud" in packages: - namespaces.append("google.cloud") +namespaces = ["google", "google.cloud"] setuptools.setup( name=name, diff --git a/tests/unit/gapic/appengine_admin_v1/test_applications.py b/tests/unit/gapic/appengine_admin_v1/test_applications.py index cdb6687..7fb2233 100644 --- a/tests/unit/gapic/appengine_admin_v1/test_applications.py +++ b/tests/unit/gapic/appengine_admin_v1/test_applications.py @@ -22,6 +22,8 @@ except ImportError: # pragma: NO COVER import mock +from collections.abc import Iterable +import json import math from google.api_core import ( @@ -43,11 +45,14 @@ from google.oauth2 import service_account from google.protobuf import duration_pb2 # type: ignore from google.protobuf import field_mask_pb2 # type: ignore +from google.protobuf import json_format import grpc from grpc.experimental import aio from proto.marshal.rules import wrappers from proto.marshal.rules.dates import DurationRule, TimestampRule import pytest +from requests import PreparedRequest, Request, Response +from requests.sessions import Session from google.cloud.appengine_admin_v1.services.applications import ( ApplicationsAsyncClient, @@ -104,6 +109,7 @@ def test__get_default_mtls_endpoint(): [ (ApplicationsClient, "grpc"), (ApplicationsAsyncClient, "grpc_asyncio"), + (ApplicationsClient, "rest"), ], ) def test_applications_client_from_service_account_info(client_class, transport_name): @@ -117,7 +123,11 @@ def test_applications_client_from_service_account_info(client_class, transport_n assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -125,6 +135,7 @@ def test_applications_client_from_service_account_info(client_class, transport_n [ (transports.ApplicationsGrpcTransport, "grpc"), (transports.ApplicationsGrpcAsyncIOTransport, "grpc_asyncio"), + (transports.ApplicationsRestTransport, "rest"), ], ) def test_applications_client_service_account_always_use_jwt( @@ -150,6 +161,7 @@ def test_applications_client_service_account_always_use_jwt( [ (ApplicationsClient, "grpc"), (ApplicationsAsyncClient, "grpc_asyncio"), + (ApplicationsClient, "rest"), ], ) def test_applications_client_from_service_account_file(client_class, transport_name): @@ -170,13 +182,18 @@ def test_applications_client_from_service_account_file(client_class, transport_n assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) def test_applications_client_get_transport_class(): transport = ApplicationsClient.get_transport_class() available_transports = [ transports.ApplicationsGrpcTransport, + transports.ApplicationsRestTransport, ] assert transport in available_transports @@ -193,6 +210,7 @@ def test_applications_client_get_transport_class(): transports.ApplicationsGrpcAsyncIOTransport, "grpc_asyncio", ), + (ApplicationsClient, transports.ApplicationsRestTransport, "rest"), ], ) @mock.patch.object( @@ -336,6 +354,8 @@ def test_applications_client_client_options( "grpc_asyncio", "false", ), + (ApplicationsClient, transports.ApplicationsRestTransport, "rest", "true"), + (ApplicationsClient, transports.ApplicationsRestTransport, "rest", "false"), ], ) @mock.patch.object( @@ -529,6 +549,7 @@ def test_applications_client_get_mtls_endpoint_and_cert_source(client_class): transports.ApplicationsGrpcAsyncIOTransport, "grpc_asyncio", ), + (ApplicationsClient, transports.ApplicationsRestTransport, "rest"), ], ) def test_applications_client_client_options_scopes( @@ -569,6 +590,7 @@ def test_applications_client_client_options_scopes( "grpc_asyncio", grpc_helpers_async, ), + (ApplicationsClient, transports.ApplicationsRestTransport, "rest", None), ], ) def test_applications_client_client_options_credentials_file( @@ -1356,6 +1378,688 @@ async def test_repair_application_field_headers_async(): ) in kw["metadata"] +@pytest.mark.parametrize( + "request_type", + [ + appengine.GetApplicationRequest, + dict, + ], +) +def test_get_application_rest(request_type): + client = ApplicationsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = application.Application( + name="name_value", + id="id_value", + auth_domain="auth_domain_value", + location_id="location_id_value", + code_bucket="code_bucket_value", + serving_status=application.Application.ServingStatus.SERVING, + default_hostname="default_hostname_value", + default_bucket="default_bucket_value", + service_account="service_account_value", + gcr_domain="gcr_domain_value", + database_type=application.Application.DatabaseType.CLOUD_DATASTORE, + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = application.Application.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.get_application(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, application.Application) + assert response.name == "name_value" + assert response.id == "id_value" + assert response.auth_domain == "auth_domain_value" + assert response.location_id == "location_id_value" + assert response.code_bucket == "code_bucket_value" + assert response.serving_status == application.Application.ServingStatus.SERVING + assert response.default_hostname == "default_hostname_value" + assert response.default_bucket == "default_bucket_value" + assert response.service_account == "service_account_value" + assert response.gcr_domain == "gcr_domain_value" + assert ( + response.database_type == application.Application.DatabaseType.CLOUD_DATASTORE + ) + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_get_application_rest_interceptors(null_interceptor): + transport = transports.ApplicationsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None + if null_interceptor + else transports.ApplicationsRestInterceptor(), + ) + client = ApplicationsClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.ApplicationsRestInterceptor, "post_get_application" + ) as post, mock.patch.object( + transports.ApplicationsRestInterceptor, "pre_get_application" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.GetApplicationRequest.pb( + appengine.GetApplicationRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = application.Application.to_json( + application.Application() + ) + + request = appengine.GetApplicationRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = application.Application() + + client.get_application( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_get_application_rest_bad_request( + transport: str = "rest", request_type=appengine.GetApplicationRequest +): + client = ApplicationsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.get_application(request) + + +def test_get_application_rest_flattened(): + client = ApplicationsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = application.Application() + + # get arguments that satisfy an http rule for this method + sample_request = {"name": "apps/sample1"} + + # get truthy value for each flattened field + mock_args = dict( + name="name_value", + ) + mock_args.update(sample_request) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = application.Application.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + + client.get_application(**mock_args) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(req.mock_calls) == 1 + _, args, _ = req.mock_calls[0] + assert path_template.validate( + "%s/v1/{name=apps/*}" % client.transport._host, args[1] + ) + + +def test_get_application_rest_flattened_error(transport: str = "rest"): + client = ApplicationsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + client.get_application( + appengine.GetApplicationRequest(), + name="name_value", + ) + + +def test_get_application_rest_error(): + client = ApplicationsClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.CreateApplicationRequest, + dict, + ], +) +def test_create_application_rest(request_type): + client = ApplicationsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {} + request_init["application"] = { + "name": "name_value", + "id": "id_value", + "dispatch_rules": [ + {"domain": "domain_value", "path": "path_value", "service": "service_value"} + ], + "auth_domain": "auth_domain_value", + "location_id": "location_id_value", + "code_bucket": "code_bucket_value", + "default_cookie_expiration": {"seconds": 751, "nanos": 543}, + "serving_status": 1, + "default_hostname": "default_hostname_value", + "default_bucket": "default_bucket_value", + "service_account": "service_account_value", + "iap": { + "enabled": True, + "oauth2_client_id": "oauth2_client_id_value", + "oauth2_client_secret": "oauth2_client_secret_value", + "oauth2_client_secret_sha256": "oauth2_client_secret_sha256_value", + }, + "gcr_domain": "gcr_domain_value", + "database_type": 1, + "feature_settings": { + "split_health_checks": True, + "use_container_optimized_os": True, + }, + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = operations_pb2.Operation(name="operations/spam") + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + json_return_value = json_format.MessageToJson(return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.create_application(request) + + # Establish that the response is the type that we expect. + assert response.operation.name == "operations/spam" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_create_application_rest_interceptors(null_interceptor): + transport = transports.ApplicationsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None + if null_interceptor + else transports.ApplicationsRestInterceptor(), + ) + client = ApplicationsClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + operation.Operation, "_set_result_from_operation" + ), mock.patch.object( + transports.ApplicationsRestInterceptor, "post_create_application" + ) as post, mock.patch.object( + transports.ApplicationsRestInterceptor, "pre_create_application" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.CreateApplicationRequest.pb( + appengine.CreateApplicationRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = json_format.MessageToJson( + operations_pb2.Operation() + ) + + request = appengine.CreateApplicationRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = operations_pb2.Operation() + + client.create_application( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_create_application_rest_bad_request( + transport: str = "rest", request_type=appengine.CreateApplicationRequest +): + client = ApplicationsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {} + request_init["application"] = { + "name": "name_value", + "id": "id_value", + "dispatch_rules": [ + {"domain": "domain_value", "path": "path_value", "service": "service_value"} + ], + "auth_domain": "auth_domain_value", + "location_id": "location_id_value", + "code_bucket": "code_bucket_value", + "default_cookie_expiration": {"seconds": 751, "nanos": 543}, + "serving_status": 1, + "default_hostname": "default_hostname_value", + "default_bucket": "default_bucket_value", + "service_account": "service_account_value", + "iap": { + "enabled": True, + "oauth2_client_id": "oauth2_client_id_value", + "oauth2_client_secret": "oauth2_client_secret_value", + "oauth2_client_secret_sha256": "oauth2_client_secret_sha256_value", + }, + "gcr_domain": "gcr_domain_value", + "database_type": 1, + "feature_settings": { + "split_health_checks": True, + "use_container_optimized_os": True, + }, + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.create_application(request) + + +def test_create_application_rest_error(): + client = ApplicationsClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.UpdateApplicationRequest, + dict, + ], +) +def test_update_application_rest(request_type): + client = ApplicationsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1"} + request_init["application"] = { + "name": "name_value", + "id": "id_value", + "dispatch_rules": [ + {"domain": "domain_value", "path": "path_value", "service": "service_value"} + ], + "auth_domain": "auth_domain_value", + "location_id": "location_id_value", + "code_bucket": "code_bucket_value", + "default_cookie_expiration": {"seconds": 751, "nanos": 543}, + "serving_status": 1, + "default_hostname": "default_hostname_value", + "default_bucket": "default_bucket_value", + "service_account": "service_account_value", + "iap": { + "enabled": True, + "oauth2_client_id": "oauth2_client_id_value", + "oauth2_client_secret": "oauth2_client_secret_value", + "oauth2_client_secret_sha256": "oauth2_client_secret_sha256_value", + }, + "gcr_domain": "gcr_domain_value", + "database_type": 1, + "feature_settings": { + "split_health_checks": True, + "use_container_optimized_os": True, + }, + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = operations_pb2.Operation(name="operations/spam") + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + json_return_value = json_format.MessageToJson(return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.update_application(request) + + # Establish that the response is the type that we expect. + assert response.operation.name == "operations/spam" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_update_application_rest_interceptors(null_interceptor): + transport = transports.ApplicationsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None + if null_interceptor + else transports.ApplicationsRestInterceptor(), + ) + client = ApplicationsClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + operation.Operation, "_set_result_from_operation" + ), mock.patch.object( + transports.ApplicationsRestInterceptor, "post_update_application" + ) as post, mock.patch.object( + transports.ApplicationsRestInterceptor, "pre_update_application" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.UpdateApplicationRequest.pb( + appengine.UpdateApplicationRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = json_format.MessageToJson( + operations_pb2.Operation() + ) + + request = appengine.UpdateApplicationRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = operations_pb2.Operation() + + client.update_application( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_update_application_rest_bad_request( + transport: str = "rest", request_type=appengine.UpdateApplicationRequest +): + client = ApplicationsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1"} + request_init["application"] = { + "name": "name_value", + "id": "id_value", + "dispatch_rules": [ + {"domain": "domain_value", "path": "path_value", "service": "service_value"} + ], + "auth_domain": "auth_domain_value", + "location_id": "location_id_value", + "code_bucket": "code_bucket_value", + "default_cookie_expiration": {"seconds": 751, "nanos": 543}, + "serving_status": 1, + "default_hostname": "default_hostname_value", + "default_bucket": "default_bucket_value", + "service_account": "service_account_value", + "iap": { + "enabled": True, + "oauth2_client_id": "oauth2_client_id_value", + "oauth2_client_secret": "oauth2_client_secret_value", + "oauth2_client_secret_sha256": "oauth2_client_secret_sha256_value", + }, + "gcr_domain": "gcr_domain_value", + "database_type": 1, + "feature_settings": { + "split_health_checks": True, + "use_container_optimized_os": True, + }, + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.update_application(request) + + +def test_update_application_rest_error(): + client = ApplicationsClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.RepairApplicationRequest, + dict, + ], +) +def test_repair_application_rest(request_type): + client = ApplicationsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = operations_pb2.Operation(name="operations/spam") + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + json_return_value = json_format.MessageToJson(return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.repair_application(request) + + # Establish that the response is the type that we expect. + assert response.operation.name == "operations/spam" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_repair_application_rest_interceptors(null_interceptor): + transport = transports.ApplicationsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None + if null_interceptor + else transports.ApplicationsRestInterceptor(), + ) + client = ApplicationsClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + operation.Operation, "_set_result_from_operation" + ), mock.patch.object( + transports.ApplicationsRestInterceptor, "post_repair_application" + ) as post, mock.patch.object( + transports.ApplicationsRestInterceptor, "pre_repair_application" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.RepairApplicationRequest.pb( + appengine.RepairApplicationRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = json_format.MessageToJson( + operations_pb2.Operation() + ) + + request = appengine.RepairApplicationRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = operations_pb2.Operation() + + client.repair_application( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_repair_application_rest_bad_request( + transport: str = "rest", request_type=appengine.RepairApplicationRequest +): + client = ApplicationsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.repair_application(request) + + +def test_repair_application_rest_error(): + client = ApplicationsClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + def test_credentials_transport_error(): # It is an error to provide credentials and a transport instance. transport = transports.ApplicationsGrpcTransport( @@ -1437,6 +2141,7 @@ def test_transport_get_channel(): [ transports.ApplicationsGrpcTransport, transports.ApplicationsGrpcAsyncIOTransport, + transports.ApplicationsRestTransport, ], ) def test_transport_adc(transport_class): @@ -1451,6 +2156,7 @@ def test_transport_adc(transport_class): "transport_name", [ "grpc", + "rest", ], ) def test_transport_kind(transport_name): @@ -1600,6 +2306,7 @@ def test_applications_transport_auth_adc(transport_class): [ transports.ApplicationsGrpcTransport, transports.ApplicationsGrpcAsyncIOTransport, + transports.ApplicationsRestTransport, ], ) def test_applications_transport_auth_gdch_credentials(transport_class): @@ -1698,11 +2405,40 @@ def test_applications_grpc_transport_client_cert_source_for_mtls(transport_class ) +def test_applications_http_transport_client_cert_source_for_mtls(): + cred = ga_credentials.AnonymousCredentials() + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.configure_mtls_channel" + ) as mock_configure_mtls_channel: + transports.ApplicationsRestTransport( + credentials=cred, client_cert_source_for_mtls=client_cert_source_callback + ) + mock_configure_mtls_channel.assert_called_once_with(client_cert_source_callback) + + +def test_applications_rest_lro_client(): + client = ApplicationsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + transport = client.transport + + # Ensure that we have a api-core operations client. + assert isinstance( + transport.operations_client, + operations_v1.AbstractOperationsClient, + ) + + # Ensure that subsequent calls to the property send the exact same object. + assert transport.operations_client is transport.operations_client + + @pytest.mark.parametrize( "transport_name", [ "grpc", "grpc_asyncio", + "rest", ], ) def test_applications_host_no_port(transport_name): @@ -1713,7 +2449,11 @@ def test_applications_host_no_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -1721,6 +2461,7 @@ def test_applications_host_no_port(transport_name): [ "grpc", "grpc_asyncio", + "rest", ], ) def test_applications_host_with_port(transport_name): @@ -1731,7 +2472,42 @@ def test_applications_host_with_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:8000") + assert client.transport._host == ( + "appengine.googleapis.com:8000" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com:8000" + ) + + +@pytest.mark.parametrize( + "transport_name", + [ + "rest", + ], +) +def test_applications_client_transport_session_collision(transport_name): + creds1 = ga_credentials.AnonymousCredentials() + creds2 = ga_credentials.AnonymousCredentials() + client1 = ApplicationsClient( + credentials=creds1, + transport=transport_name, + ) + client2 = ApplicationsClient( + credentials=creds2, + transport=transport_name, + ) + session1 = client1.transport.get_application._session + session2 = client2.transport.get_application._session + assert session1 != session2 + session1 = client1.transport.create_application._session + session2 = client2.transport.create_application._session + assert session1 != session2 + session1 = client1.transport.update_application._session + session2 = client2.transport.update_application._session + assert session1 != session2 + session1 = client1.transport.repair_application._session + session2 = client2.transport.repair_application._session + assert session1 != session2 def test_applications_grpc_transport_channel(): @@ -2028,6 +2804,7 @@ async def test_transport_close_async(): def test_transport_close(): transports = { + "rest": "_session", "grpc": "_grpc_channel", } @@ -2045,6 +2822,7 @@ def test_transport_close(): def test_client_ctx(): transports = [ + "rest", "grpc", ] for transport in transports: diff --git a/tests/unit/gapic/appengine_admin_v1/test_authorized_certificates.py b/tests/unit/gapic/appengine_admin_v1/test_authorized_certificates.py index 60d680e..e37feae 100644 --- a/tests/unit/gapic/appengine_admin_v1/test_authorized_certificates.py +++ b/tests/unit/gapic/appengine_admin_v1/test_authorized_certificates.py @@ -22,6 +22,8 @@ except ImportError: # pragma: NO COVER import mock +from collections.abc import Iterable +import json import math from google.api_core import gapic_v1, grpc_helpers, grpc_helpers_async, path_template @@ -32,12 +34,15 @@ from google.auth.exceptions import MutualTLSChannelError from google.oauth2 import service_account from google.protobuf import field_mask_pb2 # type: ignore +from google.protobuf import json_format from google.protobuf import timestamp_pb2 # type: ignore import grpc from grpc.experimental import aio from proto.marshal.rules import wrappers from proto.marshal.rules.dates import DurationRule, TimestampRule import pytest +from requests import PreparedRequest, Request, Response +from requests.sessions import Session from google.cloud.appengine_admin_v1.services.authorized_certificates import ( AuthorizedCertificatesAsyncClient, @@ -98,6 +103,7 @@ def test__get_default_mtls_endpoint(): [ (AuthorizedCertificatesClient, "grpc"), (AuthorizedCertificatesAsyncClient, "grpc_asyncio"), + (AuthorizedCertificatesClient, "rest"), ], ) def test_authorized_certificates_client_from_service_account_info( @@ -113,7 +119,11 @@ def test_authorized_certificates_client_from_service_account_info( assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -121,6 +131,7 @@ def test_authorized_certificates_client_from_service_account_info( [ (transports.AuthorizedCertificatesGrpcTransport, "grpc"), (transports.AuthorizedCertificatesGrpcAsyncIOTransport, "grpc_asyncio"), + (transports.AuthorizedCertificatesRestTransport, "rest"), ], ) def test_authorized_certificates_client_service_account_always_use_jwt( @@ -146,6 +157,7 @@ def test_authorized_certificates_client_service_account_always_use_jwt( [ (AuthorizedCertificatesClient, "grpc"), (AuthorizedCertificatesAsyncClient, "grpc_asyncio"), + (AuthorizedCertificatesClient, "rest"), ], ) def test_authorized_certificates_client_from_service_account_file( @@ -168,13 +180,18 @@ def test_authorized_certificates_client_from_service_account_file( assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) def test_authorized_certificates_client_get_transport_class(): transport = AuthorizedCertificatesClient.get_transport_class() available_transports = [ transports.AuthorizedCertificatesGrpcTransport, + transports.AuthorizedCertificatesRestTransport, ] assert transport in available_transports @@ -195,6 +212,11 @@ def test_authorized_certificates_client_get_transport_class(): transports.AuthorizedCertificatesGrpcAsyncIOTransport, "grpc_asyncio", ), + ( + AuthorizedCertificatesClient, + transports.AuthorizedCertificatesRestTransport, + "rest", + ), ], ) @mock.patch.object( @@ -350,6 +372,18 @@ def test_authorized_certificates_client_client_options( "grpc_asyncio", "false", ), + ( + AuthorizedCertificatesClient, + transports.AuthorizedCertificatesRestTransport, + "rest", + "true", + ), + ( + AuthorizedCertificatesClient, + transports.AuthorizedCertificatesRestTransport, + "rest", + "false", + ), ], ) @mock.patch.object( @@ -553,6 +587,11 @@ def test_authorized_certificates_client_get_mtls_endpoint_and_cert_source(client transports.AuthorizedCertificatesGrpcAsyncIOTransport, "grpc_asyncio", ), + ( + AuthorizedCertificatesClient, + transports.AuthorizedCertificatesRestTransport, + "rest", + ), ], ) def test_authorized_certificates_client_client_options_scopes( @@ -593,6 +632,12 @@ def test_authorized_certificates_client_client_options_scopes( "grpc_asyncio", grpc_helpers_async, ), + ( + AuthorizedCertificatesClient, + transports.AuthorizedCertificatesRestTransport, + "rest", + None, + ), ], ) def test_authorized_certificates_client_client_options_credentials_file( @@ -1760,6 +1805,790 @@ async def test_delete_authorized_certificate_field_headers_async(): ) in kw["metadata"] +@pytest.mark.parametrize( + "request_type", + [ + appengine.ListAuthorizedCertificatesRequest, + dict, + ], +) +def test_list_authorized_certificates_rest(request_type): + client = AuthorizedCertificatesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = appengine.ListAuthorizedCertificatesResponse( + next_page_token="next_page_token_value", + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = appengine.ListAuthorizedCertificatesResponse.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.list_authorized_certificates(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, pagers.ListAuthorizedCertificatesPager) + assert response.next_page_token == "next_page_token_value" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_list_authorized_certificates_rest_interceptors(null_interceptor): + transport = transports.AuthorizedCertificatesRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None + if null_interceptor + else transports.AuthorizedCertificatesRestInterceptor(), + ) + client = AuthorizedCertificatesClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.AuthorizedCertificatesRestInterceptor, + "post_list_authorized_certificates", + ) as post, mock.patch.object( + transports.AuthorizedCertificatesRestInterceptor, + "pre_list_authorized_certificates", + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.ListAuthorizedCertificatesRequest.pb( + appengine.ListAuthorizedCertificatesRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = ( + appengine.ListAuthorizedCertificatesResponse.to_json( + appengine.ListAuthorizedCertificatesResponse() + ) + ) + + request = appengine.ListAuthorizedCertificatesRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = appengine.ListAuthorizedCertificatesResponse() + + client.list_authorized_certificates( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_list_authorized_certificates_rest_bad_request( + transport: str = "rest", request_type=appengine.ListAuthorizedCertificatesRequest +): + client = AuthorizedCertificatesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.list_authorized_certificates(request) + + +def test_list_authorized_certificates_rest_pager(transport: str = "rest"): + client = AuthorizedCertificatesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(Session, "request") as req: + # TODO(kbandes): remove this mock unless there's a good reason for it. + # with mock.patch.object(path_template, 'transcode') as transcode: + # Set the response as a series of pages + response = ( + appengine.ListAuthorizedCertificatesResponse( + certificates=[ + certificate.AuthorizedCertificate(), + certificate.AuthorizedCertificate(), + certificate.AuthorizedCertificate(), + ], + next_page_token="abc", + ), + appengine.ListAuthorizedCertificatesResponse( + certificates=[], + next_page_token="def", + ), + appengine.ListAuthorizedCertificatesResponse( + certificates=[ + certificate.AuthorizedCertificate(), + ], + next_page_token="ghi", + ), + appengine.ListAuthorizedCertificatesResponse( + certificates=[ + certificate.AuthorizedCertificate(), + certificate.AuthorizedCertificate(), + ], + ), + ) + # Two responses for two calls + response = response + response + + # Wrap the values into proper Response objs + response = tuple( + appengine.ListAuthorizedCertificatesResponse.to_json(x) for x in response + ) + return_values = tuple(Response() for i in response) + for return_val, response_val in zip(return_values, response): + return_val._content = response_val.encode("UTF-8") + return_val.status_code = 200 + req.side_effect = return_values + + sample_request = {"parent": "apps/sample1"} + + pager = client.list_authorized_certificates(request=sample_request) + + results = list(pager) + assert len(results) == 6 + assert all(isinstance(i, certificate.AuthorizedCertificate) for i in results) + + pages = list(client.list_authorized_certificates(request=sample_request).pages) + for page_, token in zip(pages, ["abc", "def", "ghi", ""]): + assert page_.raw_page.next_page_token == token + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.GetAuthorizedCertificateRequest, + dict, + ], +) +def test_get_authorized_certificate_rest(request_type): + client = AuthorizedCertificatesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/authorizedCertificates/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = certificate.AuthorizedCertificate( + name="name_value", + id="id_value", + display_name="display_name_value", + domain_names=["domain_names_value"], + visible_domain_mappings=["visible_domain_mappings_value"], + domain_mappings_count=2238, + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = certificate.AuthorizedCertificate.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.get_authorized_certificate(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, certificate.AuthorizedCertificate) + assert response.name == "name_value" + assert response.id == "id_value" + assert response.display_name == "display_name_value" + assert response.domain_names == ["domain_names_value"] + assert response.visible_domain_mappings == ["visible_domain_mappings_value"] + assert response.domain_mappings_count == 2238 + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_get_authorized_certificate_rest_interceptors(null_interceptor): + transport = transports.AuthorizedCertificatesRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None + if null_interceptor + else transports.AuthorizedCertificatesRestInterceptor(), + ) + client = AuthorizedCertificatesClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.AuthorizedCertificatesRestInterceptor, + "post_get_authorized_certificate", + ) as post, mock.patch.object( + transports.AuthorizedCertificatesRestInterceptor, + "pre_get_authorized_certificate", + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.GetAuthorizedCertificateRequest.pb( + appengine.GetAuthorizedCertificateRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = certificate.AuthorizedCertificate.to_json( + certificate.AuthorizedCertificate() + ) + + request = appengine.GetAuthorizedCertificateRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = certificate.AuthorizedCertificate() + + client.get_authorized_certificate( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_get_authorized_certificate_rest_bad_request( + transport: str = "rest", request_type=appengine.GetAuthorizedCertificateRequest +): + client = AuthorizedCertificatesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/authorizedCertificates/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.get_authorized_certificate(request) + + +def test_get_authorized_certificate_rest_error(): + client = AuthorizedCertificatesClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.CreateAuthorizedCertificateRequest, + dict, + ], +) +def test_create_authorized_certificate_rest(request_type): + client = AuthorizedCertificatesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request_init["certificate"] = { + "name": "name_value", + "id": "id_value", + "display_name": "display_name_value", + "domain_names": ["domain_names_value1", "domain_names_value2"], + "expire_time": {"seconds": 751, "nanos": 543}, + "certificate_raw_data": { + "public_certificate": "public_certificate_value", + "private_key": "private_key_value", + }, + "managed_certificate": {"last_renewal_time": {}, "status": 1}, + "visible_domain_mappings": [ + "visible_domain_mappings_value1", + "visible_domain_mappings_value2", + ], + "domain_mappings_count": 2238, + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = certificate.AuthorizedCertificate( + name="name_value", + id="id_value", + display_name="display_name_value", + domain_names=["domain_names_value"], + visible_domain_mappings=["visible_domain_mappings_value"], + domain_mappings_count=2238, + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = certificate.AuthorizedCertificate.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.create_authorized_certificate(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, certificate.AuthorizedCertificate) + assert response.name == "name_value" + assert response.id == "id_value" + assert response.display_name == "display_name_value" + assert response.domain_names == ["domain_names_value"] + assert response.visible_domain_mappings == ["visible_domain_mappings_value"] + assert response.domain_mappings_count == 2238 + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_create_authorized_certificate_rest_interceptors(null_interceptor): + transport = transports.AuthorizedCertificatesRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None + if null_interceptor + else transports.AuthorizedCertificatesRestInterceptor(), + ) + client = AuthorizedCertificatesClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.AuthorizedCertificatesRestInterceptor, + "post_create_authorized_certificate", + ) as post, mock.patch.object( + transports.AuthorizedCertificatesRestInterceptor, + "pre_create_authorized_certificate", + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.CreateAuthorizedCertificateRequest.pb( + appengine.CreateAuthorizedCertificateRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = certificate.AuthorizedCertificate.to_json( + certificate.AuthorizedCertificate() + ) + + request = appengine.CreateAuthorizedCertificateRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = certificate.AuthorizedCertificate() + + client.create_authorized_certificate( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_create_authorized_certificate_rest_bad_request( + transport: str = "rest", request_type=appengine.CreateAuthorizedCertificateRequest +): + client = AuthorizedCertificatesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request_init["certificate"] = { + "name": "name_value", + "id": "id_value", + "display_name": "display_name_value", + "domain_names": ["domain_names_value1", "domain_names_value2"], + "expire_time": {"seconds": 751, "nanos": 543}, + "certificate_raw_data": { + "public_certificate": "public_certificate_value", + "private_key": "private_key_value", + }, + "managed_certificate": {"last_renewal_time": {}, "status": 1}, + "visible_domain_mappings": [ + "visible_domain_mappings_value1", + "visible_domain_mappings_value2", + ], + "domain_mappings_count": 2238, + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.create_authorized_certificate(request) + + +def test_create_authorized_certificate_rest_error(): + client = AuthorizedCertificatesClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.UpdateAuthorizedCertificateRequest, + dict, + ], +) +def test_update_authorized_certificate_rest(request_type): + client = AuthorizedCertificatesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/authorizedCertificates/sample2"} + request_init["certificate"] = { + "name": "name_value", + "id": "id_value", + "display_name": "display_name_value", + "domain_names": ["domain_names_value1", "domain_names_value2"], + "expire_time": {"seconds": 751, "nanos": 543}, + "certificate_raw_data": { + "public_certificate": "public_certificate_value", + "private_key": "private_key_value", + }, + "managed_certificate": {"last_renewal_time": {}, "status": 1}, + "visible_domain_mappings": [ + "visible_domain_mappings_value1", + "visible_domain_mappings_value2", + ], + "domain_mappings_count": 2238, + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = certificate.AuthorizedCertificate( + name="name_value", + id="id_value", + display_name="display_name_value", + domain_names=["domain_names_value"], + visible_domain_mappings=["visible_domain_mappings_value"], + domain_mappings_count=2238, + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = certificate.AuthorizedCertificate.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.update_authorized_certificate(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, certificate.AuthorizedCertificate) + assert response.name == "name_value" + assert response.id == "id_value" + assert response.display_name == "display_name_value" + assert response.domain_names == ["domain_names_value"] + assert response.visible_domain_mappings == ["visible_domain_mappings_value"] + assert response.domain_mappings_count == 2238 + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_update_authorized_certificate_rest_interceptors(null_interceptor): + transport = transports.AuthorizedCertificatesRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None + if null_interceptor + else transports.AuthorizedCertificatesRestInterceptor(), + ) + client = AuthorizedCertificatesClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.AuthorizedCertificatesRestInterceptor, + "post_update_authorized_certificate", + ) as post, mock.patch.object( + transports.AuthorizedCertificatesRestInterceptor, + "pre_update_authorized_certificate", + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.UpdateAuthorizedCertificateRequest.pb( + appengine.UpdateAuthorizedCertificateRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = certificate.AuthorizedCertificate.to_json( + certificate.AuthorizedCertificate() + ) + + request = appengine.UpdateAuthorizedCertificateRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = certificate.AuthorizedCertificate() + + client.update_authorized_certificate( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_update_authorized_certificate_rest_bad_request( + transport: str = "rest", request_type=appengine.UpdateAuthorizedCertificateRequest +): + client = AuthorizedCertificatesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/authorizedCertificates/sample2"} + request_init["certificate"] = { + "name": "name_value", + "id": "id_value", + "display_name": "display_name_value", + "domain_names": ["domain_names_value1", "domain_names_value2"], + "expire_time": {"seconds": 751, "nanos": 543}, + "certificate_raw_data": { + "public_certificate": "public_certificate_value", + "private_key": "private_key_value", + }, + "managed_certificate": {"last_renewal_time": {}, "status": 1}, + "visible_domain_mappings": [ + "visible_domain_mappings_value1", + "visible_domain_mappings_value2", + ], + "domain_mappings_count": 2238, + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.update_authorized_certificate(request) + + +def test_update_authorized_certificate_rest_error(): + client = AuthorizedCertificatesClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.DeleteAuthorizedCertificateRequest, + dict, + ], +) +def test_delete_authorized_certificate_rest(request_type): + client = AuthorizedCertificatesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/authorizedCertificates/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = None + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + json_return_value = "" + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.delete_authorized_certificate(request) + + # Establish that the response is the type that we expect. + assert response is None + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_delete_authorized_certificate_rest_interceptors(null_interceptor): + transport = transports.AuthorizedCertificatesRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None + if null_interceptor + else transports.AuthorizedCertificatesRestInterceptor(), + ) + client = AuthorizedCertificatesClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.AuthorizedCertificatesRestInterceptor, + "pre_delete_authorized_certificate", + ) as pre: + pre.assert_not_called() + pb_message = appengine.DeleteAuthorizedCertificateRequest.pb( + appengine.DeleteAuthorizedCertificateRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + + request = appengine.DeleteAuthorizedCertificateRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + + client.delete_authorized_certificate( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + + +def test_delete_authorized_certificate_rest_bad_request( + transport: str = "rest", request_type=appengine.DeleteAuthorizedCertificateRequest +): + client = AuthorizedCertificatesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/authorizedCertificates/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.delete_authorized_certificate(request) + + +def test_delete_authorized_certificate_rest_error(): + client = AuthorizedCertificatesClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + def test_credentials_transport_error(): # It is an error to provide credentials and a transport instance. transport = transports.AuthorizedCertificatesGrpcTransport( @@ -1841,6 +2670,7 @@ def test_transport_get_channel(): [ transports.AuthorizedCertificatesGrpcTransport, transports.AuthorizedCertificatesGrpcAsyncIOTransport, + transports.AuthorizedCertificatesRestTransport, ], ) def test_transport_adc(transport_class): @@ -1855,6 +2685,7 @@ def test_transport_adc(transport_class): "transport_name", [ "grpc", + "rest", ], ) def test_transport_kind(transport_name): @@ -2000,6 +2831,7 @@ def test_authorized_certificates_transport_auth_adc(transport_class): [ transports.AuthorizedCertificatesGrpcTransport, transports.AuthorizedCertificatesGrpcAsyncIOTransport, + transports.AuthorizedCertificatesRestTransport, ], ) def test_authorized_certificates_transport_auth_gdch_credentials(transport_class): @@ -2105,11 +2937,23 @@ def test_authorized_certificates_grpc_transport_client_cert_source_for_mtls( ) +def test_authorized_certificates_http_transport_client_cert_source_for_mtls(): + cred = ga_credentials.AnonymousCredentials() + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.configure_mtls_channel" + ) as mock_configure_mtls_channel: + transports.AuthorizedCertificatesRestTransport( + credentials=cred, client_cert_source_for_mtls=client_cert_source_callback + ) + mock_configure_mtls_channel.assert_called_once_with(client_cert_source_callback) + + @pytest.mark.parametrize( "transport_name", [ "grpc", "grpc_asyncio", + "rest", ], ) def test_authorized_certificates_host_no_port(transport_name): @@ -2120,7 +2964,11 @@ def test_authorized_certificates_host_no_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -2128,6 +2976,7 @@ def test_authorized_certificates_host_no_port(transport_name): [ "grpc", "grpc_asyncio", + "rest", ], ) def test_authorized_certificates_host_with_port(transport_name): @@ -2138,7 +2987,45 @@ def test_authorized_certificates_host_with_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:8000") + assert client.transport._host == ( + "appengine.googleapis.com:8000" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com:8000" + ) + + +@pytest.mark.parametrize( + "transport_name", + [ + "rest", + ], +) +def test_authorized_certificates_client_transport_session_collision(transport_name): + creds1 = ga_credentials.AnonymousCredentials() + creds2 = ga_credentials.AnonymousCredentials() + client1 = AuthorizedCertificatesClient( + credentials=creds1, + transport=transport_name, + ) + client2 = AuthorizedCertificatesClient( + credentials=creds2, + transport=transport_name, + ) + session1 = client1.transport.list_authorized_certificates._session + session2 = client2.transport.list_authorized_certificates._session + assert session1 != session2 + session1 = client1.transport.get_authorized_certificate._session + session2 = client2.transport.get_authorized_certificate._session + assert session1 != session2 + session1 = client1.transport.create_authorized_certificate._session + session2 = client2.transport.create_authorized_certificate._session + assert session1 != session2 + session1 = client1.transport.update_authorized_certificate._session + session2 = client2.transport.update_authorized_certificate._session + assert session1 != session2 + session1 = client1.transport.delete_authorized_certificate._session + session2 = client2.transport.delete_authorized_certificate._session + assert session1 != session2 def test_authorized_certificates_grpc_transport_channel(): @@ -2409,6 +3296,7 @@ async def test_transport_close_async(): def test_transport_close(): transports = { + "rest": "_session", "grpc": "_grpc_channel", } @@ -2426,6 +3314,7 @@ def test_transport_close(): def test_client_ctx(): transports = [ + "rest", "grpc", ] for transport in transports: diff --git a/tests/unit/gapic/appengine_admin_v1/test_authorized_domains.py b/tests/unit/gapic/appengine_admin_v1/test_authorized_domains.py index faee289..b4b6f36 100644 --- a/tests/unit/gapic/appengine_admin_v1/test_authorized_domains.py +++ b/tests/unit/gapic/appengine_admin_v1/test_authorized_domains.py @@ -22,6 +22,8 @@ except ImportError: # pragma: NO COVER import mock +from collections.abc import Iterable +import json import math from google.api_core import gapic_v1, grpc_helpers, grpc_helpers_async, path_template @@ -31,11 +33,14 @@ from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError from google.oauth2 import service_account +from google.protobuf import json_format import grpc from grpc.experimental import aio from proto.marshal.rules import wrappers from proto.marshal.rules.dates import DurationRule, TimestampRule import pytest +from requests import PreparedRequest, Request, Response +from requests.sessions import Session from google.cloud.appengine_admin_v1.services.authorized_domains import ( AuthorizedDomainsAsyncClient, @@ -96,6 +101,7 @@ def test__get_default_mtls_endpoint(): [ (AuthorizedDomainsClient, "grpc"), (AuthorizedDomainsAsyncClient, "grpc_asyncio"), + (AuthorizedDomainsClient, "rest"), ], ) def test_authorized_domains_client_from_service_account_info( @@ -111,7 +117,11 @@ def test_authorized_domains_client_from_service_account_info( assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -119,6 +129,7 @@ def test_authorized_domains_client_from_service_account_info( [ (transports.AuthorizedDomainsGrpcTransport, "grpc"), (transports.AuthorizedDomainsGrpcAsyncIOTransport, "grpc_asyncio"), + (transports.AuthorizedDomainsRestTransport, "rest"), ], ) def test_authorized_domains_client_service_account_always_use_jwt( @@ -144,6 +155,7 @@ def test_authorized_domains_client_service_account_always_use_jwt( [ (AuthorizedDomainsClient, "grpc"), (AuthorizedDomainsAsyncClient, "grpc_asyncio"), + (AuthorizedDomainsClient, "rest"), ], ) def test_authorized_domains_client_from_service_account_file( @@ -166,13 +178,18 @@ def test_authorized_domains_client_from_service_account_file( assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) def test_authorized_domains_client_get_transport_class(): transport = AuthorizedDomainsClient.get_transport_class() available_transports = [ transports.AuthorizedDomainsGrpcTransport, + transports.AuthorizedDomainsRestTransport, ] assert transport in available_transports @@ -189,6 +206,7 @@ def test_authorized_domains_client_get_transport_class(): transports.AuthorizedDomainsGrpcAsyncIOTransport, "grpc_asyncio", ), + (AuthorizedDomainsClient, transports.AuthorizedDomainsRestTransport, "rest"), ], ) @mock.patch.object( @@ -344,6 +362,18 @@ def test_authorized_domains_client_client_options( "grpc_asyncio", "false", ), + ( + AuthorizedDomainsClient, + transports.AuthorizedDomainsRestTransport, + "rest", + "true", + ), + ( + AuthorizedDomainsClient, + transports.AuthorizedDomainsRestTransport, + "rest", + "false", + ), ], ) @mock.patch.object( @@ -543,6 +573,7 @@ def test_authorized_domains_client_get_mtls_endpoint_and_cert_source(client_clas transports.AuthorizedDomainsGrpcAsyncIOTransport, "grpc_asyncio", ), + (AuthorizedDomainsClient, transports.AuthorizedDomainsRestTransport, "rest"), ], ) def test_authorized_domains_client_client_options_scopes( @@ -583,6 +614,12 @@ def test_authorized_domains_client_client_options_scopes( "grpc_asyncio", grpc_helpers_async, ), + ( + AuthorizedDomainsClient, + transports.AuthorizedDomainsRestTransport, + "rest", + None, + ), ], ) def test_authorized_domains_client_client_options_credentials_file( @@ -1055,6 +1092,189 @@ async def test_list_authorized_domains_async_pages(): assert page_.raw_page.next_page_token == token +@pytest.mark.parametrize( + "request_type", + [ + appengine.ListAuthorizedDomainsRequest, + dict, + ], +) +def test_list_authorized_domains_rest(request_type): + client = AuthorizedDomainsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = appengine.ListAuthorizedDomainsResponse( + next_page_token="next_page_token_value", + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = appengine.ListAuthorizedDomainsResponse.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.list_authorized_domains(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, pagers.ListAuthorizedDomainsPager) + assert response.next_page_token == "next_page_token_value" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_list_authorized_domains_rest_interceptors(null_interceptor): + transport = transports.AuthorizedDomainsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None + if null_interceptor + else transports.AuthorizedDomainsRestInterceptor(), + ) + client = AuthorizedDomainsClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.AuthorizedDomainsRestInterceptor, "post_list_authorized_domains" + ) as post, mock.patch.object( + transports.AuthorizedDomainsRestInterceptor, "pre_list_authorized_domains" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.ListAuthorizedDomainsRequest.pb( + appengine.ListAuthorizedDomainsRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = appengine.ListAuthorizedDomainsResponse.to_json( + appengine.ListAuthorizedDomainsResponse() + ) + + request = appengine.ListAuthorizedDomainsRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = appengine.ListAuthorizedDomainsResponse() + + client.list_authorized_domains( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_list_authorized_domains_rest_bad_request( + transport: str = "rest", request_type=appengine.ListAuthorizedDomainsRequest +): + client = AuthorizedDomainsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.list_authorized_domains(request) + + +def test_list_authorized_domains_rest_pager(transport: str = "rest"): + client = AuthorizedDomainsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(Session, "request") as req: + # TODO(kbandes): remove this mock unless there's a good reason for it. + # with mock.patch.object(path_template, 'transcode') as transcode: + # Set the response as a series of pages + response = ( + appengine.ListAuthorizedDomainsResponse( + domains=[ + domain.AuthorizedDomain(), + domain.AuthorizedDomain(), + domain.AuthorizedDomain(), + ], + next_page_token="abc", + ), + appengine.ListAuthorizedDomainsResponse( + domains=[], + next_page_token="def", + ), + appengine.ListAuthorizedDomainsResponse( + domains=[ + domain.AuthorizedDomain(), + ], + next_page_token="ghi", + ), + appengine.ListAuthorizedDomainsResponse( + domains=[ + domain.AuthorizedDomain(), + domain.AuthorizedDomain(), + ], + ), + ) + # Two responses for two calls + response = response + response + + # Wrap the values into proper Response objs + response = tuple( + appengine.ListAuthorizedDomainsResponse.to_json(x) for x in response + ) + return_values = tuple(Response() for i in response) + for return_val, response_val in zip(return_values, response): + return_val._content = response_val.encode("UTF-8") + return_val.status_code = 200 + req.side_effect = return_values + + sample_request = {"parent": "apps/sample1"} + + pager = client.list_authorized_domains(request=sample_request) + + results = list(pager) + assert len(results) == 6 + assert all(isinstance(i, domain.AuthorizedDomain) for i in results) + + pages = list(client.list_authorized_domains(request=sample_request).pages) + for page_, token in zip(pages, ["abc", "def", "ghi", ""]): + assert page_.raw_page.next_page_token == token + + def test_credentials_transport_error(): # It is an error to provide credentials and a transport instance. transport = transports.AuthorizedDomainsGrpcTransport( @@ -1136,6 +1356,7 @@ def test_transport_get_channel(): [ transports.AuthorizedDomainsGrpcTransport, transports.AuthorizedDomainsGrpcAsyncIOTransport, + transports.AuthorizedDomainsRestTransport, ], ) def test_transport_adc(transport_class): @@ -1150,6 +1371,7 @@ def test_transport_adc(transport_class): "transport_name", [ "grpc", + "rest", ], ) def test_transport_kind(transport_name): @@ -1289,6 +1511,7 @@ def test_authorized_domains_transport_auth_adc(transport_class): [ transports.AuthorizedDomainsGrpcTransport, transports.AuthorizedDomainsGrpcAsyncIOTransport, + transports.AuthorizedDomainsRestTransport, ], ) def test_authorized_domains_transport_auth_gdch_credentials(transport_class): @@ -1390,11 +1613,23 @@ def test_authorized_domains_grpc_transport_client_cert_source_for_mtls(transport ) +def test_authorized_domains_http_transport_client_cert_source_for_mtls(): + cred = ga_credentials.AnonymousCredentials() + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.configure_mtls_channel" + ) as mock_configure_mtls_channel: + transports.AuthorizedDomainsRestTransport( + credentials=cred, client_cert_source_for_mtls=client_cert_source_callback + ) + mock_configure_mtls_channel.assert_called_once_with(client_cert_source_callback) + + @pytest.mark.parametrize( "transport_name", [ "grpc", "grpc_asyncio", + "rest", ], ) def test_authorized_domains_host_no_port(transport_name): @@ -1405,7 +1640,11 @@ def test_authorized_domains_host_no_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -1413,6 +1652,7 @@ def test_authorized_domains_host_no_port(transport_name): [ "grpc", "grpc_asyncio", + "rest", ], ) def test_authorized_domains_host_with_port(transport_name): @@ -1423,7 +1663,33 @@ def test_authorized_domains_host_with_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:8000") + assert client.transport._host == ( + "appengine.googleapis.com:8000" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com:8000" + ) + + +@pytest.mark.parametrize( + "transport_name", + [ + "rest", + ], +) +def test_authorized_domains_client_transport_session_collision(transport_name): + creds1 = ga_credentials.AnonymousCredentials() + creds2 = ga_credentials.AnonymousCredentials() + client1 = AuthorizedDomainsClient( + credentials=creds1, + transport=transport_name, + ) + client2 = AuthorizedDomainsClient( + credentials=creds2, + transport=transport_name, + ) + session1 = client1.transport.list_authorized_domains._session + session2 = client2.transport.list_authorized_domains._session + assert session1 != session2 def test_authorized_domains_grpc_transport_channel(): @@ -1694,6 +1960,7 @@ async def test_transport_close_async(): def test_transport_close(): transports = { + "rest": "_session", "grpc": "_grpc_channel", } @@ -1711,6 +1978,7 @@ def test_transport_close(): def test_client_ctx(): transports = [ + "rest", "grpc", ] for transport in transports: diff --git a/tests/unit/gapic/appengine_admin_v1/test_domain_mappings.py b/tests/unit/gapic/appengine_admin_v1/test_domain_mappings.py index e7095b9..ce41f03 100644 --- a/tests/unit/gapic/appengine_admin_v1/test_domain_mappings.py +++ b/tests/unit/gapic/appengine_admin_v1/test_domain_mappings.py @@ -22,6 +22,8 @@ except ImportError: # pragma: NO COVER import mock +from collections.abc import Iterable +import json import math from google.api_core import ( @@ -43,11 +45,14 @@ from google.oauth2 import service_account from google.protobuf import empty_pb2 # type: ignore from google.protobuf import field_mask_pb2 # type: ignore +from google.protobuf import json_format import grpc from grpc.experimental import aio from proto.marshal.rules import wrappers from proto.marshal.rules.dates import DurationRule, TimestampRule import pytest +from requests import PreparedRequest, Request, Response +from requests.sessions import Session from google.cloud.appengine_admin_v1.services.domain_mappings import ( DomainMappingsAsyncClient, @@ -108,6 +113,7 @@ def test__get_default_mtls_endpoint(): [ (DomainMappingsClient, "grpc"), (DomainMappingsAsyncClient, "grpc_asyncio"), + (DomainMappingsClient, "rest"), ], ) def test_domain_mappings_client_from_service_account_info(client_class, transport_name): @@ -121,7 +127,11 @@ def test_domain_mappings_client_from_service_account_info(client_class, transpor assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -129,6 +139,7 @@ def test_domain_mappings_client_from_service_account_info(client_class, transpor [ (transports.DomainMappingsGrpcTransport, "grpc"), (transports.DomainMappingsGrpcAsyncIOTransport, "grpc_asyncio"), + (transports.DomainMappingsRestTransport, "rest"), ], ) def test_domain_mappings_client_service_account_always_use_jwt( @@ -154,6 +165,7 @@ def test_domain_mappings_client_service_account_always_use_jwt( [ (DomainMappingsClient, "grpc"), (DomainMappingsAsyncClient, "grpc_asyncio"), + (DomainMappingsClient, "rest"), ], ) def test_domain_mappings_client_from_service_account_file(client_class, transport_name): @@ -174,13 +186,18 @@ def test_domain_mappings_client_from_service_account_file(client_class, transpor assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) def test_domain_mappings_client_get_transport_class(): transport = DomainMappingsClient.get_transport_class() available_transports = [ transports.DomainMappingsGrpcTransport, + transports.DomainMappingsRestTransport, ] assert transport in available_transports @@ -197,6 +214,7 @@ def test_domain_mappings_client_get_transport_class(): transports.DomainMappingsGrpcAsyncIOTransport, "grpc_asyncio", ), + (DomainMappingsClient, transports.DomainMappingsRestTransport, "rest"), ], ) @mock.patch.object( @@ -342,6 +360,8 @@ def test_domain_mappings_client_client_options( "grpc_asyncio", "false", ), + (DomainMappingsClient, transports.DomainMappingsRestTransport, "rest", "true"), + (DomainMappingsClient, transports.DomainMappingsRestTransport, "rest", "false"), ], ) @mock.patch.object( @@ -541,6 +561,7 @@ def test_domain_mappings_client_get_mtls_endpoint_and_cert_source(client_class): transports.DomainMappingsGrpcAsyncIOTransport, "grpc_asyncio", ), + (DomainMappingsClient, transports.DomainMappingsRestTransport, "rest"), ], ) def test_domain_mappings_client_client_options_scopes( @@ -581,6 +602,7 @@ def test_domain_mappings_client_client_options_scopes( "grpc_asyncio", grpc_helpers_async, ), + (DomainMappingsClient, transports.DomainMappingsRestTransport, "rest", None), ], ) def test_domain_mappings_client_client_options_credentials_file( @@ -1679,6 +1701,737 @@ async def test_delete_domain_mapping_field_headers_async(): ) in kw["metadata"] +@pytest.mark.parametrize( + "request_type", + [ + appengine.ListDomainMappingsRequest, + dict, + ], +) +def test_list_domain_mappings_rest(request_type): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = appengine.ListDomainMappingsResponse( + next_page_token="next_page_token_value", + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = appengine.ListDomainMappingsResponse.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.list_domain_mappings(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, pagers.ListDomainMappingsPager) + assert response.next_page_token == "next_page_token_value" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_list_domain_mappings_rest_interceptors(null_interceptor): + transport = transports.DomainMappingsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None + if null_interceptor + else transports.DomainMappingsRestInterceptor(), + ) + client = DomainMappingsClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.DomainMappingsRestInterceptor, "post_list_domain_mappings" + ) as post, mock.patch.object( + transports.DomainMappingsRestInterceptor, "pre_list_domain_mappings" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.ListDomainMappingsRequest.pb( + appengine.ListDomainMappingsRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = appengine.ListDomainMappingsResponse.to_json( + appengine.ListDomainMappingsResponse() + ) + + request = appengine.ListDomainMappingsRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = appengine.ListDomainMappingsResponse() + + client.list_domain_mappings( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_list_domain_mappings_rest_bad_request( + transport: str = "rest", request_type=appengine.ListDomainMappingsRequest +): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.list_domain_mappings(request) + + +def test_list_domain_mappings_rest_pager(transport: str = "rest"): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(Session, "request") as req: + # TODO(kbandes): remove this mock unless there's a good reason for it. + # with mock.patch.object(path_template, 'transcode') as transcode: + # Set the response as a series of pages + response = ( + appengine.ListDomainMappingsResponse( + domain_mappings=[ + domain_mapping.DomainMapping(), + domain_mapping.DomainMapping(), + domain_mapping.DomainMapping(), + ], + next_page_token="abc", + ), + appengine.ListDomainMappingsResponse( + domain_mappings=[], + next_page_token="def", + ), + appengine.ListDomainMappingsResponse( + domain_mappings=[ + domain_mapping.DomainMapping(), + ], + next_page_token="ghi", + ), + appengine.ListDomainMappingsResponse( + domain_mappings=[ + domain_mapping.DomainMapping(), + domain_mapping.DomainMapping(), + ], + ), + ) + # Two responses for two calls + response = response + response + + # Wrap the values into proper Response objs + response = tuple( + appengine.ListDomainMappingsResponse.to_json(x) for x in response + ) + return_values = tuple(Response() for i in response) + for return_val, response_val in zip(return_values, response): + return_val._content = response_val.encode("UTF-8") + return_val.status_code = 200 + req.side_effect = return_values + + sample_request = {"parent": "apps/sample1"} + + pager = client.list_domain_mappings(request=sample_request) + + results = list(pager) + assert len(results) == 6 + assert all(isinstance(i, domain_mapping.DomainMapping) for i in results) + + pages = list(client.list_domain_mappings(request=sample_request).pages) + for page_, token in zip(pages, ["abc", "def", "ghi", ""]): + assert page_.raw_page.next_page_token == token + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.GetDomainMappingRequest, + dict, + ], +) +def test_get_domain_mapping_rest(request_type): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/domainMappings/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = domain_mapping.DomainMapping( + name="name_value", + id="id_value", + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = domain_mapping.DomainMapping.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.get_domain_mapping(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, domain_mapping.DomainMapping) + assert response.name == "name_value" + assert response.id == "id_value" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_get_domain_mapping_rest_interceptors(null_interceptor): + transport = transports.DomainMappingsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None + if null_interceptor + else transports.DomainMappingsRestInterceptor(), + ) + client = DomainMappingsClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.DomainMappingsRestInterceptor, "post_get_domain_mapping" + ) as post, mock.patch.object( + transports.DomainMappingsRestInterceptor, "pre_get_domain_mapping" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.GetDomainMappingRequest.pb( + appengine.GetDomainMappingRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = domain_mapping.DomainMapping.to_json( + domain_mapping.DomainMapping() + ) + + request = appengine.GetDomainMappingRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = domain_mapping.DomainMapping() + + client.get_domain_mapping( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_get_domain_mapping_rest_bad_request( + transport: str = "rest", request_type=appengine.GetDomainMappingRequest +): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/domainMappings/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.get_domain_mapping(request) + + +def test_get_domain_mapping_rest_error(): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.CreateDomainMappingRequest, + dict, + ], +) +def test_create_domain_mapping_rest(request_type): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request_init["domain_mapping"] = { + "name": "name_value", + "id": "id_value", + "ssl_settings": { + "certificate_id": "certificate_id_value", + "ssl_management_type": 1, + "pending_managed_certificate_id": "pending_managed_certificate_id_value", + }, + "resource_records": [ + {"name": "name_value", "rrdata": "rrdata_value", "type_": 1} + ], + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = operations_pb2.Operation(name="operations/spam") + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + json_return_value = json_format.MessageToJson(return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.create_domain_mapping(request) + + # Establish that the response is the type that we expect. + assert response.operation.name == "operations/spam" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_create_domain_mapping_rest_interceptors(null_interceptor): + transport = transports.DomainMappingsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None + if null_interceptor + else transports.DomainMappingsRestInterceptor(), + ) + client = DomainMappingsClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + operation.Operation, "_set_result_from_operation" + ), mock.patch.object( + transports.DomainMappingsRestInterceptor, "post_create_domain_mapping" + ) as post, mock.patch.object( + transports.DomainMappingsRestInterceptor, "pre_create_domain_mapping" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.CreateDomainMappingRequest.pb( + appengine.CreateDomainMappingRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = json_format.MessageToJson( + operations_pb2.Operation() + ) + + request = appengine.CreateDomainMappingRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = operations_pb2.Operation() + + client.create_domain_mapping( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_create_domain_mapping_rest_bad_request( + transport: str = "rest", request_type=appengine.CreateDomainMappingRequest +): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request_init["domain_mapping"] = { + "name": "name_value", + "id": "id_value", + "ssl_settings": { + "certificate_id": "certificate_id_value", + "ssl_management_type": 1, + "pending_managed_certificate_id": "pending_managed_certificate_id_value", + }, + "resource_records": [ + {"name": "name_value", "rrdata": "rrdata_value", "type_": 1} + ], + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.create_domain_mapping(request) + + +def test_create_domain_mapping_rest_error(): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.UpdateDomainMappingRequest, + dict, + ], +) +def test_update_domain_mapping_rest(request_type): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/domainMappings/sample2"} + request_init["domain_mapping"] = { + "name": "name_value", + "id": "id_value", + "ssl_settings": { + "certificate_id": "certificate_id_value", + "ssl_management_type": 1, + "pending_managed_certificate_id": "pending_managed_certificate_id_value", + }, + "resource_records": [ + {"name": "name_value", "rrdata": "rrdata_value", "type_": 1} + ], + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = operations_pb2.Operation(name="operations/spam") + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + json_return_value = json_format.MessageToJson(return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.update_domain_mapping(request) + + # Establish that the response is the type that we expect. + assert response.operation.name == "operations/spam" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_update_domain_mapping_rest_interceptors(null_interceptor): + transport = transports.DomainMappingsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None + if null_interceptor + else transports.DomainMappingsRestInterceptor(), + ) + client = DomainMappingsClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + operation.Operation, "_set_result_from_operation" + ), mock.patch.object( + transports.DomainMappingsRestInterceptor, "post_update_domain_mapping" + ) as post, mock.patch.object( + transports.DomainMappingsRestInterceptor, "pre_update_domain_mapping" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.UpdateDomainMappingRequest.pb( + appengine.UpdateDomainMappingRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = json_format.MessageToJson( + operations_pb2.Operation() + ) + + request = appengine.UpdateDomainMappingRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = operations_pb2.Operation() + + client.update_domain_mapping( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_update_domain_mapping_rest_bad_request( + transport: str = "rest", request_type=appengine.UpdateDomainMappingRequest +): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/domainMappings/sample2"} + request_init["domain_mapping"] = { + "name": "name_value", + "id": "id_value", + "ssl_settings": { + "certificate_id": "certificate_id_value", + "ssl_management_type": 1, + "pending_managed_certificate_id": "pending_managed_certificate_id_value", + }, + "resource_records": [ + {"name": "name_value", "rrdata": "rrdata_value", "type_": 1} + ], + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.update_domain_mapping(request) + + +def test_update_domain_mapping_rest_error(): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.DeleteDomainMappingRequest, + dict, + ], +) +def test_delete_domain_mapping_rest(request_type): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/domainMappings/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = operations_pb2.Operation(name="operations/spam") + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + json_return_value = json_format.MessageToJson(return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.delete_domain_mapping(request) + + # Establish that the response is the type that we expect. + assert response.operation.name == "operations/spam" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_delete_domain_mapping_rest_interceptors(null_interceptor): + transport = transports.DomainMappingsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None + if null_interceptor + else transports.DomainMappingsRestInterceptor(), + ) + client = DomainMappingsClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + operation.Operation, "_set_result_from_operation" + ), mock.patch.object( + transports.DomainMappingsRestInterceptor, "post_delete_domain_mapping" + ) as post, mock.patch.object( + transports.DomainMappingsRestInterceptor, "pre_delete_domain_mapping" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.DeleteDomainMappingRequest.pb( + appengine.DeleteDomainMappingRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = json_format.MessageToJson( + operations_pb2.Operation() + ) + + request = appengine.DeleteDomainMappingRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = operations_pb2.Operation() + + client.delete_domain_mapping( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_delete_domain_mapping_rest_bad_request( + transport: str = "rest", request_type=appengine.DeleteDomainMappingRequest +): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/domainMappings/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.delete_domain_mapping(request) + + +def test_delete_domain_mapping_rest_error(): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + def test_credentials_transport_error(): # It is an error to provide credentials and a transport instance. transport = transports.DomainMappingsGrpcTransport( @@ -1760,6 +2513,7 @@ def test_transport_get_channel(): [ transports.DomainMappingsGrpcTransport, transports.DomainMappingsGrpcAsyncIOTransport, + transports.DomainMappingsRestTransport, ], ) def test_transport_adc(transport_class): @@ -1774,6 +2528,7 @@ def test_transport_adc(transport_class): "transport_name", [ "grpc", + "rest", ], ) def test_transport_kind(transport_name): @@ -1924,6 +2679,7 @@ def test_domain_mappings_transport_auth_adc(transport_class): [ transports.DomainMappingsGrpcTransport, transports.DomainMappingsGrpcAsyncIOTransport, + transports.DomainMappingsRestTransport, ], ) def test_domain_mappings_transport_auth_gdch_credentials(transport_class): @@ -2025,11 +2781,40 @@ def test_domain_mappings_grpc_transport_client_cert_source_for_mtls(transport_cl ) +def test_domain_mappings_http_transport_client_cert_source_for_mtls(): + cred = ga_credentials.AnonymousCredentials() + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.configure_mtls_channel" + ) as mock_configure_mtls_channel: + transports.DomainMappingsRestTransport( + credentials=cred, client_cert_source_for_mtls=client_cert_source_callback + ) + mock_configure_mtls_channel.assert_called_once_with(client_cert_source_callback) + + +def test_domain_mappings_rest_lro_client(): + client = DomainMappingsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + transport = client.transport + + # Ensure that we have a api-core operations client. + assert isinstance( + transport.operations_client, + operations_v1.AbstractOperationsClient, + ) + + # Ensure that subsequent calls to the property send the exact same object. + assert transport.operations_client is transport.operations_client + + @pytest.mark.parametrize( "transport_name", [ "grpc", "grpc_asyncio", + "rest", ], ) def test_domain_mappings_host_no_port(transport_name): @@ -2040,7 +2825,11 @@ def test_domain_mappings_host_no_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -2048,6 +2837,7 @@ def test_domain_mappings_host_no_port(transport_name): [ "grpc", "grpc_asyncio", + "rest", ], ) def test_domain_mappings_host_with_port(transport_name): @@ -2058,7 +2848,45 @@ def test_domain_mappings_host_with_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:8000") + assert client.transport._host == ( + "appengine.googleapis.com:8000" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com:8000" + ) + + +@pytest.mark.parametrize( + "transport_name", + [ + "rest", + ], +) +def test_domain_mappings_client_transport_session_collision(transport_name): + creds1 = ga_credentials.AnonymousCredentials() + creds2 = ga_credentials.AnonymousCredentials() + client1 = DomainMappingsClient( + credentials=creds1, + transport=transport_name, + ) + client2 = DomainMappingsClient( + credentials=creds2, + transport=transport_name, + ) + session1 = client1.transport.list_domain_mappings._session + session2 = client2.transport.list_domain_mappings._session + assert session1 != session2 + session1 = client1.transport.get_domain_mapping._session + session2 = client2.transport.get_domain_mapping._session + assert session1 != session2 + session1 = client1.transport.create_domain_mapping._session + session2 = client2.transport.create_domain_mapping._session + assert session1 != session2 + session1 = client1.transport.update_domain_mapping._session + session2 = client2.transport.update_domain_mapping._session + assert session1 != session2 + session1 = client1.transport.delete_domain_mapping._session + session2 = client2.transport.delete_domain_mapping._session + assert session1 != session2 def test_domain_mappings_grpc_transport_channel(): @@ -2363,6 +3191,7 @@ async def test_transport_close_async(): def test_transport_close(): transports = { + "rest": "_session", "grpc": "_grpc_channel", } @@ -2380,6 +3209,7 @@ def test_transport_close(): def test_client_ctx(): transports = [ + "rest", "grpc", ] for transport in transports: diff --git a/tests/unit/gapic/appengine_admin_v1/test_firewall.py b/tests/unit/gapic/appengine_admin_v1/test_firewall.py index b0fef84..bf32eb1 100644 --- a/tests/unit/gapic/appengine_admin_v1/test_firewall.py +++ b/tests/unit/gapic/appengine_admin_v1/test_firewall.py @@ -22,6 +22,8 @@ except ImportError: # pragma: NO COVER import mock +from collections.abc import Iterable +import json import math from google.api_core import gapic_v1, grpc_helpers, grpc_helpers_async, path_template @@ -32,11 +34,14 @@ from google.auth.exceptions import MutualTLSChannelError from google.oauth2 import service_account from google.protobuf import field_mask_pb2 # type: ignore +from google.protobuf import json_format import grpc from grpc.experimental import aio from proto.marshal.rules import wrappers from proto.marshal.rules.dates import DurationRule, TimestampRule import pytest +from requests import PreparedRequest, Request, Response +from requests.sessions import Session from google.cloud.appengine_admin_v1.services.firewall import ( FirewallAsyncClient, @@ -91,6 +96,7 @@ def test__get_default_mtls_endpoint(): [ (FirewallClient, "grpc"), (FirewallAsyncClient, "grpc_asyncio"), + (FirewallClient, "rest"), ], ) def test_firewall_client_from_service_account_info(client_class, transport_name): @@ -104,7 +110,11 @@ def test_firewall_client_from_service_account_info(client_class, transport_name) assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -112,6 +122,7 @@ def test_firewall_client_from_service_account_info(client_class, transport_name) [ (transports.FirewallGrpcTransport, "grpc"), (transports.FirewallGrpcAsyncIOTransport, "grpc_asyncio"), + (transports.FirewallRestTransport, "rest"), ], ) def test_firewall_client_service_account_always_use_jwt( @@ -137,6 +148,7 @@ def test_firewall_client_service_account_always_use_jwt( [ (FirewallClient, "grpc"), (FirewallAsyncClient, "grpc_asyncio"), + (FirewallClient, "rest"), ], ) def test_firewall_client_from_service_account_file(client_class, transport_name): @@ -157,13 +169,18 @@ def test_firewall_client_from_service_account_file(client_class, transport_name) assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) def test_firewall_client_get_transport_class(): transport = FirewallClient.get_transport_class() available_transports = [ transports.FirewallGrpcTransport, + transports.FirewallRestTransport, ] assert transport in available_transports @@ -176,6 +193,7 @@ def test_firewall_client_get_transport_class(): [ (FirewallClient, transports.FirewallGrpcTransport, "grpc"), (FirewallAsyncClient, transports.FirewallGrpcAsyncIOTransport, "grpc_asyncio"), + (FirewallClient, transports.FirewallRestTransport, "rest"), ], ) @mock.patch.object( @@ -317,6 +335,8 @@ def test_firewall_client_client_options(client_class, transport_class, transport "grpc_asyncio", "false", ), + (FirewallClient, transports.FirewallRestTransport, "rest", "true"), + (FirewallClient, transports.FirewallRestTransport, "rest", "false"), ], ) @mock.patch.object( @@ -506,6 +526,7 @@ def test_firewall_client_get_mtls_endpoint_and_cert_source(client_class): [ (FirewallClient, transports.FirewallGrpcTransport, "grpc"), (FirewallAsyncClient, transports.FirewallGrpcAsyncIOTransport, "grpc_asyncio"), + (FirewallClient, transports.FirewallRestTransport, "rest"), ], ) def test_firewall_client_client_options_scopes( @@ -541,6 +562,7 @@ def test_firewall_client_client_options_scopes( "grpc_asyncio", grpc_helpers_async, ), + (FirewallClient, transports.FirewallRestTransport, "rest", None), ], ) def test_firewall_client_client_options_credentials_file( @@ -1817,6 +1839,834 @@ async def test_delete_ingress_rule_field_headers_async(): ) in kw["metadata"] +@pytest.mark.parametrize( + "request_type", + [ + appengine.ListIngressRulesRequest, + dict, + ], +) +def test_list_ingress_rules_rest(request_type): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = appengine.ListIngressRulesResponse( + next_page_token="next_page_token_value", + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = appengine.ListIngressRulesResponse.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.list_ingress_rules(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, pagers.ListIngressRulesPager) + assert response.next_page_token == "next_page_token_value" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_list_ingress_rules_rest_interceptors(null_interceptor): + transport = transports.FirewallRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.FirewallRestInterceptor(), + ) + client = FirewallClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.FirewallRestInterceptor, "post_list_ingress_rules" + ) as post, mock.patch.object( + transports.FirewallRestInterceptor, "pre_list_ingress_rules" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.ListIngressRulesRequest.pb( + appengine.ListIngressRulesRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = appengine.ListIngressRulesResponse.to_json( + appengine.ListIngressRulesResponse() + ) + + request = appengine.ListIngressRulesRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = appengine.ListIngressRulesResponse() + + client.list_ingress_rules( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_list_ingress_rules_rest_bad_request( + transport: str = "rest", request_type=appengine.ListIngressRulesRequest +): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.list_ingress_rules(request) + + +def test_list_ingress_rules_rest_pager(transport: str = "rest"): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(Session, "request") as req: + # TODO(kbandes): remove this mock unless there's a good reason for it. + # with mock.patch.object(path_template, 'transcode') as transcode: + # Set the response as a series of pages + response = ( + appengine.ListIngressRulesResponse( + ingress_rules=[ + firewall.FirewallRule(), + firewall.FirewallRule(), + firewall.FirewallRule(), + ], + next_page_token="abc", + ), + appengine.ListIngressRulesResponse( + ingress_rules=[], + next_page_token="def", + ), + appengine.ListIngressRulesResponse( + ingress_rules=[ + firewall.FirewallRule(), + ], + next_page_token="ghi", + ), + appengine.ListIngressRulesResponse( + ingress_rules=[ + firewall.FirewallRule(), + firewall.FirewallRule(), + ], + ), + ) + # Two responses for two calls + response = response + response + + # Wrap the values into proper Response objs + response = tuple( + appengine.ListIngressRulesResponse.to_json(x) for x in response + ) + return_values = tuple(Response() for i in response) + for return_val, response_val in zip(return_values, response): + return_val._content = response_val.encode("UTF-8") + return_val.status_code = 200 + req.side_effect = return_values + + sample_request = {"parent": "apps/sample1"} + + pager = client.list_ingress_rules(request=sample_request) + + results = list(pager) + assert len(results) == 6 + assert all(isinstance(i, firewall.FirewallRule) for i in results) + + pages = list(client.list_ingress_rules(request=sample_request).pages) + for page_, token in zip(pages, ["abc", "def", "ghi", ""]): + assert page_.raw_page.next_page_token == token + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.BatchUpdateIngressRulesRequest, + dict, + ], +) +def test_batch_update_ingress_rules_rest(request_type): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/firewall/ingressRules"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = appengine.BatchUpdateIngressRulesResponse() + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = appengine.BatchUpdateIngressRulesResponse.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.batch_update_ingress_rules(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, appengine.BatchUpdateIngressRulesResponse) + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_batch_update_ingress_rules_rest_interceptors(null_interceptor): + transport = transports.FirewallRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.FirewallRestInterceptor(), + ) + client = FirewallClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.FirewallRestInterceptor, "post_batch_update_ingress_rules" + ) as post, mock.patch.object( + transports.FirewallRestInterceptor, "pre_batch_update_ingress_rules" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.BatchUpdateIngressRulesRequest.pb( + appengine.BatchUpdateIngressRulesRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = appengine.BatchUpdateIngressRulesResponse.to_json( + appengine.BatchUpdateIngressRulesResponse() + ) + + request = appengine.BatchUpdateIngressRulesRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = appengine.BatchUpdateIngressRulesResponse() + + client.batch_update_ingress_rules( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_batch_update_ingress_rules_rest_bad_request( + transport: str = "rest", request_type=appengine.BatchUpdateIngressRulesRequest +): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/firewall/ingressRules"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.batch_update_ingress_rules(request) + + +def test_batch_update_ingress_rules_rest_error(): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.CreateIngressRuleRequest, + dict, + ], +) +def test_create_ingress_rule_rest(request_type): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request_init["rule"] = { + "priority": 898, + "action": 1, + "source_range": "source_range_value", + "description": "description_value", + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = firewall.FirewallRule( + priority=898, + action=firewall.FirewallRule.Action.ALLOW, + source_range="source_range_value", + description="description_value", + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = firewall.FirewallRule.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.create_ingress_rule(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, firewall.FirewallRule) + assert response.priority == 898 + assert response.action == firewall.FirewallRule.Action.ALLOW + assert response.source_range == "source_range_value" + assert response.description == "description_value" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_create_ingress_rule_rest_interceptors(null_interceptor): + transport = transports.FirewallRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.FirewallRestInterceptor(), + ) + client = FirewallClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.FirewallRestInterceptor, "post_create_ingress_rule" + ) as post, mock.patch.object( + transports.FirewallRestInterceptor, "pre_create_ingress_rule" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.CreateIngressRuleRequest.pb( + appengine.CreateIngressRuleRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = firewall.FirewallRule.to_json( + firewall.FirewallRule() + ) + + request = appengine.CreateIngressRuleRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = firewall.FirewallRule() + + client.create_ingress_rule( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_create_ingress_rule_rest_bad_request( + transport: str = "rest", request_type=appengine.CreateIngressRuleRequest +): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request_init["rule"] = { + "priority": 898, + "action": 1, + "source_range": "source_range_value", + "description": "description_value", + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.create_ingress_rule(request) + + +def test_create_ingress_rule_rest_error(): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.GetIngressRuleRequest, + dict, + ], +) +def test_get_ingress_rule_rest(request_type): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/firewall/ingressRules/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = firewall.FirewallRule( + priority=898, + action=firewall.FirewallRule.Action.ALLOW, + source_range="source_range_value", + description="description_value", + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = firewall.FirewallRule.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.get_ingress_rule(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, firewall.FirewallRule) + assert response.priority == 898 + assert response.action == firewall.FirewallRule.Action.ALLOW + assert response.source_range == "source_range_value" + assert response.description == "description_value" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_get_ingress_rule_rest_interceptors(null_interceptor): + transport = transports.FirewallRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.FirewallRestInterceptor(), + ) + client = FirewallClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.FirewallRestInterceptor, "post_get_ingress_rule" + ) as post, mock.patch.object( + transports.FirewallRestInterceptor, "pre_get_ingress_rule" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.GetIngressRuleRequest.pb( + appengine.GetIngressRuleRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = firewall.FirewallRule.to_json( + firewall.FirewallRule() + ) + + request = appengine.GetIngressRuleRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = firewall.FirewallRule() + + client.get_ingress_rule( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_get_ingress_rule_rest_bad_request( + transport: str = "rest", request_type=appengine.GetIngressRuleRequest +): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/firewall/ingressRules/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.get_ingress_rule(request) + + +def test_get_ingress_rule_rest_error(): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.UpdateIngressRuleRequest, + dict, + ], +) +def test_update_ingress_rule_rest(request_type): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/firewall/ingressRules/sample2"} + request_init["rule"] = { + "priority": 898, + "action": 1, + "source_range": "source_range_value", + "description": "description_value", + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = firewall.FirewallRule( + priority=898, + action=firewall.FirewallRule.Action.ALLOW, + source_range="source_range_value", + description="description_value", + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = firewall.FirewallRule.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.update_ingress_rule(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, firewall.FirewallRule) + assert response.priority == 898 + assert response.action == firewall.FirewallRule.Action.ALLOW + assert response.source_range == "source_range_value" + assert response.description == "description_value" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_update_ingress_rule_rest_interceptors(null_interceptor): + transport = transports.FirewallRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.FirewallRestInterceptor(), + ) + client = FirewallClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.FirewallRestInterceptor, "post_update_ingress_rule" + ) as post, mock.patch.object( + transports.FirewallRestInterceptor, "pre_update_ingress_rule" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.UpdateIngressRuleRequest.pb( + appengine.UpdateIngressRuleRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = firewall.FirewallRule.to_json( + firewall.FirewallRule() + ) + + request = appengine.UpdateIngressRuleRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = firewall.FirewallRule() + + client.update_ingress_rule( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_update_ingress_rule_rest_bad_request( + transport: str = "rest", request_type=appengine.UpdateIngressRuleRequest +): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/firewall/ingressRules/sample2"} + request_init["rule"] = { + "priority": 898, + "action": 1, + "source_range": "source_range_value", + "description": "description_value", + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.update_ingress_rule(request) + + +def test_update_ingress_rule_rest_error(): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.DeleteIngressRuleRequest, + dict, + ], +) +def test_delete_ingress_rule_rest(request_type): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/firewall/ingressRules/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = None + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + json_return_value = "" + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.delete_ingress_rule(request) + + # Establish that the response is the type that we expect. + assert response is None + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_delete_ingress_rule_rest_interceptors(null_interceptor): + transport = transports.FirewallRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.FirewallRestInterceptor(), + ) + client = FirewallClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.FirewallRestInterceptor, "pre_delete_ingress_rule" + ) as pre: + pre.assert_not_called() + pb_message = appengine.DeleteIngressRuleRequest.pb( + appengine.DeleteIngressRuleRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + + request = appengine.DeleteIngressRuleRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + + client.delete_ingress_rule( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + + +def test_delete_ingress_rule_rest_bad_request( + transport: str = "rest", request_type=appengine.DeleteIngressRuleRequest +): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/firewall/ingressRules/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.delete_ingress_rule(request) + + +def test_delete_ingress_rule_rest_error(): + client = FirewallClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + def test_credentials_transport_error(): # It is an error to provide credentials and a transport instance. transport = transports.FirewallGrpcTransport( @@ -1898,6 +2748,7 @@ def test_transport_get_channel(): [ transports.FirewallGrpcTransport, transports.FirewallGrpcAsyncIOTransport, + transports.FirewallRestTransport, ], ) def test_transport_adc(transport_class): @@ -1912,6 +2763,7 @@ def test_transport_adc(transport_class): "transport_name", [ "grpc", + "rest", ], ) def test_transport_kind(transport_name): @@ -2058,6 +2910,7 @@ def test_firewall_transport_auth_adc(transport_class): [ transports.FirewallGrpcTransport, transports.FirewallGrpcAsyncIOTransport, + transports.FirewallRestTransport, ], ) def test_firewall_transport_auth_gdch_credentials(transport_class): @@ -2156,11 +3009,23 @@ def test_firewall_grpc_transport_client_cert_source_for_mtls(transport_class): ) +def test_firewall_http_transport_client_cert_source_for_mtls(): + cred = ga_credentials.AnonymousCredentials() + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.configure_mtls_channel" + ) as mock_configure_mtls_channel: + transports.FirewallRestTransport( + credentials=cred, client_cert_source_for_mtls=client_cert_source_callback + ) + mock_configure_mtls_channel.assert_called_once_with(client_cert_source_callback) + + @pytest.mark.parametrize( "transport_name", [ "grpc", "grpc_asyncio", + "rest", ], ) def test_firewall_host_no_port(transport_name): @@ -2171,7 +3036,11 @@ def test_firewall_host_no_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -2179,6 +3048,7 @@ def test_firewall_host_no_port(transport_name): [ "grpc", "grpc_asyncio", + "rest", ], ) def test_firewall_host_with_port(transport_name): @@ -2189,7 +3059,48 @@ def test_firewall_host_with_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:8000") + assert client.transport._host == ( + "appengine.googleapis.com:8000" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com:8000" + ) + + +@pytest.mark.parametrize( + "transport_name", + [ + "rest", + ], +) +def test_firewall_client_transport_session_collision(transport_name): + creds1 = ga_credentials.AnonymousCredentials() + creds2 = ga_credentials.AnonymousCredentials() + client1 = FirewallClient( + credentials=creds1, + transport=transport_name, + ) + client2 = FirewallClient( + credentials=creds2, + transport=transport_name, + ) + session1 = client1.transport.list_ingress_rules._session + session2 = client2.transport.list_ingress_rules._session + assert session1 != session2 + session1 = client1.transport.batch_update_ingress_rules._session + session2 = client2.transport.batch_update_ingress_rules._session + assert session1 != session2 + session1 = client1.transport.create_ingress_rule._session + session2 = client2.transport.create_ingress_rule._session + assert session1 != session2 + session1 = client1.transport.get_ingress_rule._session + session2 = client2.transport.get_ingress_rule._session + assert session1 != session2 + session1 = client1.transport.update_ingress_rule._session + session2 = client2.transport.update_ingress_rule._session + assert session1 != session2 + session1 = client1.transport.delete_ingress_rule._session + session2 = client2.transport.delete_ingress_rule._session + assert session1 != session2 def test_firewall_grpc_transport_channel(): @@ -2452,6 +3363,7 @@ async def test_transport_close_async(): def test_transport_close(): transports = { + "rest": "_session", "grpc": "_grpc_channel", } @@ -2469,6 +3381,7 @@ def test_transport_close(): def test_client_ctx(): transports = [ + "rest", "grpc", ] for transport in transports: diff --git a/tests/unit/gapic/appengine_admin_v1/test_instances.py b/tests/unit/gapic/appengine_admin_v1/test_instances.py index 9c35b1e..0b36aa7 100644 --- a/tests/unit/gapic/appengine_admin_v1/test_instances.py +++ b/tests/unit/gapic/appengine_admin_v1/test_instances.py @@ -22,6 +22,8 @@ except ImportError: # pragma: NO COVER import mock +from collections.abc import Iterable +import json import math from google.api_core import ( @@ -42,12 +44,15 @@ from google.longrunning import operations_pb2 from google.oauth2 import service_account from google.protobuf import empty_pb2 # type: ignore +from google.protobuf import json_format from google.protobuf import timestamp_pb2 # type: ignore import grpc from grpc.experimental import aio from proto.marshal.rules import wrappers from proto.marshal.rules.dates import DurationRule, TimestampRule import pytest +from requests import PreparedRequest, Request, Response +from requests.sessions import Session from google.cloud.appengine_admin_v1.services.instances import ( InstancesAsyncClient, @@ -103,6 +108,7 @@ def test__get_default_mtls_endpoint(): [ (InstancesClient, "grpc"), (InstancesAsyncClient, "grpc_asyncio"), + (InstancesClient, "rest"), ], ) def test_instances_client_from_service_account_info(client_class, transport_name): @@ -116,7 +122,11 @@ def test_instances_client_from_service_account_info(client_class, transport_name assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -124,6 +134,7 @@ def test_instances_client_from_service_account_info(client_class, transport_name [ (transports.InstancesGrpcTransport, "grpc"), (transports.InstancesGrpcAsyncIOTransport, "grpc_asyncio"), + (transports.InstancesRestTransport, "rest"), ], ) def test_instances_client_service_account_always_use_jwt( @@ -149,6 +160,7 @@ def test_instances_client_service_account_always_use_jwt( [ (InstancesClient, "grpc"), (InstancesAsyncClient, "grpc_asyncio"), + (InstancesClient, "rest"), ], ) def test_instances_client_from_service_account_file(client_class, transport_name): @@ -169,13 +181,18 @@ def test_instances_client_from_service_account_file(client_class, transport_name assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) def test_instances_client_get_transport_class(): transport = InstancesClient.get_transport_class() available_transports = [ transports.InstancesGrpcTransport, + transports.InstancesRestTransport, ] assert transport in available_transports @@ -192,6 +209,7 @@ def test_instances_client_get_transport_class(): transports.InstancesGrpcAsyncIOTransport, "grpc_asyncio", ), + (InstancesClient, transports.InstancesRestTransport, "rest"), ], ) @mock.patch.object( @@ -333,6 +351,8 @@ def test_instances_client_client_options(client_class, transport_class, transpor "grpc_asyncio", "false", ), + (InstancesClient, transports.InstancesRestTransport, "rest", "true"), + (InstancesClient, transports.InstancesRestTransport, "rest", "false"), ], ) @mock.patch.object( @@ -526,6 +546,7 @@ def test_instances_client_get_mtls_endpoint_and_cert_source(client_class): transports.InstancesGrpcAsyncIOTransport, "grpc_asyncio", ), + (InstancesClient, transports.InstancesRestTransport, "rest"), ], ) def test_instances_client_client_options_scopes( @@ -561,6 +582,7 @@ def test_instances_client_client_options_scopes( "grpc_asyncio", grpc_helpers_async, ), + (InstancesClient, transports.InstancesRestTransport, "rest", None), ], ) def test_instances_client_client_options_credentials_file( @@ -1504,6 +1526,587 @@ async def test_debug_instance_field_headers_async(): ) in kw["metadata"] +@pytest.mark.parametrize( + "request_type", + [ + appengine.ListInstancesRequest, + dict, + ], +) +def test_list_instances_rest(request_type): + client = InstancesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1/services/sample2/versions/sample3"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = appengine.ListInstancesResponse( + next_page_token="next_page_token_value", + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = appengine.ListInstancesResponse.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.list_instances(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, pagers.ListInstancesPager) + assert response.next_page_token == "next_page_token_value" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_list_instances_rest_interceptors(null_interceptor): + transport = transports.InstancesRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.InstancesRestInterceptor(), + ) + client = InstancesClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.InstancesRestInterceptor, "post_list_instances" + ) as post, mock.patch.object( + transports.InstancesRestInterceptor, "pre_list_instances" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.ListInstancesRequest.pb(appengine.ListInstancesRequest()) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = appengine.ListInstancesResponse.to_json( + appengine.ListInstancesResponse() + ) + + request = appengine.ListInstancesRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = appengine.ListInstancesResponse() + + client.list_instances( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_list_instances_rest_bad_request( + transport: str = "rest", request_type=appengine.ListInstancesRequest +): + client = InstancesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1/services/sample2/versions/sample3"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.list_instances(request) + + +def test_list_instances_rest_pager(transport: str = "rest"): + client = InstancesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(Session, "request") as req: + # TODO(kbandes): remove this mock unless there's a good reason for it. + # with mock.patch.object(path_template, 'transcode') as transcode: + # Set the response as a series of pages + response = ( + appengine.ListInstancesResponse( + instances=[ + instance.Instance(), + instance.Instance(), + instance.Instance(), + ], + next_page_token="abc", + ), + appengine.ListInstancesResponse( + instances=[], + next_page_token="def", + ), + appengine.ListInstancesResponse( + instances=[ + instance.Instance(), + ], + next_page_token="ghi", + ), + appengine.ListInstancesResponse( + instances=[ + instance.Instance(), + instance.Instance(), + ], + ), + ) + # Two responses for two calls + response = response + response + + # Wrap the values into proper Response objs + response = tuple(appengine.ListInstancesResponse.to_json(x) for x in response) + return_values = tuple(Response() for i in response) + for return_val, response_val in zip(return_values, response): + return_val._content = response_val.encode("UTF-8") + return_val.status_code = 200 + req.side_effect = return_values + + sample_request = {"parent": "apps/sample1/services/sample2/versions/sample3"} + + pager = client.list_instances(request=sample_request) + + results = list(pager) + assert len(results) == 6 + assert all(isinstance(i, instance.Instance) for i in results) + + pages = list(client.list_instances(request=sample_request).pages) + for page_, token in zip(pages, ["abc", "def", "ghi", ""]): + assert page_.raw_page.next_page_token == token + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.GetInstanceRequest, + dict, + ], +) +def test_get_instance_rest(request_type): + client = InstancesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = { + "name": "apps/sample1/services/sample2/versions/sample3/instances/sample4" + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = instance.Instance( + name="name_value", + id="id_value", + app_engine_release="app_engine_release_value", + availability=instance.Instance.Availability.RESIDENT, + vm_name="vm_name_value", + vm_zone_name="vm_zone_name_value", + vm_id="vm_id_value", + requests=892, + errors=669, + qps=0.34, + average_latency=1578, + memory_usage=1293, + vm_status="vm_status_value", + vm_debug_enabled=True, + vm_ip="vm_ip_value", + vm_liveness=instance.Instance.Liveness.LivenessState.UNKNOWN, + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = instance.Instance.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.get_instance(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, instance.Instance) + assert response.name == "name_value" + assert response.id == "id_value" + assert response.app_engine_release == "app_engine_release_value" + assert response.availability == instance.Instance.Availability.RESIDENT + assert response.vm_name == "vm_name_value" + assert response.vm_zone_name == "vm_zone_name_value" + assert response.vm_id == "vm_id_value" + assert response.requests == 892 + assert response.errors == 669 + assert math.isclose(response.qps, 0.34, rel_tol=1e-6) + assert response.average_latency == 1578 + assert response.memory_usage == 1293 + assert response.vm_status == "vm_status_value" + assert response.vm_debug_enabled is True + assert response.vm_ip == "vm_ip_value" + assert response.vm_liveness == instance.Instance.Liveness.LivenessState.UNKNOWN + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_get_instance_rest_interceptors(null_interceptor): + transport = transports.InstancesRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.InstancesRestInterceptor(), + ) + client = InstancesClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.InstancesRestInterceptor, "post_get_instance" + ) as post, mock.patch.object( + transports.InstancesRestInterceptor, "pre_get_instance" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.GetInstanceRequest.pb(appengine.GetInstanceRequest()) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = instance.Instance.to_json(instance.Instance()) + + request = appengine.GetInstanceRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = instance.Instance() + + client.get_instance( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_get_instance_rest_bad_request( + transport: str = "rest", request_type=appengine.GetInstanceRequest +): + client = InstancesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = { + "name": "apps/sample1/services/sample2/versions/sample3/instances/sample4" + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.get_instance(request) + + +def test_get_instance_rest_error(): + client = InstancesClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.DeleteInstanceRequest, + dict, + ], +) +def test_delete_instance_rest(request_type): + client = InstancesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = { + "name": "apps/sample1/services/sample2/versions/sample3/instances/sample4" + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = operations_pb2.Operation(name="operations/spam") + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + json_return_value = json_format.MessageToJson(return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.delete_instance(request) + + # Establish that the response is the type that we expect. + assert response.operation.name == "operations/spam" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_delete_instance_rest_interceptors(null_interceptor): + transport = transports.InstancesRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.InstancesRestInterceptor(), + ) + client = InstancesClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + operation.Operation, "_set_result_from_operation" + ), mock.patch.object( + transports.InstancesRestInterceptor, "post_delete_instance" + ) as post, mock.patch.object( + transports.InstancesRestInterceptor, "pre_delete_instance" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.DeleteInstanceRequest.pb( + appengine.DeleteInstanceRequest() + ) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = json_format.MessageToJson( + operations_pb2.Operation() + ) + + request = appengine.DeleteInstanceRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = operations_pb2.Operation() + + client.delete_instance( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_delete_instance_rest_bad_request( + transport: str = "rest", request_type=appengine.DeleteInstanceRequest +): + client = InstancesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = { + "name": "apps/sample1/services/sample2/versions/sample3/instances/sample4" + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.delete_instance(request) + + +def test_delete_instance_rest_error(): + client = InstancesClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.DebugInstanceRequest, + dict, + ], +) +def test_debug_instance_rest(request_type): + client = InstancesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = { + "name": "apps/sample1/services/sample2/versions/sample3/instances/sample4" + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = operations_pb2.Operation(name="operations/spam") + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + json_return_value = json_format.MessageToJson(return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.debug_instance(request) + + # Establish that the response is the type that we expect. + assert response.operation.name == "operations/spam" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_debug_instance_rest_interceptors(null_interceptor): + transport = transports.InstancesRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.InstancesRestInterceptor(), + ) + client = InstancesClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + operation.Operation, "_set_result_from_operation" + ), mock.patch.object( + transports.InstancesRestInterceptor, "post_debug_instance" + ) as post, mock.patch.object( + transports.InstancesRestInterceptor, "pre_debug_instance" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.DebugInstanceRequest.pb(appengine.DebugInstanceRequest()) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = json_format.MessageToJson( + operations_pb2.Operation() + ) + + request = appengine.DebugInstanceRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = operations_pb2.Operation() + + client.debug_instance( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_debug_instance_rest_bad_request( + transport: str = "rest", request_type=appengine.DebugInstanceRequest +): + client = InstancesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = { + "name": "apps/sample1/services/sample2/versions/sample3/instances/sample4" + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.debug_instance(request) + + +def test_debug_instance_rest_error(): + client = InstancesClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + def test_credentials_transport_error(): # It is an error to provide credentials and a transport instance. transport = transports.InstancesGrpcTransport( @@ -1585,6 +2188,7 @@ def test_transport_get_channel(): [ transports.InstancesGrpcTransport, transports.InstancesGrpcAsyncIOTransport, + transports.InstancesRestTransport, ], ) def test_transport_adc(transport_class): @@ -1599,6 +2203,7 @@ def test_transport_adc(transport_class): "transport_name", [ "grpc", + "rest", ], ) def test_transport_kind(transport_name): @@ -1748,6 +2353,7 @@ def test_instances_transport_auth_adc(transport_class): [ transports.InstancesGrpcTransport, transports.InstancesGrpcAsyncIOTransport, + transports.InstancesRestTransport, ], ) def test_instances_transport_auth_gdch_credentials(transport_class): @@ -1846,11 +2452,40 @@ def test_instances_grpc_transport_client_cert_source_for_mtls(transport_class): ) +def test_instances_http_transport_client_cert_source_for_mtls(): + cred = ga_credentials.AnonymousCredentials() + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.configure_mtls_channel" + ) as mock_configure_mtls_channel: + transports.InstancesRestTransport( + credentials=cred, client_cert_source_for_mtls=client_cert_source_callback + ) + mock_configure_mtls_channel.assert_called_once_with(client_cert_source_callback) + + +def test_instances_rest_lro_client(): + client = InstancesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + transport = client.transport + + # Ensure that we have a api-core operations client. + assert isinstance( + transport.operations_client, + operations_v1.AbstractOperationsClient, + ) + + # Ensure that subsequent calls to the property send the exact same object. + assert transport.operations_client is transport.operations_client + + @pytest.mark.parametrize( "transport_name", [ "grpc", "grpc_asyncio", + "rest", ], ) def test_instances_host_no_port(transport_name): @@ -1861,7 +2496,11 @@ def test_instances_host_no_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -1869,6 +2508,7 @@ def test_instances_host_no_port(transport_name): [ "grpc", "grpc_asyncio", + "rest", ], ) def test_instances_host_with_port(transport_name): @@ -1879,7 +2519,42 @@ def test_instances_host_with_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:8000") + assert client.transport._host == ( + "appengine.googleapis.com:8000" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com:8000" + ) + + +@pytest.mark.parametrize( + "transport_name", + [ + "rest", + ], +) +def test_instances_client_transport_session_collision(transport_name): + creds1 = ga_credentials.AnonymousCredentials() + creds2 = ga_credentials.AnonymousCredentials() + client1 = InstancesClient( + credentials=creds1, + transport=transport_name, + ) + client2 = InstancesClient( + credentials=creds2, + transport=transport_name, + ) + session1 = client1.transport.list_instances._session + session2 = client2.transport.list_instances._session + assert session1 != session2 + session1 = client1.transport.get_instance._session + session2 = client2.transport.get_instance._session + assert session1 != session2 + session1 = client1.transport.delete_instance._session + session2 = client2.transport.delete_instance._session + assert session1 != session2 + session1 = client1.transport.debug_instance._session + session2 = client2.transport.debug_instance._session + assert session1 != session2 def test_instances_grpc_transport_channel(): @@ -2207,6 +2882,7 @@ async def test_transport_close_async(): def test_transport_close(): transports = { + "rest": "_session", "grpc": "_grpc_channel", } @@ -2224,6 +2900,7 @@ def test_transport_close(): def test_client_ctx(): transports = [ + "rest", "grpc", ] for transport in transports: diff --git a/tests/unit/gapic/appengine_admin_v1/test_services.py b/tests/unit/gapic/appengine_admin_v1/test_services.py index fea1894..ca6388e 100644 --- a/tests/unit/gapic/appengine_admin_v1/test_services.py +++ b/tests/unit/gapic/appengine_admin_v1/test_services.py @@ -22,6 +22,8 @@ except ImportError: # pragma: NO COVER import mock +from collections.abc import Iterable +import json import math from google.api_core import ( @@ -43,11 +45,14 @@ from google.oauth2 import service_account from google.protobuf import empty_pb2 # type: ignore from google.protobuf import field_mask_pb2 # type: ignore +from google.protobuf import json_format import grpc from grpc.experimental import aio from proto.marshal.rules import wrappers from proto.marshal.rules.dates import DurationRule, TimestampRule import pytest +from requests import PreparedRequest, Request, Response +from requests.sessions import Session from google.cloud.appengine_admin_v1.services.services import ( ServicesAsyncClient, @@ -104,6 +109,7 @@ def test__get_default_mtls_endpoint(): [ (ServicesClient, "grpc"), (ServicesAsyncClient, "grpc_asyncio"), + (ServicesClient, "rest"), ], ) def test_services_client_from_service_account_info(client_class, transport_name): @@ -117,7 +123,11 @@ def test_services_client_from_service_account_info(client_class, transport_name) assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -125,6 +135,7 @@ def test_services_client_from_service_account_info(client_class, transport_name) [ (transports.ServicesGrpcTransport, "grpc"), (transports.ServicesGrpcAsyncIOTransport, "grpc_asyncio"), + (transports.ServicesRestTransport, "rest"), ], ) def test_services_client_service_account_always_use_jwt( @@ -150,6 +161,7 @@ def test_services_client_service_account_always_use_jwt( [ (ServicesClient, "grpc"), (ServicesAsyncClient, "grpc_asyncio"), + (ServicesClient, "rest"), ], ) def test_services_client_from_service_account_file(client_class, transport_name): @@ -170,13 +182,18 @@ def test_services_client_from_service_account_file(client_class, transport_name) assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) def test_services_client_get_transport_class(): transport = ServicesClient.get_transport_class() available_transports = [ transports.ServicesGrpcTransport, + transports.ServicesRestTransport, ] assert transport in available_transports @@ -189,6 +206,7 @@ def test_services_client_get_transport_class(): [ (ServicesClient, transports.ServicesGrpcTransport, "grpc"), (ServicesAsyncClient, transports.ServicesGrpcAsyncIOTransport, "grpc_asyncio"), + (ServicesClient, transports.ServicesRestTransport, "rest"), ], ) @mock.patch.object( @@ -330,6 +348,8 @@ def test_services_client_client_options(client_class, transport_class, transport "grpc_asyncio", "false", ), + (ServicesClient, transports.ServicesRestTransport, "rest", "true"), + (ServicesClient, transports.ServicesRestTransport, "rest", "false"), ], ) @mock.patch.object( @@ -519,6 +539,7 @@ def test_services_client_get_mtls_endpoint_and_cert_source(client_class): [ (ServicesClient, transports.ServicesGrpcTransport, "grpc"), (ServicesAsyncClient, transports.ServicesGrpcAsyncIOTransport, "grpc_asyncio"), + (ServicesClient, transports.ServicesRestTransport, "rest"), ], ) def test_services_client_client_options_scopes( @@ -554,6 +575,7 @@ def test_services_client_client_options_scopes( "grpc_asyncio", grpc_helpers_async, ), + (ServicesClient, transports.ServicesRestTransport, "rest", None), ], ) def test_services_client_client_options_credentials_file( @@ -1441,6 +1463,559 @@ async def test_delete_service_field_headers_async(): ) in kw["metadata"] +@pytest.mark.parametrize( + "request_type", + [ + appengine.ListServicesRequest, + dict, + ], +) +def test_list_services_rest(request_type): + client = ServicesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = appengine.ListServicesResponse( + next_page_token="next_page_token_value", + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = appengine.ListServicesResponse.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.list_services(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, pagers.ListServicesPager) + assert response.next_page_token == "next_page_token_value" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_list_services_rest_interceptors(null_interceptor): + transport = transports.ServicesRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.ServicesRestInterceptor(), + ) + client = ServicesClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.ServicesRestInterceptor, "post_list_services" + ) as post, mock.patch.object( + transports.ServicesRestInterceptor, "pre_list_services" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.ListServicesRequest.pb(appengine.ListServicesRequest()) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = appengine.ListServicesResponse.to_json( + appengine.ListServicesResponse() + ) + + request = appengine.ListServicesRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = appengine.ListServicesResponse() + + client.list_services( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_list_services_rest_bad_request( + transport: str = "rest", request_type=appengine.ListServicesRequest +): + client = ServicesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.list_services(request) + + +def test_list_services_rest_pager(transport: str = "rest"): + client = ServicesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(Session, "request") as req: + # TODO(kbandes): remove this mock unless there's a good reason for it. + # with mock.patch.object(path_template, 'transcode') as transcode: + # Set the response as a series of pages + response = ( + appengine.ListServicesResponse( + services=[ + service.Service(), + service.Service(), + service.Service(), + ], + next_page_token="abc", + ), + appengine.ListServicesResponse( + services=[], + next_page_token="def", + ), + appengine.ListServicesResponse( + services=[ + service.Service(), + ], + next_page_token="ghi", + ), + appengine.ListServicesResponse( + services=[ + service.Service(), + service.Service(), + ], + ), + ) + # Two responses for two calls + response = response + response + + # Wrap the values into proper Response objs + response = tuple(appengine.ListServicesResponse.to_json(x) for x in response) + return_values = tuple(Response() for i in response) + for return_val, response_val in zip(return_values, response): + return_val._content = response_val.encode("UTF-8") + return_val.status_code = 200 + req.side_effect = return_values + + sample_request = {"parent": "apps/sample1"} + + pager = client.list_services(request=sample_request) + + results = list(pager) + assert len(results) == 6 + assert all(isinstance(i, service.Service) for i in results) + + pages = list(client.list_services(request=sample_request).pages) + for page_, token in zip(pages, ["abc", "def", "ghi", ""]): + assert page_.raw_page.next_page_token == token + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.GetServiceRequest, + dict, + ], +) +def test_get_service_rest(request_type): + client = ServicesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/services/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = service.Service( + name="name_value", + id="id_value", + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = service.Service.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.get_service(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, service.Service) + assert response.name == "name_value" + assert response.id == "id_value" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_get_service_rest_interceptors(null_interceptor): + transport = transports.ServicesRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.ServicesRestInterceptor(), + ) + client = ServicesClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.ServicesRestInterceptor, "post_get_service" + ) as post, mock.patch.object( + transports.ServicesRestInterceptor, "pre_get_service" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.GetServiceRequest.pb(appengine.GetServiceRequest()) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = service.Service.to_json(service.Service()) + + request = appengine.GetServiceRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = service.Service() + + client.get_service( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_get_service_rest_bad_request( + transport: str = "rest", request_type=appengine.GetServiceRequest +): + client = ServicesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/services/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.get_service(request) + + +def test_get_service_rest_error(): + client = ServicesClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.UpdateServiceRequest, + dict, + ], +) +def test_update_service_rest(request_type): + client = ServicesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/services/sample2"} + request_init["service"] = { + "name": "name_value", + "id": "id_value", + "split": {"shard_by": 1, "allocations": {}}, + "labels": {}, + "network_settings": {"ingress_traffic_allowed": 1}, + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = operations_pb2.Operation(name="operations/spam") + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + json_return_value = json_format.MessageToJson(return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.update_service(request) + + # Establish that the response is the type that we expect. + assert response.operation.name == "operations/spam" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_update_service_rest_interceptors(null_interceptor): + transport = transports.ServicesRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.ServicesRestInterceptor(), + ) + client = ServicesClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + operation.Operation, "_set_result_from_operation" + ), mock.patch.object( + transports.ServicesRestInterceptor, "post_update_service" + ) as post, mock.patch.object( + transports.ServicesRestInterceptor, "pre_update_service" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.UpdateServiceRequest.pb(appengine.UpdateServiceRequest()) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = json_format.MessageToJson( + operations_pb2.Operation() + ) + + request = appengine.UpdateServiceRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = operations_pb2.Operation() + + client.update_service( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_update_service_rest_bad_request( + transport: str = "rest", request_type=appengine.UpdateServiceRequest +): + client = ServicesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/services/sample2"} + request_init["service"] = { + "name": "name_value", + "id": "id_value", + "split": {"shard_by": 1, "allocations": {}}, + "labels": {}, + "network_settings": {"ingress_traffic_allowed": 1}, + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.update_service(request) + + +def test_update_service_rest_error(): + client = ServicesClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.DeleteServiceRequest, + dict, + ], +) +def test_delete_service_rest(request_type): + client = ServicesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/services/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = operations_pb2.Operation(name="operations/spam") + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + json_return_value = json_format.MessageToJson(return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.delete_service(request) + + # Establish that the response is the type that we expect. + assert response.operation.name == "operations/spam" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_delete_service_rest_interceptors(null_interceptor): + transport = transports.ServicesRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.ServicesRestInterceptor(), + ) + client = ServicesClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + operation.Operation, "_set_result_from_operation" + ), mock.patch.object( + transports.ServicesRestInterceptor, "post_delete_service" + ) as post, mock.patch.object( + transports.ServicesRestInterceptor, "pre_delete_service" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.DeleteServiceRequest.pb(appengine.DeleteServiceRequest()) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = json_format.MessageToJson( + operations_pb2.Operation() + ) + + request = appengine.DeleteServiceRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = operations_pb2.Operation() + + client.delete_service( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_delete_service_rest_bad_request( + transport: str = "rest", request_type=appengine.DeleteServiceRequest +): + client = ServicesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/services/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.delete_service(request) + + +def test_delete_service_rest_error(): + client = ServicesClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + def test_credentials_transport_error(): # It is an error to provide credentials and a transport instance. transport = transports.ServicesGrpcTransport( @@ -1522,6 +2097,7 @@ def test_transport_get_channel(): [ transports.ServicesGrpcTransport, transports.ServicesGrpcAsyncIOTransport, + transports.ServicesRestTransport, ], ) def test_transport_adc(transport_class): @@ -1536,6 +2112,7 @@ def test_transport_adc(transport_class): "transport_name", [ "grpc", + "rest", ], ) def test_transport_kind(transport_name): @@ -1685,6 +2262,7 @@ def test_services_transport_auth_adc(transport_class): [ transports.ServicesGrpcTransport, transports.ServicesGrpcAsyncIOTransport, + transports.ServicesRestTransport, ], ) def test_services_transport_auth_gdch_credentials(transport_class): @@ -1783,11 +2361,40 @@ def test_services_grpc_transport_client_cert_source_for_mtls(transport_class): ) +def test_services_http_transport_client_cert_source_for_mtls(): + cred = ga_credentials.AnonymousCredentials() + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.configure_mtls_channel" + ) as mock_configure_mtls_channel: + transports.ServicesRestTransport( + credentials=cred, client_cert_source_for_mtls=client_cert_source_callback + ) + mock_configure_mtls_channel.assert_called_once_with(client_cert_source_callback) + + +def test_services_rest_lro_client(): + client = ServicesClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + transport = client.transport + + # Ensure that we have a api-core operations client. + assert isinstance( + transport.operations_client, + operations_v1.AbstractOperationsClient, + ) + + # Ensure that subsequent calls to the property send the exact same object. + assert transport.operations_client is transport.operations_client + + @pytest.mark.parametrize( "transport_name", [ "grpc", "grpc_asyncio", + "rest", ], ) def test_services_host_no_port(transport_name): @@ -1798,7 +2405,11 @@ def test_services_host_no_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -1806,6 +2417,7 @@ def test_services_host_no_port(transport_name): [ "grpc", "grpc_asyncio", + "rest", ], ) def test_services_host_with_port(transport_name): @@ -1816,7 +2428,42 @@ def test_services_host_with_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:8000") + assert client.transport._host == ( + "appengine.googleapis.com:8000" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com:8000" + ) + + +@pytest.mark.parametrize( + "transport_name", + [ + "rest", + ], +) +def test_services_client_transport_session_collision(transport_name): + creds1 = ga_credentials.AnonymousCredentials() + creds2 = ga_credentials.AnonymousCredentials() + client1 = ServicesClient( + credentials=creds1, + transport=transport_name, + ) + client2 = ServicesClient( + credentials=creds2, + transport=transport_name, + ) + session1 = client1.transport.list_services._session + session2 = client2.transport.list_services._session + assert session1 != session2 + session1 = client1.transport.get_service._session + session2 = client2.transport.get_service._session + assert session1 != session2 + session1 = client1.transport.update_service._session + session2 = client2.transport.update_service._session + assert session1 != session2 + session1 = client1.transport.delete_service._session + session2 = client2.transport.delete_service._session + assert session1 != session2 def test_services_grpc_transport_channel(): @@ -2113,6 +2760,7 @@ async def test_transport_close_async(): def test_transport_close(): transports = { + "rest": "_session", "grpc": "_grpc_channel", } @@ -2130,6 +2778,7 @@ def test_transport_close(): def test_client_ctx(): transports = [ + "rest", "grpc", ] for transport in transports: diff --git a/tests/unit/gapic/appengine_admin_v1/test_versions.py b/tests/unit/gapic/appengine_admin_v1/test_versions.py index 5cd663b..8ea3eb5 100644 --- a/tests/unit/gapic/appengine_admin_v1/test_versions.py +++ b/tests/unit/gapic/appengine_admin_v1/test_versions.py @@ -22,6 +22,8 @@ except ImportError: # pragma: NO COVER import mock +from collections.abc import Iterable +import json import math from google.api_core import ( @@ -44,12 +46,15 @@ from google.protobuf import duration_pb2 # type: ignore from google.protobuf import empty_pb2 # type: ignore from google.protobuf import field_mask_pb2 # type: ignore +from google.protobuf import json_format from google.protobuf import timestamp_pb2 # type: ignore import grpc from grpc.experimental import aio from proto.marshal.rules import wrappers from proto.marshal.rules.dates import DurationRule, TimestampRule import pytest +from requests import PreparedRequest, Request, Response +from requests.sessions import Session from google.cloud.appengine_admin_v1.services.versions import ( VersionsAsyncClient, @@ -106,6 +111,7 @@ def test__get_default_mtls_endpoint(): [ (VersionsClient, "grpc"), (VersionsAsyncClient, "grpc_asyncio"), + (VersionsClient, "rest"), ], ) def test_versions_client_from_service_account_info(client_class, transport_name): @@ -119,7 +125,11 @@ def test_versions_client_from_service_account_info(client_class, transport_name) assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -127,6 +137,7 @@ def test_versions_client_from_service_account_info(client_class, transport_name) [ (transports.VersionsGrpcTransport, "grpc"), (transports.VersionsGrpcAsyncIOTransport, "grpc_asyncio"), + (transports.VersionsRestTransport, "rest"), ], ) def test_versions_client_service_account_always_use_jwt( @@ -152,6 +163,7 @@ def test_versions_client_service_account_always_use_jwt( [ (VersionsClient, "grpc"), (VersionsAsyncClient, "grpc_asyncio"), + (VersionsClient, "rest"), ], ) def test_versions_client_from_service_account_file(client_class, transport_name): @@ -172,13 +184,18 @@ def test_versions_client_from_service_account_file(client_class, transport_name) assert client.transport._credentials == creds assert isinstance(client, client_class) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) def test_versions_client_get_transport_class(): transport = VersionsClient.get_transport_class() available_transports = [ transports.VersionsGrpcTransport, + transports.VersionsRestTransport, ] assert transport in available_transports @@ -191,6 +208,7 @@ def test_versions_client_get_transport_class(): [ (VersionsClient, transports.VersionsGrpcTransport, "grpc"), (VersionsAsyncClient, transports.VersionsGrpcAsyncIOTransport, "grpc_asyncio"), + (VersionsClient, transports.VersionsRestTransport, "rest"), ], ) @mock.patch.object( @@ -332,6 +350,8 @@ def test_versions_client_client_options(client_class, transport_class, transport "grpc_asyncio", "false", ), + (VersionsClient, transports.VersionsRestTransport, "rest", "true"), + (VersionsClient, transports.VersionsRestTransport, "rest", "false"), ], ) @mock.patch.object( @@ -521,6 +541,7 @@ def test_versions_client_get_mtls_endpoint_and_cert_source(client_class): [ (VersionsClient, transports.VersionsGrpcTransport, "grpc"), (VersionsAsyncClient, transports.VersionsGrpcAsyncIOTransport, "grpc_asyncio"), + (VersionsClient, transports.VersionsRestTransport, "rest"), ], ) def test_versions_client_client_options_scopes( @@ -556,6 +577,7 @@ def test_versions_client_client_options_scopes( "grpc_asyncio", grpc_helpers_async, ), + (VersionsClient, transports.VersionsRestTransport, "rest", None), ], ) def test_versions_client_client_options_credentials_file( @@ -1662,6 +1684,1356 @@ async def test_delete_version_field_headers_async(): ) in kw["metadata"] +@pytest.mark.parametrize( + "request_type", + [ + appengine.ListVersionsRequest, + dict, + ], +) +def test_list_versions_rest(request_type): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1/services/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = appengine.ListVersionsResponse( + next_page_token="next_page_token_value", + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = appengine.ListVersionsResponse.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.list_versions(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, pagers.ListVersionsPager) + assert response.next_page_token == "next_page_token_value" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_list_versions_rest_interceptors(null_interceptor): + transport = transports.VersionsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.VersionsRestInterceptor(), + ) + client = VersionsClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.VersionsRestInterceptor, "post_list_versions" + ) as post, mock.patch.object( + transports.VersionsRestInterceptor, "pre_list_versions" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.ListVersionsRequest.pb(appengine.ListVersionsRequest()) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = appengine.ListVersionsResponse.to_json( + appengine.ListVersionsResponse() + ) + + request = appengine.ListVersionsRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = appengine.ListVersionsResponse() + + client.list_versions( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_list_versions_rest_bad_request( + transport: str = "rest", request_type=appengine.ListVersionsRequest +): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1/services/sample2"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.list_versions(request) + + +def test_list_versions_rest_pager(transport: str = "rest"): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(Session, "request") as req: + # TODO(kbandes): remove this mock unless there's a good reason for it. + # with mock.patch.object(path_template, 'transcode') as transcode: + # Set the response as a series of pages + response = ( + appengine.ListVersionsResponse( + versions=[ + version.Version(), + version.Version(), + version.Version(), + ], + next_page_token="abc", + ), + appengine.ListVersionsResponse( + versions=[], + next_page_token="def", + ), + appengine.ListVersionsResponse( + versions=[ + version.Version(), + ], + next_page_token="ghi", + ), + appengine.ListVersionsResponse( + versions=[ + version.Version(), + version.Version(), + ], + ), + ) + # Two responses for two calls + response = response + response + + # Wrap the values into proper Response objs + response = tuple(appengine.ListVersionsResponse.to_json(x) for x in response) + return_values = tuple(Response() for i in response) + for return_val, response_val in zip(return_values, response): + return_val._content = response_val.encode("UTF-8") + return_val.status_code = 200 + req.side_effect = return_values + + sample_request = {"parent": "apps/sample1/services/sample2"} + + pager = client.list_versions(request=sample_request) + + results = list(pager) + assert len(results) == 6 + assert all(isinstance(i, version.Version) for i in results) + + pages = list(client.list_versions(request=sample_request).pages) + for page_, token in zip(pages, ["abc", "def", "ghi", ""]): + assert page_.raw_page.next_page_token == token + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.GetVersionRequest, + dict, + ], +) +def test_get_version_rest(request_type): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/services/sample2/versions/sample3"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = version.Version( + name="name_value", + id="id_value", + inbound_services=[version.InboundServiceType.INBOUND_SERVICE_MAIL], + instance_class="instance_class_value", + zones=["zones_value"], + runtime="runtime_value", + runtime_channel="runtime_channel_value", + threadsafe=True, + vm=True, + app_engine_apis=True, + env="env_value", + serving_status=version.ServingStatus.SERVING, + created_by="created_by_value", + disk_usage_bytes=1701, + runtime_api_version="runtime_api_version_value", + runtime_main_executable_path="runtime_main_executable_path_value", + service_account="service_account_value", + nobuild_files_regex="nobuild_files_regex_value", + version_url="version_url_value", + automatic_scaling=version.AutomaticScaling( + cool_down_period=duration_pb2.Duration(seconds=751) + ), + ) + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + pb_return_value = version.Version.pb(return_value) + json_return_value = json_format.MessageToJson(pb_return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.get_version(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, version.Version) + assert response.name == "name_value" + assert response.id == "id_value" + assert response.inbound_services == [ + version.InboundServiceType.INBOUND_SERVICE_MAIL + ] + assert response.instance_class == "instance_class_value" + assert response.zones == ["zones_value"] + assert response.runtime == "runtime_value" + assert response.runtime_channel == "runtime_channel_value" + assert response.threadsafe is True + assert response.vm is True + assert response.app_engine_apis is True + assert response.env == "env_value" + assert response.serving_status == version.ServingStatus.SERVING + assert response.created_by == "created_by_value" + assert response.disk_usage_bytes == 1701 + assert response.runtime_api_version == "runtime_api_version_value" + assert response.runtime_main_executable_path == "runtime_main_executable_path_value" + assert response.service_account == "service_account_value" + assert response.nobuild_files_regex == "nobuild_files_regex_value" + assert response.version_url == "version_url_value" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_get_version_rest_interceptors(null_interceptor): + transport = transports.VersionsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.VersionsRestInterceptor(), + ) + client = VersionsClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + transports.VersionsRestInterceptor, "post_get_version" + ) as post, mock.patch.object( + transports.VersionsRestInterceptor, "pre_get_version" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.GetVersionRequest.pb(appengine.GetVersionRequest()) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = version.Version.to_json(version.Version()) + + request = appengine.GetVersionRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = version.Version() + + client.get_version( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_get_version_rest_bad_request( + transport: str = "rest", request_type=appengine.GetVersionRequest +): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/services/sample2/versions/sample3"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.get_version(request) + + +def test_get_version_rest_error(): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.CreateVersionRequest, + dict, + ], +) +def test_create_version_rest(request_type): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1/services/sample2"} + request_init["version"] = { + "name": "name_value", + "id": "id_value", + "automatic_scaling": { + "cool_down_period": {"seconds": 751, "nanos": 543}, + "cpu_utilization": { + "aggregation_window_length": {}, + "target_utilization": 0.19540000000000002, + }, + "max_concurrent_requests": 2499, + "max_idle_instances": 1898, + "max_total_instances": 2032, + "max_pending_latency": {}, + "min_idle_instances": 1896, + "min_total_instances": 2030, + "min_pending_latency": {}, + "request_utilization": { + "target_request_count_per_second": 3320, + "target_concurrent_requests": 2820, + }, + "disk_utilization": { + "target_write_bytes_per_second": 3096, + "target_write_ops_per_second": 2883, + "target_read_bytes_per_second": 2953, + "target_read_ops_per_second": 2740, + }, + "network_utilization": { + "target_sent_bytes_per_second": 2983, + "target_sent_packets_per_second": 3179, + "target_received_bytes_per_second": 3380, + "target_received_packets_per_second": 3576, + }, + "standard_scheduler_settings": { + "target_cpu_utilization": 0.23770000000000002, + "target_throughput_utilization": 0.3163, + "min_instances": 1387, + "max_instances": 1389, + }, + }, + "basic_scaling": {"idle_timeout": {}, "max_instances": 1389}, + "manual_scaling": {"instances": 968}, + "inbound_services": [1], + "instance_class": "instance_class_value", + "network": { + "forwarded_ports": ["forwarded_ports_value1", "forwarded_ports_value2"], + "instance_tag": "instance_tag_value", + "name": "name_value", + "subnetwork_name": "subnetwork_name_value", + "session_affinity": True, + }, + "zones": ["zones_value1", "zones_value2"], + "resources": { + "cpu": 0.328, + "disk_gb": 0.723, + "memory_gb": 0.961, + "volumes": [ + { + "name": "name_value", + "volume_type": "volume_type_value", + "size_gb": 0.739, + } + ], + "kms_key_reference": "kms_key_reference_value", + }, + "runtime": "runtime_value", + "runtime_channel": "runtime_channel_value", + "threadsafe": True, + "vm": True, + "app_engine_apis": True, + "beta_settings": {}, + "env": "env_value", + "serving_status": 1, + "created_by": "created_by_value", + "create_time": {"seconds": 751, "nanos": 543}, + "disk_usage_bytes": 1701, + "runtime_api_version": "runtime_api_version_value", + "runtime_main_executable_path": "runtime_main_executable_path_value", + "service_account": "service_account_value", + "handlers": [ + { + "url_regex": "url_regex_value", + "static_files": { + "path": "path_value", + "upload_path_regex": "upload_path_regex_value", + "http_headers": {}, + "mime_type": "mime_type_value", + "expiration": {}, + "require_matching_file": True, + "application_readable": True, + }, + "script": {"script_path": "script_path_value"}, + "api_endpoint": {"script_path": "script_path_value"}, + "security_level": 1, + "login": 1, + "auth_fail_action": 1, + "redirect_http_response_code": 1, + } + ], + "error_handlers": [ + { + "error_code": 1, + "static_file": "static_file_value", + "mime_type": "mime_type_value", + } + ], + "libraries": [{"name": "name_value", "version": "version_value"}], + "api_config": { + "auth_fail_action": 1, + "login": 1, + "script": "script_value", + "security_level": 1, + "url": "url_value", + }, + "env_variables": {}, + "build_env_variables": {}, + "default_expiration": {}, + "health_check": { + "disable_health_check": True, + "host": "host_value", + "healthy_threshold": 1819, + "unhealthy_threshold": 2046, + "restart_threshold": 1841, + "check_interval": {}, + "timeout": {}, + }, + "readiness_check": { + "path": "path_value", + "host": "host_value", + "failure_threshold": 1812, + "success_threshold": 1829, + "check_interval": {}, + "timeout": {}, + "app_start_timeout": {}, + }, + "liveness_check": { + "path": "path_value", + "host": "host_value", + "failure_threshold": 1812, + "success_threshold": 1829, + "check_interval": {}, + "timeout": {}, + "initial_delay": {}, + }, + "nobuild_files_regex": "nobuild_files_regex_value", + "deployment": { + "files": {}, + "container": {"image": "image_value"}, + "zip_": {"source_url": "source_url_value", "files_count": 1179}, + "cloud_build_options": { + "app_yaml_path": "app_yaml_path_value", + "cloud_build_timeout": {}, + }, + }, + "version_url": "version_url_value", + "endpoints_api_service": { + "name": "name_value", + "config_id": "config_id_value", + "rollout_strategy": 1, + "disable_trace_sampling": True, + }, + "entrypoint": {"shell": "shell_value"}, + "vpc_access_connector": {"name": "name_value", "egress_setting": 1}, + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = operations_pb2.Operation(name="operations/spam") + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + json_return_value = json_format.MessageToJson(return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.create_version(request) + + # Establish that the response is the type that we expect. + assert response.operation.name == "operations/spam" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_create_version_rest_interceptors(null_interceptor): + transport = transports.VersionsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.VersionsRestInterceptor(), + ) + client = VersionsClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + operation.Operation, "_set_result_from_operation" + ), mock.patch.object( + transports.VersionsRestInterceptor, "post_create_version" + ) as post, mock.patch.object( + transports.VersionsRestInterceptor, "pre_create_version" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.CreateVersionRequest.pb(appengine.CreateVersionRequest()) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = json_format.MessageToJson( + operations_pb2.Operation() + ) + + request = appengine.CreateVersionRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = operations_pb2.Operation() + + client.create_version( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_create_version_rest_bad_request( + transport: str = "rest", request_type=appengine.CreateVersionRequest +): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"parent": "apps/sample1/services/sample2"} + request_init["version"] = { + "name": "name_value", + "id": "id_value", + "automatic_scaling": { + "cool_down_period": {"seconds": 751, "nanos": 543}, + "cpu_utilization": { + "aggregation_window_length": {}, + "target_utilization": 0.19540000000000002, + }, + "max_concurrent_requests": 2499, + "max_idle_instances": 1898, + "max_total_instances": 2032, + "max_pending_latency": {}, + "min_idle_instances": 1896, + "min_total_instances": 2030, + "min_pending_latency": {}, + "request_utilization": { + "target_request_count_per_second": 3320, + "target_concurrent_requests": 2820, + }, + "disk_utilization": { + "target_write_bytes_per_second": 3096, + "target_write_ops_per_second": 2883, + "target_read_bytes_per_second": 2953, + "target_read_ops_per_second": 2740, + }, + "network_utilization": { + "target_sent_bytes_per_second": 2983, + "target_sent_packets_per_second": 3179, + "target_received_bytes_per_second": 3380, + "target_received_packets_per_second": 3576, + }, + "standard_scheduler_settings": { + "target_cpu_utilization": 0.23770000000000002, + "target_throughput_utilization": 0.3163, + "min_instances": 1387, + "max_instances": 1389, + }, + }, + "basic_scaling": {"idle_timeout": {}, "max_instances": 1389}, + "manual_scaling": {"instances": 968}, + "inbound_services": [1], + "instance_class": "instance_class_value", + "network": { + "forwarded_ports": ["forwarded_ports_value1", "forwarded_ports_value2"], + "instance_tag": "instance_tag_value", + "name": "name_value", + "subnetwork_name": "subnetwork_name_value", + "session_affinity": True, + }, + "zones": ["zones_value1", "zones_value2"], + "resources": { + "cpu": 0.328, + "disk_gb": 0.723, + "memory_gb": 0.961, + "volumes": [ + { + "name": "name_value", + "volume_type": "volume_type_value", + "size_gb": 0.739, + } + ], + "kms_key_reference": "kms_key_reference_value", + }, + "runtime": "runtime_value", + "runtime_channel": "runtime_channel_value", + "threadsafe": True, + "vm": True, + "app_engine_apis": True, + "beta_settings": {}, + "env": "env_value", + "serving_status": 1, + "created_by": "created_by_value", + "create_time": {"seconds": 751, "nanos": 543}, + "disk_usage_bytes": 1701, + "runtime_api_version": "runtime_api_version_value", + "runtime_main_executable_path": "runtime_main_executable_path_value", + "service_account": "service_account_value", + "handlers": [ + { + "url_regex": "url_regex_value", + "static_files": { + "path": "path_value", + "upload_path_regex": "upload_path_regex_value", + "http_headers": {}, + "mime_type": "mime_type_value", + "expiration": {}, + "require_matching_file": True, + "application_readable": True, + }, + "script": {"script_path": "script_path_value"}, + "api_endpoint": {"script_path": "script_path_value"}, + "security_level": 1, + "login": 1, + "auth_fail_action": 1, + "redirect_http_response_code": 1, + } + ], + "error_handlers": [ + { + "error_code": 1, + "static_file": "static_file_value", + "mime_type": "mime_type_value", + } + ], + "libraries": [{"name": "name_value", "version": "version_value"}], + "api_config": { + "auth_fail_action": 1, + "login": 1, + "script": "script_value", + "security_level": 1, + "url": "url_value", + }, + "env_variables": {}, + "build_env_variables": {}, + "default_expiration": {}, + "health_check": { + "disable_health_check": True, + "host": "host_value", + "healthy_threshold": 1819, + "unhealthy_threshold": 2046, + "restart_threshold": 1841, + "check_interval": {}, + "timeout": {}, + }, + "readiness_check": { + "path": "path_value", + "host": "host_value", + "failure_threshold": 1812, + "success_threshold": 1829, + "check_interval": {}, + "timeout": {}, + "app_start_timeout": {}, + }, + "liveness_check": { + "path": "path_value", + "host": "host_value", + "failure_threshold": 1812, + "success_threshold": 1829, + "check_interval": {}, + "timeout": {}, + "initial_delay": {}, + }, + "nobuild_files_regex": "nobuild_files_regex_value", + "deployment": { + "files": {}, + "container": {"image": "image_value"}, + "zip_": {"source_url": "source_url_value", "files_count": 1179}, + "cloud_build_options": { + "app_yaml_path": "app_yaml_path_value", + "cloud_build_timeout": {}, + }, + }, + "version_url": "version_url_value", + "endpoints_api_service": { + "name": "name_value", + "config_id": "config_id_value", + "rollout_strategy": 1, + "disable_trace_sampling": True, + }, + "entrypoint": {"shell": "shell_value"}, + "vpc_access_connector": {"name": "name_value", "egress_setting": 1}, + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.create_version(request) + + +def test_create_version_rest_error(): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.UpdateVersionRequest, + dict, + ], +) +def test_update_version_rest(request_type): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/services/sample2/versions/sample3"} + request_init["version"] = { + "name": "name_value", + "id": "id_value", + "automatic_scaling": { + "cool_down_period": {"seconds": 751, "nanos": 543}, + "cpu_utilization": { + "aggregation_window_length": {}, + "target_utilization": 0.19540000000000002, + }, + "max_concurrent_requests": 2499, + "max_idle_instances": 1898, + "max_total_instances": 2032, + "max_pending_latency": {}, + "min_idle_instances": 1896, + "min_total_instances": 2030, + "min_pending_latency": {}, + "request_utilization": { + "target_request_count_per_second": 3320, + "target_concurrent_requests": 2820, + }, + "disk_utilization": { + "target_write_bytes_per_second": 3096, + "target_write_ops_per_second": 2883, + "target_read_bytes_per_second": 2953, + "target_read_ops_per_second": 2740, + }, + "network_utilization": { + "target_sent_bytes_per_second": 2983, + "target_sent_packets_per_second": 3179, + "target_received_bytes_per_second": 3380, + "target_received_packets_per_second": 3576, + }, + "standard_scheduler_settings": { + "target_cpu_utilization": 0.23770000000000002, + "target_throughput_utilization": 0.3163, + "min_instances": 1387, + "max_instances": 1389, + }, + }, + "basic_scaling": {"idle_timeout": {}, "max_instances": 1389}, + "manual_scaling": {"instances": 968}, + "inbound_services": [1], + "instance_class": "instance_class_value", + "network": { + "forwarded_ports": ["forwarded_ports_value1", "forwarded_ports_value2"], + "instance_tag": "instance_tag_value", + "name": "name_value", + "subnetwork_name": "subnetwork_name_value", + "session_affinity": True, + }, + "zones": ["zones_value1", "zones_value2"], + "resources": { + "cpu": 0.328, + "disk_gb": 0.723, + "memory_gb": 0.961, + "volumes": [ + { + "name": "name_value", + "volume_type": "volume_type_value", + "size_gb": 0.739, + } + ], + "kms_key_reference": "kms_key_reference_value", + }, + "runtime": "runtime_value", + "runtime_channel": "runtime_channel_value", + "threadsafe": True, + "vm": True, + "app_engine_apis": True, + "beta_settings": {}, + "env": "env_value", + "serving_status": 1, + "created_by": "created_by_value", + "create_time": {"seconds": 751, "nanos": 543}, + "disk_usage_bytes": 1701, + "runtime_api_version": "runtime_api_version_value", + "runtime_main_executable_path": "runtime_main_executable_path_value", + "service_account": "service_account_value", + "handlers": [ + { + "url_regex": "url_regex_value", + "static_files": { + "path": "path_value", + "upload_path_regex": "upload_path_regex_value", + "http_headers": {}, + "mime_type": "mime_type_value", + "expiration": {}, + "require_matching_file": True, + "application_readable": True, + }, + "script": {"script_path": "script_path_value"}, + "api_endpoint": {"script_path": "script_path_value"}, + "security_level": 1, + "login": 1, + "auth_fail_action": 1, + "redirect_http_response_code": 1, + } + ], + "error_handlers": [ + { + "error_code": 1, + "static_file": "static_file_value", + "mime_type": "mime_type_value", + } + ], + "libraries": [{"name": "name_value", "version": "version_value"}], + "api_config": { + "auth_fail_action": 1, + "login": 1, + "script": "script_value", + "security_level": 1, + "url": "url_value", + }, + "env_variables": {}, + "build_env_variables": {}, + "default_expiration": {}, + "health_check": { + "disable_health_check": True, + "host": "host_value", + "healthy_threshold": 1819, + "unhealthy_threshold": 2046, + "restart_threshold": 1841, + "check_interval": {}, + "timeout": {}, + }, + "readiness_check": { + "path": "path_value", + "host": "host_value", + "failure_threshold": 1812, + "success_threshold": 1829, + "check_interval": {}, + "timeout": {}, + "app_start_timeout": {}, + }, + "liveness_check": { + "path": "path_value", + "host": "host_value", + "failure_threshold": 1812, + "success_threshold": 1829, + "check_interval": {}, + "timeout": {}, + "initial_delay": {}, + }, + "nobuild_files_regex": "nobuild_files_regex_value", + "deployment": { + "files": {}, + "container": {"image": "image_value"}, + "zip_": {"source_url": "source_url_value", "files_count": 1179}, + "cloud_build_options": { + "app_yaml_path": "app_yaml_path_value", + "cloud_build_timeout": {}, + }, + }, + "version_url": "version_url_value", + "endpoints_api_service": { + "name": "name_value", + "config_id": "config_id_value", + "rollout_strategy": 1, + "disable_trace_sampling": True, + }, + "entrypoint": {"shell": "shell_value"}, + "vpc_access_connector": {"name": "name_value", "egress_setting": 1}, + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = operations_pb2.Operation(name="operations/spam") + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + json_return_value = json_format.MessageToJson(return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.update_version(request) + + # Establish that the response is the type that we expect. + assert response.operation.name == "operations/spam" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_update_version_rest_interceptors(null_interceptor): + transport = transports.VersionsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.VersionsRestInterceptor(), + ) + client = VersionsClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + operation.Operation, "_set_result_from_operation" + ), mock.patch.object( + transports.VersionsRestInterceptor, "post_update_version" + ) as post, mock.patch.object( + transports.VersionsRestInterceptor, "pre_update_version" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.UpdateVersionRequest.pb(appengine.UpdateVersionRequest()) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = json_format.MessageToJson( + operations_pb2.Operation() + ) + + request = appengine.UpdateVersionRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = operations_pb2.Operation() + + client.update_version( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_update_version_rest_bad_request( + transport: str = "rest", request_type=appengine.UpdateVersionRequest +): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/services/sample2/versions/sample3"} + request_init["version"] = { + "name": "name_value", + "id": "id_value", + "automatic_scaling": { + "cool_down_period": {"seconds": 751, "nanos": 543}, + "cpu_utilization": { + "aggregation_window_length": {}, + "target_utilization": 0.19540000000000002, + }, + "max_concurrent_requests": 2499, + "max_idle_instances": 1898, + "max_total_instances": 2032, + "max_pending_latency": {}, + "min_idle_instances": 1896, + "min_total_instances": 2030, + "min_pending_latency": {}, + "request_utilization": { + "target_request_count_per_second": 3320, + "target_concurrent_requests": 2820, + }, + "disk_utilization": { + "target_write_bytes_per_second": 3096, + "target_write_ops_per_second": 2883, + "target_read_bytes_per_second": 2953, + "target_read_ops_per_second": 2740, + }, + "network_utilization": { + "target_sent_bytes_per_second": 2983, + "target_sent_packets_per_second": 3179, + "target_received_bytes_per_second": 3380, + "target_received_packets_per_second": 3576, + }, + "standard_scheduler_settings": { + "target_cpu_utilization": 0.23770000000000002, + "target_throughput_utilization": 0.3163, + "min_instances": 1387, + "max_instances": 1389, + }, + }, + "basic_scaling": {"idle_timeout": {}, "max_instances": 1389}, + "manual_scaling": {"instances": 968}, + "inbound_services": [1], + "instance_class": "instance_class_value", + "network": { + "forwarded_ports": ["forwarded_ports_value1", "forwarded_ports_value2"], + "instance_tag": "instance_tag_value", + "name": "name_value", + "subnetwork_name": "subnetwork_name_value", + "session_affinity": True, + }, + "zones": ["zones_value1", "zones_value2"], + "resources": { + "cpu": 0.328, + "disk_gb": 0.723, + "memory_gb": 0.961, + "volumes": [ + { + "name": "name_value", + "volume_type": "volume_type_value", + "size_gb": 0.739, + } + ], + "kms_key_reference": "kms_key_reference_value", + }, + "runtime": "runtime_value", + "runtime_channel": "runtime_channel_value", + "threadsafe": True, + "vm": True, + "app_engine_apis": True, + "beta_settings": {}, + "env": "env_value", + "serving_status": 1, + "created_by": "created_by_value", + "create_time": {"seconds": 751, "nanos": 543}, + "disk_usage_bytes": 1701, + "runtime_api_version": "runtime_api_version_value", + "runtime_main_executable_path": "runtime_main_executable_path_value", + "service_account": "service_account_value", + "handlers": [ + { + "url_regex": "url_regex_value", + "static_files": { + "path": "path_value", + "upload_path_regex": "upload_path_regex_value", + "http_headers": {}, + "mime_type": "mime_type_value", + "expiration": {}, + "require_matching_file": True, + "application_readable": True, + }, + "script": {"script_path": "script_path_value"}, + "api_endpoint": {"script_path": "script_path_value"}, + "security_level": 1, + "login": 1, + "auth_fail_action": 1, + "redirect_http_response_code": 1, + } + ], + "error_handlers": [ + { + "error_code": 1, + "static_file": "static_file_value", + "mime_type": "mime_type_value", + } + ], + "libraries": [{"name": "name_value", "version": "version_value"}], + "api_config": { + "auth_fail_action": 1, + "login": 1, + "script": "script_value", + "security_level": 1, + "url": "url_value", + }, + "env_variables": {}, + "build_env_variables": {}, + "default_expiration": {}, + "health_check": { + "disable_health_check": True, + "host": "host_value", + "healthy_threshold": 1819, + "unhealthy_threshold": 2046, + "restart_threshold": 1841, + "check_interval": {}, + "timeout": {}, + }, + "readiness_check": { + "path": "path_value", + "host": "host_value", + "failure_threshold": 1812, + "success_threshold": 1829, + "check_interval": {}, + "timeout": {}, + "app_start_timeout": {}, + }, + "liveness_check": { + "path": "path_value", + "host": "host_value", + "failure_threshold": 1812, + "success_threshold": 1829, + "check_interval": {}, + "timeout": {}, + "initial_delay": {}, + }, + "nobuild_files_regex": "nobuild_files_regex_value", + "deployment": { + "files": {}, + "container": {"image": "image_value"}, + "zip_": {"source_url": "source_url_value", "files_count": 1179}, + "cloud_build_options": { + "app_yaml_path": "app_yaml_path_value", + "cloud_build_timeout": {}, + }, + }, + "version_url": "version_url_value", + "endpoints_api_service": { + "name": "name_value", + "config_id": "config_id_value", + "rollout_strategy": 1, + "disable_trace_sampling": True, + }, + "entrypoint": {"shell": "shell_value"}, + "vpc_access_connector": {"name": "name_value", "egress_setting": 1}, + } + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.update_version(request) + + +def test_update_version_rest_error(): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + +@pytest.mark.parametrize( + "request_type", + [ + appengine.DeleteVersionRequest, + dict, + ], +) +def test_delete_version_rest(request_type): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/services/sample2/versions/sample3"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), "request") as req: + # Designate an appropriate value for the returned response. + return_value = operations_pb2.Operation(name="operations/spam") + + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 200 + json_return_value = json_format.MessageToJson(return_value) + + response_value._content = json_return_value.encode("UTF-8") + req.return_value = response_value + response = client.delete_version(request) + + # Establish that the response is the type that we expect. + assert response.operation.name == "operations/spam" + + +@pytest.mark.parametrize("null_interceptor", [True, False]) +def test_delete_version_rest_interceptors(null_interceptor): + transport = transports.VersionsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptor=None if null_interceptor else transports.VersionsRestInterceptor(), + ) + client = VersionsClient(transport=transport) + with mock.patch.object( + type(client.transport._session), "request" + ) as req, mock.patch.object( + path_template, "transcode" + ) as transcode, mock.patch.object( + operation.Operation, "_set_result_from_operation" + ), mock.patch.object( + transports.VersionsRestInterceptor, "post_delete_version" + ) as post, mock.patch.object( + transports.VersionsRestInterceptor, "pre_delete_version" + ) as pre: + pre.assert_not_called() + post.assert_not_called() + pb_message = appengine.DeleteVersionRequest.pb(appengine.DeleteVersionRequest()) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = Response() + req.return_value.status_code = 200 + req.return_value.request = PreparedRequest() + req.return_value._content = json_format.MessageToJson( + operations_pb2.Operation() + ) + + request = appengine.DeleteVersionRequest() + metadata = [ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = operations_pb2.Operation() + + client.delete_version( + request, + metadata=[ + ("key", "val"), + ("cephalopod", "squid"), + ], + ) + + pre.assert_called_once() + post.assert_called_once() + + +def test_delete_version_rest_bad_request( + transport: str = "rest", request_type=appengine.DeleteVersionRequest +): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport, + ) + + # send a request that will satisfy transcoding + request_init = {"name": "apps/sample1/services/sample2/versions/sample3"} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(Session, "request") as req, pytest.raises( + core_exceptions.BadRequest + ): + # Wrap the value into a proper Response obj + response_value = Response() + response_value.status_code = 400 + response_value.request = Request() + req.return_value = response_value + client.delete_version(request) + + +def test_delete_version_rest_error(): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), transport="rest" + ) + + def test_credentials_transport_error(): # It is an error to provide credentials and a transport instance. transport = transports.VersionsGrpcTransport( @@ -1743,6 +3115,7 @@ def test_transport_get_channel(): [ transports.VersionsGrpcTransport, transports.VersionsGrpcAsyncIOTransport, + transports.VersionsRestTransport, ], ) def test_transport_adc(transport_class): @@ -1757,6 +3130,7 @@ def test_transport_adc(transport_class): "transport_name", [ "grpc", + "rest", ], ) def test_transport_kind(transport_name): @@ -1907,6 +3281,7 @@ def test_versions_transport_auth_adc(transport_class): [ transports.VersionsGrpcTransport, transports.VersionsGrpcAsyncIOTransport, + transports.VersionsRestTransport, ], ) def test_versions_transport_auth_gdch_credentials(transport_class): @@ -2005,11 +3380,40 @@ def test_versions_grpc_transport_client_cert_source_for_mtls(transport_class): ) +def test_versions_http_transport_client_cert_source_for_mtls(): + cred = ga_credentials.AnonymousCredentials() + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.configure_mtls_channel" + ) as mock_configure_mtls_channel: + transports.VersionsRestTransport( + credentials=cred, client_cert_source_for_mtls=client_cert_source_callback + ) + mock_configure_mtls_channel.assert_called_once_with(client_cert_source_callback) + + +def test_versions_rest_lro_client(): + client = VersionsClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + transport = client.transport + + # Ensure that we have a api-core operations client. + assert isinstance( + transport.operations_client, + operations_v1.AbstractOperationsClient, + ) + + # Ensure that subsequent calls to the property send the exact same object. + assert transport.operations_client is transport.operations_client + + @pytest.mark.parametrize( "transport_name", [ "grpc", "grpc_asyncio", + "rest", ], ) def test_versions_host_no_port(transport_name): @@ -2020,7 +3424,11 @@ def test_versions_host_no_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:443") + assert client.transport._host == ( + "appengine.googleapis.com:443" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com" + ) @pytest.mark.parametrize( @@ -2028,6 +3436,7 @@ def test_versions_host_no_port(transport_name): [ "grpc", "grpc_asyncio", + "rest", ], ) def test_versions_host_with_port(transport_name): @@ -2038,7 +3447,45 @@ def test_versions_host_with_port(transport_name): ), transport=transport_name, ) - assert client.transport._host == ("appengine.googleapis.com:8000") + assert client.transport._host == ( + "appengine.googleapis.com:8000" + if transport_name in ["grpc", "grpc_asyncio"] + else "https://appengine.googleapis.com:8000" + ) + + +@pytest.mark.parametrize( + "transport_name", + [ + "rest", + ], +) +def test_versions_client_transport_session_collision(transport_name): + creds1 = ga_credentials.AnonymousCredentials() + creds2 = ga_credentials.AnonymousCredentials() + client1 = VersionsClient( + credentials=creds1, + transport=transport_name, + ) + client2 = VersionsClient( + credentials=creds2, + transport=transport_name, + ) + session1 = client1.transport.list_versions._session + session2 = client2.transport.list_versions._session + assert session1 != session2 + session1 = client1.transport.get_version._session + session2 = client2.transport.get_version._session + assert session1 != session2 + session1 = client1.transport.create_version._session + session2 = client2.transport.create_version._session + assert session1 != session2 + session1 = client1.transport.update_version._session + session2 = client2.transport.update_version._session + assert session1 != session2 + session1 = client1.transport.delete_version._session + session2 = client2.transport.delete_version._session + assert session1 != session2 def test_versions_grpc_transport_channel(): @@ -2335,6 +3782,7 @@ async def test_transport_close_async(): def test_transport_close(): transports = { + "rest": "_session", "grpc": "_grpc_channel", } @@ -2352,6 +3800,7 @@ def test_transport_close(): def test_client_ctx(): transports = [ + "rest", "grpc", ] for transport in transports: