From 84ba173d4eadd75cc5289ce76ee800909b20a5ff Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 29 Jul 2021 13:52:23 +0000 Subject: [PATCH] feat: Add restricted Retail Search features for Retail API v2. (#68) PiperOrigin-RevId: 387366941 Source-Link: https://github.com/googleapis/googleapis/commit/cea896d1032b311179e5114b3e5a1b798e7040b7 Source-Link: https://github.com/googleapis/googleapis-gen/commit/11a99d5ec0606cace54cee951bc023650af5be7d --- docs/retail_v2/completion_service.rst | 6 + docs/retail_v2/product_service.rst | 4 + docs/retail_v2/search_service.rst | 10 + docs/retail_v2/services.rst | 2 + google/cloud/retail/__init__.py | 70 + google/cloud/retail_v2/__init__.py | 64 + google/cloud/retail_v2/gapic_metadata.json | 118 ++ .../services/catalog_service/async_client.py | 190 ++- .../services/catalog_service/client.py | 204 ++- .../catalog_service/transports/base.py | 28 + .../catalog_service/transports/grpc.py | 102 ++ .../transports/grpc_asyncio.py | 104 ++ .../services/completion_service/__init__.py | 22 + .../completion_service/async_client.py | 300 ++++ .../services/completion_service/client.py | 493 ++++++ .../completion_service/transports/__init__.py | 33 + .../completion_service/transports/base.py | 195 +++ .../completion_service/transports/grpc.py | 319 ++++ .../transports/grpc_asyncio.py | 324 ++++ .../services/product_service/async_client.py | 504 +++++- .../services/product_service/client.py | 506 +++++- .../services/product_service/pagers.py | 156 ++ .../product_service/transports/base.py | 55 + .../product_service/transports/grpc.py | 190 +++ .../transports/grpc_asyncio.py | 195 +++ .../services/search_service/__init__.py | 22 + .../services/search_service/async_client.py | 246 +++ .../services/search_service/client.py | 454 ++++++ .../services/search_service/pagers.py | 155 ++ .../search_service/transports/__init__.py | 33 + .../search_service/transports/base.py | 170 ++ .../search_service/transports/grpc.py | 263 +++ .../search_service/transports/grpc_asyncio.py | 268 ++++ .../user_event_service/async_client.py | 2 + .../services/user_event_service/client.py | 16 + google/cloud/retail_v2/types/__init__.py | 60 + google/cloud/retail_v2/types/catalog.py | 8 +- .../cloud/retail_v2/types/catalog_service.py | 76 +- google/cloud/retail_v2/types/common.py | 339 +++- .../retail_v2/types/completion_service.py | 171 ++ google/cloud/retail_v2/types/import_config.py | 138 +- .../retail_v2/types/prediction_service.py | 82 +- google/cloud/retail_v2/types/product.py | 328 +++- .../cloud/retail_v2/types/product_service.py | 412 ++++- google/cloud/retail_v2/types/purge_config.py | 7 +- .../cloud/retail_v2/types/search_service.py | 782 +++++++++ google/cloud/retail_v2/types/user_event.py | 125 +- owlbot.py | 14 + scripts/fixup_retail_v2_keywords.py | 13 +- .../gapic/retail_v2/test_catalog_service.py | 493 +++++- .../retail_v2/test_completion_service.py | 1413 ++++++++++++++++ .../retail_v2/test_prediction_service.py | 2 + .../gapic/retail_v2/test_product_service.py | 1132 ++++++++++++- .../gapic/retail_v2/test_search_service.py | 1424 +++++++++++++++++ .../retail_v2/test_user_event_service.py | 83 +- 55 files changed, 12778 insertions(+), 147 deletions(-) create mode 100644 docs/retail_v2/completion_service.rst create mode 100644 docs/retail_v2/search_service.rst create mode 100644 google/cloud/retail_v2/services/completion_service/__init__.py create mode 100644 google/cloud/retail_v2/services/completion_service/async_client.py create mode 100644 google/cloud/retail_v2/services/completion_service/client.py create mode 100644 google/cloud/retail_v2/services/completion_service/transports/__init__.py create mode 100644 google/cloud/retail_v2/services/completion_service/transports/base.py create mode 100644 google/cloud/retail_v2/services/completion_service/transports/grpc.py create mode 100644 google/cloud/retail_v2/services/completion_service/transports/grpc_asyncio.py create mode 100644 google/cloud/retail_v2/services/product_service/pagers.py create mode 100644 google/cloud/retail_v2/services/search_service/__init__.py create mode 100644 google/cloud/retail_v2/services/search_service/async_client.py create mode 100644 google/cloud/retail_v2/services/search_service/client.py create mode 100644 google/cloud/retail_v2/services/search_service/pagers.py create mode 100644 google/cloud/retail_v2/services/search_service/transports/__init__.py create mode 100644 google/cloud/retail_v2/services/search_service/transports/base.py create mode 100644 google/cloud/retail_v2/services/search_service/transports/grpc.py create mode 100644 google/cloud/retail_v2/services/search_service/transports/grpc_asyncio.py create mode 100644 google/cloud/retail_v2/types/completion_service.py create mode 100644 google/cloud/retail_v2/types/search_service.py create mode 100644 tests/unit/gapic/retail_v2/test_completion_service.py create mode 100644 tests/unit/gapic/retail_v2/test_search_service.py diff --git a/docs/retail_v2/completion_service.rst b/docs/retail_v2/completion_service.rst new file mode 100644 index 00000000..551f89ed --- /dev/null +++ b/docs/retail_v2/completion_service.rst @@ -0,0 +1,6 @@ +CompletionService +----------------------------------- + +.. automodule:: google.cloud.retail_v2.services.completion_service + :members: + :inherited-members: diff --git a/docs/retail_v2/product_service.rst b/docs/retail_v2/product_service.rst index 59ba0f7e..0b8fa911 100644 --- a/docs/retail_v2/product_service.rst +++ b/docs/retail_v2/product_service.rst @@ -4,3 +4,7 @@ ProductService .. automodule:: google.cloud.retail_v2.services.product_service :members: :inherited-members: + +.. automodule:: google.cloud.retail_v2.services.product_service.pagers + :members: + :inherited-members: diff --git a/docs/retail_v2/search_service.rst b/docs/retail_v2/search_service.rst new file mode 100644 index 00000000..af72819d --- /dev/null +++ b/docs/retail_v2/search_service.rst @@ -0,0 +1,10 @@ +SearchService +------------------------------- + +.. automodule:: google.cloud.retail_v2.services.search_service + :members: + :inherited-members: + +.. automodule:: google.cloud.retail_v2.services.search_service.pagers + :members: + :inherited-members: diff --git a/docs/retail_v2/services.rst b/docs/retail_v2/services.rst index 0308da27..f86138d7 100644 --- a/docs/retail_v2/services.rst +++ b/docs/retail_v2/services.rst @@ -4,6 +4,8 @@ Services for Google Cloud Retail v2 API :maxdepth: 2 catalog_service + completion_service prediction_service product_service + search_service user_event_service diff --git a/google/cloud/retail/__init__.py b/google/cloud/retail/__init__.py index d7f36d37..0f4ce4b7 100644 --- a/google/cloud/retail/__init__.py +++ b/google/cloud/retail/__init__.py @@ -18,6 +18,12 @@ from google.cloud.retail_v2.services.catalog_service.async_client import ( CatalogServiceAsyncClient, ) +from google.cloud.retail_v2.services.completion_service.client import ( + CompletionServiceClient, +) +from google.cloud.retail_v2.services.completion_service.async_client import ( + CompletionServiceAsyncClient, +) from google.cloud.retail_v2.services.prediction_service.client import ( PredictionServiceClient, ) @@ -28,6 +34,10 @@ from google.cloud.retail_v2.services.product_service.async_client import ( ProductServiceAsyncClient, ) +from google.cloud.retail_v2.services.search_service.client import SearchServiceClient +from google.cloud.retail_v2.services.search_service.async_client import ( + SearchServiceAsyncClient, +) from google.cloud.retail_v2.services.user_event_service.client import ( UserEventServiceClient, ) @@ -37,15 +47,29 @@ from google.cloud.retail_v2.types.catalog import Catalog from google.cloud.retail_v2.types.catalog import ProductLevelConfig +from google.cloud.retail_v2.types.catalog_service import GetDefaultBranchRequest +from google.cloud.retail_v2.types.catalog_service import GetDefaultBranchResponse from google.cloud.retail_v2.types.catalog_service import ListCatalogsRequest from google.cloud.retail_v2.types.catalog_service import ListCatalogsResponse +from google.cloud.retail_v2.types.catalog_service import SetDefaultBranchRequest from google.cloud.retail_v2.types.catalog_service import UpdateCatalogRequest +from google.cloud.retail_v2.types.common import Audience +from google.cloud.retail_v2.types.common import ColorInfo from google.cloud.retail_v2.types.common import CustomAttribute +from google.cloud.retail_v2.types.common import FulfillmentInfo from google.cloud.retail_v2.types.common import Image +from google.cloud.retail_v2.types.common import Interval from google.cloud.retail_v2.types.common import PriceInfo +from google.cloud.retail_v2.types.common import Promotion +from google.cloud.retail_v2.types.common import Rating from google.cloud.retail_v2.types.common import UserInfo +from google.cloud.retail_v2.types.completion_service import CompleteQueryRequest +from google.cloud.retail_v2.types.completion_service import CompleteQueryResponse from google.cloud.retail_v2.types.import_config import BigQuerySource +from google.cloud.retail_v2.types.import_config import CompletionDataInputConfig from google.cloud.retail_v2.types.import_config import GcsSource +from google.cloud.retail_v2.types.import_config import ImportCompletionDataRequest +from google.cloud.retail_v2.types.import_config import ImportCompletionDataResponse from google.cloud.retail_v2.types.import_config import ImportErrorsConfig from google.cloud.retail_v2.types.import_config import ImportMetadata from google.cloud.retail_v2.types.import_config import ImportProductsRequest @@ -60,13 +84,27 @@ from google.cloud.retail_v2.types.prediction_service import PredictRequest from google.cloud.retail_v2.types.prediction_service import PredictResponse from google.cloud.retail_v2.types.product import Product +from google.cloud.retail_v2.types.product_service import AddFulfillmentPlacesMetadata +from google.cloud.retail_v2.types.product_service import AddFulfillmentPlacesRequest +from google.cloud.retail_v2.types.product_service import AddFulfillmentPlacesResponse from google.cloud.retail_v2.types.product_service import CreateProductRequest from google.cloud.retail_v2.types.product_service import DeleteProductRequest from google.cloud.retail_v2.types.product_service import GetProductRequest +from google.cloud.retail_v2.types.product_service import ListProductsRequest +from google.cloud.retail_v2.types.product_service import ListProductsResponse +from google.cloud.retail_v2.types.product_service import RemoveFulfillmentPlacesMetadata +from google.cloud.retail_v2.types.product_service import RemoveFulfillmentPlacesRequest +from google.cloud.retail_v2.types.product_service import RemoveFulfillmentPlacesResponse +from google.cloud.retail_v2.types.product_service import SetInventoryMetadata +from google.cloud.retail_v2.types.product_service import SetInventoryRequest +from google.cloud.retail_v2.types.product_service import SetInventoryResponse from google.cloud.retail_v2.types.product_service import UpdateProductRequest from google.cloud.retail_v2.types.purge_config import PurgeMetadata from google.cloud.retail_v2.types.purge_config import PurgeUserEventsRequest from google.cloud.retail_v2.types.purge_config import PurgeUserEventsResponse +from google.cloud.retail_v2.types.search_service import SearchRequest +from google.cloud.retail_v2.types.search_service import SearchResponse +from google.cloud.retail_v2.types.user_event import CompletionDetail from google.cloud.retail_v2.types.user_event import ProductDetail from google.cloud.retail_v2.types.user_event import PurchaseTransaction from google.cloud.retail_v2.types.user_event import UserEvent @@ -79,23 +117,41 @@ __all__ = ( "CatalogServiceClient", "CatalogServiceAsyncClient", + "CompletionServiceClient", + "CompletionServiceAsyncClient", "PredictionServiceClient", "PredictionServiceAsyncClient", "ProductServiceClient", "ProductServiceAsyncClient", + "SearchServiceClient", + "SearchServiceAsyncClient", "UserEventServiceClient", "UserEventServiceAsyncClient", "Catalog", "ProductLevelConfig", + "GetDefaultBranchRequest", + "GetDefaultBranchResponse", "ListCatalogsRequest", "ListCatalogsResponse", + "SetDefaultBranchRequest", "UpdateCatalogRequest", + "Audience", + "ColorInfo", "CustomAttribute", + "FulfillmentInfo", "Image", + "Interval", "PriceInfo", + "Promotion", + "Rating", "UserInfo", + "CompleteQueryRequest", + "CompleteQueryResponse", "BigQuerySource", + "CompletionDataInputConfig", "GcsSource", + "ImportCompletionDataRequest", + "ImportCompletionDataResponse", "ImportErrorsConfig", "ImportMetadata", "ImportProductsRequest", @@ -110,13 +166,27 @@ "PredictRequest", "PredictResponse", "Product", + "AddFulfillmentPlacesMetadata", + "AddFulfillmentPlacesRequest", + "AddFulfillmentPlacesResponse", "CreateProductRequest", "DeleteProductRequest", "GetProductRequest", + "ListProductsRequest", + "ListProductsResponse", + "RemoveFulfillmentPlacesMetadata", + "RemoveFulfillmentPlacesRequest", + "RemoveFulfillmentPlacesResponse", + "SetInventoryMetadata", + "SetInventoryRequest", + "SetInventoryResponse", "UpdateProductRequest", "PurgeMetadata", "PurgeUserEventsRequest", "PurgeUserEventsResponse", + "SearchRequest", + "SearchResponse", + "CompletionDetail", "ProductDetail", "PurchaseTransaction", "UserEvent", diff --git a/google/cloud/retail_v2/__init__.py b/google/cloud/retail_v2/__init__.py index bae85bd0..fdf54805 100644 --- a/google/cloud/retail_v2/__init__.py +++ b/google/cloud/retail_v2/__init__.py @@ -16,24 +16,42 @@ from .services.catalog_service import CatalogServiceClient from .services.catalog_service import CatalogServiceAsyncClient +from .services.completion_service import CompletionServiceClient +from .services.completion_service import CompletionServiceAsyncClient from .services.prediction_service import PredictionServiceClient from .services.prediction_service import PredictionServiceAsyncClient from .services.product_service import ProductServiceClient from .services.product_service import ProductServiceAsyncClient +from .services.search_service import SearchServiceClient +from .services.search_service import SearchServiceAsyncClient from .services.user_event_service import UserEventServiceClient from .services.user_event_service import UserEventServiceAsyncClient from .types.catalog import Catalog from .types.catalog import ProductLevelConfig +from .types.catalog_service import GetDefaultBranchRequest +from .types.catalog_service import GetDefaultBranchResponse from .types.catalog_service import ListCatalogsRequest from .types.catalog_service import ListCatalogsResponse +from .types.catalog_service import SetDefaultBranchRequest from .types.catalog_service import UpdateCatalogRequest +from .types.common import Audience +from .types.common import ColorInfo from .types.common import CustomAttribute +from .types.common import FulfillmentInfo from .types.common import Image +from .types.common import Interval from .types.common import PriceInfo +from .types.common import Promotion +from .types.common import Rating from .types.common import UserInfo +from .types.completion_service import CompleteQueryRequest +from .types.completion_service import CompleteQueryResponse from .types.import_config import BigQuerySource +from .types.import_config import CompletionDataInputConfig from .types.import_config import GcsSource +from .types.import_config import ImportCompletionDataRequest +from .types.import_config import ImportCompletionDataResponse from .types.import_config import ImportErrorsConfig from .types.import_config import ImportMetadata from .types.import_config import ImportProductsRequest @@ -48,13 +66,27 @@ from .types.prediction_service import PredictRequest from .types.prediction_service import PredictResponse from .types.product import Product +from .types.product_service import AddFulfillmentPlacesMetadata +from .types.product_service import AddFulfillmentPlacesRequest +from .types.product_service import AddFulfillmentPlacesResponse from .types.product_service import CreateProductRequest from .types.product_service import DeleteProductRequest from .types.product_service import GetProductRequest +from .types.product_service import ListProductsRequest +from .types.product_service import ListProductsResponse +from .types.product_service import RemoveFulfillmentPlacesMetadata +from .types.product_service import RemoveFulfillmentPlacesRequest +from .types.product_service import RemoveFulfillmentPlacesResponse +from .types.product_service import SetInventoryMetadata +from .types.product_service import SetInventoryRequest +from .types.product_service import SetInventoryResponse from .types.product_service import UpdateProductRequest from .types.purge_config import PurgeMetadata from .types.purge_config import PurgeUserEventsRequest from .types.purge_config import PurgeUserEventsResponse +from .types.search_service import SearchRequest +from .types.search_service import SearchResponse +from .types.user_event import CompletionDetail from .types.user_event import ProductDetail from .types.user_event import PurchaseTransaction from .types.user_event import UserEvent @@ -66,27 +98,47 @@ __all__ = ( "CatalogServiceAsyncClient", + "CompletionServiceAsyncClient", "PredictionServiceAsyncClient", "ProductServiceAsyncClient", + "SearchServiceAsyncClient", "UserEventServiceAsyncClient", + "AddFulfillmentPlacesMetadata", + "AddFulfillmentPlacesRequest", + "AddFulfillmentPlacesResponse", + "Audience", "BigQuerySource", "Catalog", "CatalogServiceClient", "CollectUserEventRequest", + "ColorInfo", + "CompleteQueryRequest", + "CompleteQueryResponse", + "CompletionDataInputConfig", + "CompletionDetail", + "CompletionServiceClient", "CreateProductRequest", "CustomAttribute", "DeleteProductRequest", + "FulfillmentInfo", "GcsSource", + "GetDefaultBranchRequest", + "GetDefaultBranchResponse", "GetProductRequest", "Image", + "ImportCompletionDataRequest", + "ImportCompletionDataResponse", "ImportErrorsConfig", "ImportMetadata", "ImportProductsRequest", "ImportProductsResponse", "ImportUserEventsRequest", "ImportUserEventsResponse", + "Interval", "ListCatalogsRequest", "ListCatalogsResponse", + "ListProductsRequest", + "ListProductsResponse", "PredictRequest", "PredictResponse", "PredictionServiceClient", @@ -97,13 +149,25 @@ "ProductInputConfig", "ProductLevelConfig", "ProductServiceClient", + "Promotion", "PurchaseTransaction", "PurgeMetadata", "PurgeUserEventsRequest", "PurgeUserEventsResponse", + "Rating", "RejoinUserEventsMetadata", "RejoinUserEventsRequest", "RejoinUserEventsResponse", + "RemoveFulfillmentPlacesMetadata", + "RemoveFulfillmentPlacesRequest", + "RemoveFulfillmentPlacesResponse", + "SearchRequest", + "SearchResponse", + "SearchServiceClient", + "SetDefaultBranchRequest", + "SetInventoryMetadata", + "SetInventoryRequest", + "SetInventoryResponse", "UpdateCatalogRequest", "UpdateProductRequest", "UserEvent", diff --git a/google/cloud/retail_v2/gapic_metadata.json b/google/cloud/retail_v2/gapic_metadata.json index 967d6706..088fcc42 100644 --- a/google/cloud/retail_v2/gapic_metadata.json +++ b/google/cloud/retail_v2/gapic_metadata.json @@ -10,11 +10,21 @@ "grpc": { "libraryClient": "CatalogServiceClient", "rpcs": { + "GetDefaultBranch": { + "methods": [ + "get_default_branch" + ] + }, "ListCatalogs": { "methods": [ "list_catalogs" ] }, + "SetDefaultBranch": { + "methods": [ + "set_default_branch" + ] + }, "UpdateCatalog": { "methods": [ "update_catalog" @@ -25,11 +35,21 @@ "grpc-async": { "libraryClient": "CatalogServiceAsyncClient", "rpcs": { + "GetDefaultBranch": { + "methods": [ + "get_default_branch" + ] + }, "ListCatalogs": { "methods": [ "list_catalogs" ] }, + "SetDefaultBranch": { + "methods": [ + "set_default_branch" + ] + }, "UpdateCatalog": { "methods": [ "update_catalog" @@ -39,6 +59,40 @@ } } }, + "CompletionService": { + "clients": { + "grpc": { + "libraryClient": "CompletionServiceClient", + "rpcs": { + "CompleteQuery": { + "methods": [ + "complete_query" + ] + }, + "ImportCompletionData": { + "methods": [ + "import_completion_data" + ] + } + } + }, + "grpc-async": { + "libraryClient": "CompletionServiceAsyncClient", + "rpcs": { + "CompleteQuery": { + "methods": [ + "complete_query" + ] + }, + "ImportCompletionData": { + "methods": [ + "import_completion_data" + ] + } + } + } + } + }, "PredictionService": { "clients": { "grpc": { @@ -68,6 +122,11 @@ "grpc": { "libraryClient": "ProductServiceClient", "rpcs": { + "AddFulfillmentPlaces": { + "methods": [ + "add_fulfillment_places" + ] + }, "CreateProduct": { "methods": [ "create_product" @@ -88,6 +147,21 @@ "import_products" ] }, + "ListProducts": { + "methods": [ + "list_products" + ] + }, + "RemoveFulfillmentPlaces": { + "methods": [ + "remove_fulfillment_places" + ] + }, + "SetInventory": { + "methods": [ + "set_inventory" + ] + }, "UpdateProduct": { "methods": [ "update_product" @@ -98,6 +172,11 @@ "grpc-async": { "libraryClient": "ProductServiceAsyncClient", "rpcs": { + "AddFulfillmentPlaces": { + "methods": [ + "add_fulfillment_places" + ] + }, "CreateProduct": { "methods": [ "create_product" @@ -118,6 +197,21 @@ "import_products" ] }, + "ListProducts": { + "methods": [ + "list_products" + ] + }, + "RemoveFulfillmentPlaces": { + "methods": [ + "remove_fulfillment_places" + ] + }, + "SetInventory": { + "methods": [ + "set_inventory" + ] + }, "UpdateProduct": { "methods": [ "update_product" @@ -127,6 +221,30 @@ } } }, + "SearchService": { + "clients": { + "grpc": { + "libraryClient": "SearchServiceClient", + "rpcs": { + "Search": { + "methods": [ + "search" + ] + } + } + }, + "grpc-async": { + "libraryClient": "SearchServiceAsyncClient", + "rpcs": { + "Search": { + "methods": [ + "search" + ] + } + } + } + } + }, "UserEventService": { "clients": { "grpc": { diff --git a/google/cloud/retail_v2/services/catalog_service/async_client.py b/google/cloud/retail_v2/services/catalog_service/async_client.py index aafd3840..65f2c54b 100644 --- a/google/cloud/retail_v2/services/catalog_service/async_client.py +++ b/google/cloud/retail_v2/services/catalog_service/async_client.py @@ -31,6 +31,7 @@ from google.cloud.retail_v2.types import catalog as gcr_catalog from google.cloud.retail_v2.types import catalog_service from google.protobuf import field_mask_pb2 # type: ignore +from google.protobuf import timestamp_pb2 # type: ignore from .transports.base import CatalogServiceTransport, DEFAULT_CLIENT_INFO from .transports.grpc_asyncio import CatalogServiceGrpcAsyncIOTransport from .client import CatalogServiceClient @@ -44,6 +45,8 @@ class CatalogServiceAsyncClient: DEFAULT_ENDPOINT = CatalogServiceClient.DEFAULT_ENDPOINT DEFAULT_MTLS_ENDPOINT = CatalogServiceClient.DEFAULT_MTLS_ENDPOINT + branch_path = staticmethod(CatalogServiceClient.branch_path) + parse_branch_path = staticmethod(CatalogServiceClient.parse_branch_path) catalog_path = staticmethod(CatalogServiceClient.catalog_path) parse_catalog_path = staticmethod(CatalogServiceClient.parse_catalog_path) common_billing_account_path = staticmethod( @@ -286,11 +289,7 @@ async def update_catalog( should not be set. update_mask (:class:`google.protobuf.field_mask_pb2.FieldMask`): Indicates which fields in the provided - [Catalog][google.cloud.retail.v2.Catalog] to update. If - not set, will only update the - [Catalog.product_level_config][google.cloud.retail.v2.Catalog.product_level_config] - field, which is also the only currently supported field - to update. + [Catalog][google.cloud.retail.v2.Catalog] to update. If an unsupported or unknown field is provided, an INVALID_ARGUMENT error is returned. @@ -349,6 +348,187 @@ async def update_catalog( # Done; return the response. return response + async def set_default_branch( + self, + request: catalog_service.SetDefaultBranchRequest = None, + *, + catalog: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> None: + r"""Set a specified branch id as default branch. API methods such as + [SearchService.Search][google.cloud.retail.v2.SearchService.Search], + [ProductService.GetProduct][google.cloud.retail.v2.ProductService.GetProduct], + [ProductService.ListProducts][google.cloud.retail.v2.ProductService.ListProducts] + will treat requests using "default_branch" to the actual branch + id set as default. + + For example, if ``projects/*/locations/*/catalogs/*/branches/1`` + is set as default, setting + [SearchRequest.branch][google.cloud.retail.v2.SearchRequest.branch] + to ``projects/*/locations/*/catalogs/*/branches/default_branch`` + is equivalent to setting + [SearchRequest.branch][google.cloud.retail.v2.SearchRequest.branch] + to ``projects/*/locations/*/catalogs/*/branches/1``. + + Using multiple branches can be useful when developers would like + to have a staging branch to test and verify for future usage. + When it becomes ready, developers switch on the staging branch + using this API while keeping using + ``projects/*/locations/*/catalogs/*/branches/default_branch`` as + [SearchRequest.branch][google.cloud.retail.v2.SearchRequest.branch] + to route the traffic to this staging branch. + + CAUTION: If you have live predict/search traffic, switching the + default branch could potentially cause outages if the ID space + of the new branch is very different from the old one. + + More specifically: + + - PredictionService will only return product IDs from branch + {newBranch}. + - SearchService will only return product IDs from branch + {newBranch} (if branch is not explicitly set). + - UserEventService will only join events with products from + branch {newBranch}. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Args: + request (:class:`google.cloud.retail_v2.types.SetDefaultBranchRequest`): + The request object. Request message to set a specified + branch as new default_branch. + catalog (:class:`str`): + Full resource name of the catalog, such as + ``projects/*/locations/global/catalogs/default_catalog``. + + This corresponds to the ``catalog`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + 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. + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([catalog]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + request = catalog_service.SetDefaultBranchRequest(request) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if catalog is not None: + request.catalog = catalog + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method_async.wrap_method( + self._client._transport.set_default_branch, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("catalog", request.catalog),)), + ) + + # Send the request. + await rpc( + request, retry=retry, timeout=timeout, metadata=metadata, + ) + + async def get_default_branch( + self, + request: catalog_service.GetDefaultBranchRequest = None, + *, + catalog: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> catalog_service.GetDefaultBranchResponse: + r"""Get which branch is currently default branch set by + [CatalogService.SetDefaultBranch][google.cloud.retail.v2.CatalogService.SetDefaultBranch] + method under a specified parent catalog. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Args: + request (:class:`google.cloud.retail_v2.types.GetDefaultBranchRequest`): + The request object. Request message to show which branch + is currently the default branch. + catalog (:class:`str`): + The parent catalog resource name, such as + ``projects/*/locations/global/catalogs/default_catalog``. + + This corresponds to the ``catalog`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + 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: + google.cloud.retail_v2.types.GetDefaultBranchResponse: + Response message of + [CatalogService.GetDefaultBranch][google.cloud.retail.v2.CatalogService.GetDefaultBranch]. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([catalog]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + request = catalog_service.GetDefaultBranchRequest(request) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if catalog is not None: + request.catalog = catalog + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method_async.wrap_method( + self._client._transport.get_default_branch, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("catalog", request.catalog),)), + ) + + # Send the request. + response = await rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + try: DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( diff --git a/google/cloud/retail_v2/services/catalog_service/client.py b/google/cloud/retail_v2/services/catalog_service/client.py index e36b6f92..d9a7dbb5 100644 --- a/google/cloud/retail_v2/services/catalog_service/client.py +++ b/google/cloud/retail_v2/services/catalog_service/client.py @@ -35,6 +35,7 @@ from google.cloud.retail_v2.types import catalog as gcr_catalog from google.cloud.retail_v2.types import catalog_service from google.protobuf import field_mask_pb2 # type: ignore +from google.protobuf import timestamp_pb2 # type: ignore from .transports.base import CatalogServiceTransport, DEFAULT_CLIENT_INFO from .transports.grpc import CatalogServiceGrpcTransport from .transports.grpc_asyncio import CatalogServiceGrpcAsyncIOTransport @@ -158,6 +159,22 @@ def transport(self) -> CatalogServiceTransport: """ return self._transport + @staticmethod + def branch_path(project: str, location: str, catalog: str, branch: str,) -> str: + """Returns a fully-qualified branch string.""" + return "projects/{project}/locations/{location}/catalogs/{catalog}/branches/{branch}".format( + project=project, location=location, catalog=catalog, branch=branch, + ) + + @staticmethod + def parse_branch_path(path: str) -> Dict[str, str]: + """Parses a branch path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/catalogs/(?P.+?)/branches/(?P.+?)$", + path, + ) + return m.groupdict() if m else {} + @staticmethod def catalog_path(project: str, location: str, catalog: str,) -> str: """Returns a fully-qualified catalog string.""" @@ -475,11 +492,7 @@ def update_catalog( should not be set. update_mask (google.protobuf.field_mask_pb2.FieldMask): Indicates which fields in the provided - [Catalog][google.cloud.retail.v2.Catalog] to update. If - not set, will only update the - [Catalog.product_level_config][google.cloud.retail.v2.Catalog.product_level_config] - field, which is also the only currently supported field - to update. + [Catalog][google.cloud.retail.v2.Catalog] to update. If an unsupported or unknown field is provided, an INVALID_ARGUMENT error is returned. @@ -538,6 +551,187 @@ def update_catalog( # Done; return the response. return response + def set_default_branch( + self, + request: catalog_service.SetDefaultBranchRequest = None, + *, + catalog: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> None: + r"""Set a specified branch id as default branch. API methods such as + [SearchService.Search][google.cloud.retail.v2.SearchService.Search], + [ProductService.GetProduct][google.cloud.retail.v2.ProductService.GetProduct], + [ProductService.ListProducts][google.cloud.retail.v2.ProductService.ListProducts] + will treat requests using "default_branch" to the actual branch + id set as default. + + For example, if ``projects/*/locations/*/catalogs/*/branches/1`` + is set as default, setting + [SearchRequest.branch][google.cloud.retail.v2.SearchRequest.branch] + to ``projects/*/locations/*/catalogs/*/branches/default_branch`` + is equivalent to setting + [SearchRequest.branch][google.cloud.retail.v2.SearchRequest.branch] + to ``projects/*/locations/*/catalogs/*/branches/1``. + + Using multiple branches can be useful when developers would like + to have a staging branch to test and verify for future usage. + When it becomes ready, developers switch on the staging branch + using this API while keeping using + ``projects/*/locations/*/catalogs/*/branches/default_branch`` as + [SearchRequest.branch][google.cloud.retail.v2.SearchRequest.branch] + to route the traffic to this staging branch. + + CAUTION: If you have live predict/search traffic, switching the + default branch could potentially cause outages if the ID space + of the new branch is very different from the old one. + + More specifically: + + - PredictionService will only return product IDs from branch + {newBranch}. + - SearchService will only return product IDs from branch + {newBranch} (if branch is not explicitly set). + - UserEventService will only join events with products from + branch {newBranch}. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Args: + request (google.cloud.retail_v2.types.SetDefaultBranchRequest): + The request object. Request message to set a specified + branch as new default_branch. + catalog (str): + Full resource name of the catalog, such as + ``projects/*/locations/global/catalogs/default_catalog``. + + This corresponds to the ``catalog`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + 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. + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([catalog]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + # Minor optimization to avoid making a copy if the user passes + # in a catalog_service.SetDefaultBranchRequest. + # There's no risk of modifying the input as we've already verified + # there are no flattened fields. + if not isinstance(request, catalog_service.SetDefaultBranchRequest): + request = catalog_service.SetDefaultBranchRequest(request) + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if catalog is not None: + request.catalog = catalog + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.set_default_branch] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("catalog", request.catalog),)), + ) + + # Send the request. + rpc( + request, retry=retry, timeout=timeout, metadata=metadata, + ) + + def get_default_branch( + self, + request: catalog_service.GetDefaultBranchRequest = None, + *, + catalog: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> catalog_service.GetDefaultBranchResponse: + r"""Get which branch is currently default branch set by + [CatalogService.SetDefaultBranch][google.cloud.retail.v2.CatalogService.SetDefaultBranch] + method under a specified parent catalog. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Args: + request (google.cloud.retail_v2.types.GetDefaultBranchRequest): + The request object. Request message to show which branch + is currently the default branch. + catalog (str): + The parent catalog resource name, such as + ``projects/*/locations/global/catalogs/default_catalog``. + + This corresponds to the ``catalog`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + 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: + google.cloud.retail_v2.types.GetDefaultBranchResponse: + Response message of + [CatalogService.GetDefaultBranch][google.cloud.retail.v2.CatalogService.GetDefaultBranch]. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([catalog]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + # Minor optimization to avoid making a copy if the user passes + # in a catalog_service.GetDefaultBranchRequest. + # There's no risk of modifying the input as we've already verified + # there are no flattened fields. + if not isinstance(request, catalog_service.GetDefaultBranchRequest): + request = catalog_service.GetDefaultBranchRequest(request) + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if catalog is not None: + request.catalog = catalog + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.get_default_branch] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("catalog", request.catalog),)), + ) + + # Send the request. + response = rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + try: DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( diff --git a/google/cloud/retail_v2/services/catalog_service/transports/base.py b/google/cloud/retail_v2/services/catalog_service/transports/base.py index 7826805d..917f370c 100644 --- a/google/cloud/retail_v2/services/catalog_service/transports/base.py +++ b/google/cloud/retail_v2/services/catalog_service/transports/base.py @@ -28,6 +28,7 @@ from google.cloud.retail_v2.types import catalog as gcr_catalog from google.cloud.retail_v2.types import catalog_service +from google.protobuf import empty_pb2 # type: ignore try: DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( @@ -159,6 +160,12 @@ def _prep_wrapped_messages(self, client_info): self.update_catalog: gapic_v1.method.wrap_method( self.update_catalog, default_timeout=None, client_info=client_info, ), + self.set_default_branch: gapic_v1.method.wrap_method( + self.set_default_branch, default_timeout=None, client_info=client_info, + ), + self.get_default_branch: gapic_v1.method.wrap_method( + self.get_default_branch, default_timeout=None, client_info=client_info, + ), } @property @@ -182,5 +189,26 @@ def update_catalog( ]: raise NotImplementedError() + @property + def set_default_branch( + self, + ) -> Callable[ + [catalog_service.SetDefaultBranchRequest], + Union[empty_pb2.Empty, Awaitable[empty_pb2.Empty]], + ]: + raise NotImplementedError() + + @property + def get_default_branch( + self, + ) -> Callable[ + [catalog_service.GetDefaultBranchRequest], + Union[ + catalog_service.GetDefaultBranchResponse, + Awaitable[catalog_service.GetDefaultBranchResponse], + ], + ]: + raise NotImplementedError() + __all__ = ("CatalogServiceTransport",) diff --git a/google/cloud/retail_v2/services/catalog_service/transports/grpc.py b/google/cloud/retail_v2/services/catalog_service/transports/grpc.py index 4b39990c..f4f37675 100644 --- a/google/cloud/retail_v2/services/catalog_service/transports/grpc.py +++ b/google/cloud/retail_v2/services/catalog_service/transports/grpc.py @@ -26,6 +26,7 @@ from google.cloud.retail_v2.types import catalog as gcr_catalog from google.cloud.retail_v2.types import catalog_service +from google.protobuf import empty_pb2 # type: ignore from .base import CatalogServiceTransport, DEFAULT_CLIENT_INFO @@ -281,5 +282,106 @@ def update_catalog( ) return self._stubs["update_catalog"] + @property + def set_default_branch( + self, + ) -> Callable[[catalog_service.SetDefaultBranchRequest], empty_pb2.Empty]: + r"""Return a callable for the set default branch method over gRPC. + + Set a specified branch id as default branch. API methods such as + [SearchService.Search][google.cloud.retail.v2.SearchService.Search], + [ProductService.GetProduct][google.cloud.retail.v2.ProductService.GetProduct], + [ProductService.ListProducts][google.cloud.retail.v2.ProductService.ListProducts] + will treat requests using "default_branch" to the actual branch + id set as default. + + For example, if ``projects/*/locations/*/catalogs/*/branches/1`` + is set as default, setting + [SearchRequest.branch][google.cloud.retail.v2.SearchRequest.branch] + to ``projects/*/locations/*/catalogs/*/branches/default_branch`` + is equivalent to setting + [SearchRequest.branch][google.cloud.retail.v2.SearchRequest.branch] + to ``projects/*/locations/*/catalogs/*/branches/1``. + + Using multiple branches can be useful when developers would like + to have a staging branch to test and verify for future usage. + When it becomes ready, developers switch on the staging branch + using this API while keeping using + ``projects/*/locations/*/catalogs/*/branches/default_branch`` as + [SearchRequest.branch][google.cloud.retail.v2.SearchRequest.branch] + to route the traffic to this staging branch. + + CAUTION: If you have live predict/search traffic, switching the + default branch could potentially cause outages if the ID space + of the new branch is very different from the old one. + + More specifically: + + - PredictionService will only return product IDs from branch + {newBranch}. + - SearchService will only return product IDs from branch + {newBranch} (if branch is not explicitly set). + - UserEventService will only join events with products from + branch {newBranch}. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Returns: + Callable[[~.SetDefaultBranchRequest], + ~.Empty]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "set_default_branch" not in self._stubs: + self._stubs["set_default_branch"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.CatalogService/SetDefaultBranch", + request_serializer=catalog_service.SetDefaultBranchRequest.serialize, + response_deserializer=empty_pb2.Empty.FromString, + ) + return self._stubs["set_default_branch"] + + @property + def get_default_branch( + self, + ) -> Callable[ + [catalog_service.GetDefaultBranchRequest], + catalog_service.GetDefaultBranchResponse, + ]: + r"""Return a callable for the get default branch method over gRPC. + + Get which branch is currently default branch set by + [CatalogService.SetDefaultBranch][google.cloud.retail.v2.CatalogService.SetDefaultBranch] + method under a specified parent catalog. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Returns: + Callable[[~.GetDefaultBranchRequest], + ~.GetDefaultBranchResponse]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "get_default_branch" not in self._stubs: + self._stubs["get_default_branch"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.CatalogService/GetDefaultBranch", + request_serializer=catalog_service.GetDefaultBranchRequest.serialize, + response_deserializer=catalog_service.GetDefaultBranchResponse.deserialize, + ) + return self._stubs["get_default_branch"] + __all__ = ("CatalogServiceGrpcTransport",) diff --git a/google/cloud/retail_v2/services/catalog_service/transports/grpc_asyncio.py b/google/cloud/retail_v2/services/catalog_service/transports/grpc_asyncio.py index e885b5b0..c4661cfe 100644 --- a/google/cloud/retail_v2/services/catalog_service/transports/grpc_asyncio.py +++ b/google/cloud/retail_v2/services/catalog_service/transports/grpc_asyncio.py @@ -27,6 +27,7 @@ from google.cloud.retail_v2.types import catalog as gcr_catalog from google.cloud.retail_v2.types import catalog_service +from google.protobuf import empty_pb2 # type: ignore from .base import CatalogServiceTransport, DEFAULT_CLIENT_INFO from .grpc import CatalogServiceGrpcTransport @@ -287,5 +288,108 @@ def update_catalog( ) return self._stubs["update_catalog"] + @property + def set_default_branch( + self, + ) -> Callable[ + [catalog_service.SetDefaultBranchRequest], Awaitable[empty_pb2.Empty] + ]: + r"""Return a callable for the set default branch method over gRPC. + + Set a specified branch id as default branch. API methods such as + [SearchService.Search][google.cloud.retail.v2.SearchService.Search], + [ProductService.GetProduct][google.cloud.retail.v2.ProductService.GetProduct], + [ProductService.ListProducts][google.cloud.retail.v2.ProductService.ListProducts] + will treat requests using "default_branch" to the actual branch + id set as default. + + For example, if ``projects/*/locations/*/catalogs/*/branches/1`` + is set as default, setting + [SearchRequest.branch][google.cloud.retail.v2.SearchRequest.branch] + to ``projects/*/locations/*/catalogs/*/branches/default_branch`` + is equivalent to setting + [SearchRequest.branch][google.cloud.retail.v2.SearchRequest.branch] + to ``projects/*/locations/*/catalogs/*/branches/1``. + + Using multiple branches can be useful when developers would like + to have a staging branch to test and verify for future usage. + When it becomes ready, developers switch on the staging branch + using this API while keeping using + ``projects/*/locations/*/catalogs/*/branches/default_branch`` as + [SearchRequest.branch][google.cloud.retail.v2.SearchRequest.branch] + to route the traffic to this staging branch. + + CAUTION: If you have live predict/search traffic, switching the + default branch could potentially cause outages if the ID space + of the new branch is very different from the old one. + + More specifically: + + - PredictionService will only return product IDs from branch + {newBranch}. + - SearchService will only return product IDs from branch + {newBranch} (if branch is not explicitly set). + - UserEventService will only join events with products from + branch {newBranch}. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Returns: + Callable[[~.SetDefaultBranchRequest], + Awaitable[~.Empty]]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "set_default_branch" not in self._stubs: + self._stubs["set_default_branch"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.CatalogService/SetDefaultBranch", + request_serializer=catalog_service.SetDefaultBranchRequest.serialize, + response_deserializer=empty_pb2.Empty.FromString, + ) + return self._stubs["set_default_branch"] + + @property + def get_default_branch( + self, + ) -> Callable[ + [catalog_service.GetDefaultBranchRequest], + Awaitable[catalog_service.GetDefaultBranchResponse], + ]: + r"""Return a callable for the get default branch method over gRPC. + + Get which branch is currently default branch set by + [CatalogService.SetDefaultBranch][google.cloud.retail.v2.CatalogService.SetDefaultBranch] + method under a specified parent catalog. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Returns: + Callable[[~.GetDefaultBranchRequest], + Awaitable[~.GetDefaultBranchResponse]]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "get_default_branch" not in self._stubs: + self._stubs["get_default_branch"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.CatalogService/GetDefaultBranch", + request_serializer=catalog_service.GetDefaultBranchRequest.serialize, + response_deserializer=catalog_service.GetDefaultBranchResponse.deserialize, + ) + return self._stubs["get_default_branch"] + __all__ = ("CatalogServiceGrpcAsyncIOTransport",) diff --git a/google/cloud/retail_v2/services/completion_service/__init__.py b/google/cloud/retail_v2/services/completion_service/__init__.py new file mode 100644 index 00000000..c23cb6fb --- /dev/null +++ b/google/cloud/retail_v2/services/completion_service/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from .client import CompletionServiceClient +from .async_client import CompletionServiceAsyncClient + +__all__ = ( + "CompletionServiceClient", + "CompletionServiceAsyncClient", +) diff --git a/google/cloud/retail_v2/services/completion_service/async_client.py b/google/cloud/retail_v2/services/completion_service/async_client.py new file mode 100644 index 00000000..669460f9 --- /dev/null +++ b/google/cloud/retail_v2/services/completion_service/async_client.py @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from collections import OrderedDict +import functools +import re +from typing import Dict, Sequence, Tuple, Type, Union +import pkg_resources + +import google.api_core.client_options as ClientOptions # type: ignore +from google.api_core import exceptions as core_exceptions # type: ignore +from google.api_core import gapic_v1 # type: ignore +from google.api_core import retry as retries # type: ignore +from google.auth import credentials as ga_credentials # type: ignore +from google.oauth2 import service_account # type: ignore + +from google.api_core import operation # type: ignore +from google.api_core import operation_async # type: ignore +from google.cloud.retail_v2.types import completion_service +from google.cloud.retail_v2.types import import_config +from .transports.base import CompletionServiceTransport, DEFAULT_CLIENT_INFO +from .transports.grpc_asyncio import CompletionServiceGrpcAsyncIOTransport +from .client import CompletionServiceClient + + +class CompletionServiceAsyncClient: + """Auto-completion service for retail. + This feature is only available for users who have Retail Search + enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using Retail + Search. + """ + + _client: CompletionServiceClient + + DEFAULT_ENDPOINT = CompletionServiceClient.DEFAULT_ENDPOINT + DEFAULT_MTLS_ENDPOINT = CompletionServiceClient.DEFAULT_MTLS_ENDPOINT + + catalog_path = staticmethod(CompletionServiceClient.catalog_path) + parse_catalog_path = staticmethod(CompletionServiceClient.parse_catalog_path) + common_billing_account_path = staticmethod( + CompletionServiceClient.common_billing_account_path + ) + parse_common_billing_account_path = staticmethod( + CompletionServiceClient.parse_common_billing_account_path + ) + common_folder_path = staticmethod(CompletionServiceClient.common_folder_path) + parse_common_folder_path = staticmethod( + CompletionServiceClient.parse_common_folder_path + ) + common_organization_path = staticmethod( + CompletionServiceClient.common_organization_path + ) + parse_common_organization_path = staticmethod( + CompletionServiceClient.parse_common_organization_path + ) + common_project_path = staticmethod(CompletionServiceClient.common_project_path) + parse_common_project_path = staticmethod( + CompletionServiceClient.parse_common_project_path + ) + common_location_path = staticmethod(CompletionServiceClient.common_location_path) + parse_common_location_path = staticmethod( + CompletionServiceClient.parse_common_location_path + ) + + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials + info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + CompletionServiceAsyncClient: The constructed client. + """ + return CompletionServiceClient.from_service_account_info.__func__(CompletionServiceAsyncClient, info, *args, **kwargs) # type: ignore + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """Creates an instance of this client using the provided credentials + file. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + CompletionServiceAsyncClient: The constructed client. + """ + return CompletionServiceClient.from_service_account_file.__func__(CompletionServiceAsyncClient, filename, *args, **kwargs) # type: ignore + + from_service_account_json = from_service_account_file + + @property + def transport(self) -> CompletionServiceTransport: + """Returns the transport used by the client instance. + + Returns: + CompletionServiceTransport: The transport used by the client instance. + """ + return self._client.transport + + get_transport_class = functools.partial( + type(CompletionServiceClient).get_transport_class, type(CompletionServiceClient) + ) + + def __init__( + self, + *, + credentials: ga_credentials.Credentials = None, + transport: Union[str, CompletionServiceTransport] = "grpc_asyncio", + client_options: ClientOptions = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + ) -> None: + """Instantiates the completion service client. + + Args: + 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. + transport (Union[str, ~.CompletionServiceTransport]): The + transport to use. If set to None, a transport is chosen + automatically. + client_options (ClientOptions): Custom options for the client. It + won't take effect if a ``transport`` instance is provided. + (1) The ``api_endpoint`` property can be used to override the + default endpoint provided by the client. GOOGLE_API_USE_MTLS_ENDPOINT + environment variable can also be used to override the endpoint: + "always" (always use the default mTLS endpoint), "never" (always + use the default regular endpoint) and "auto" (auto switch to the + default mTLS endpoint if client certificate is present, this is + the default value). However, the ``api_endpoint`` property takes + precedence if provided. + (2) If GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable + is "true", then the ``client_cert_source`` property can be used + to provide client certificate for mutual TLS transport. If + not provided, the default SSL client certificate will be used if + present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not + set, no client certificate will be used. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. + """ + self._client = CompletionServiceClient( + credentials=credentials, + transport=transport, + client_options=client_options, + client_info=client_info, + ) + + async def complete_query( + self, + request: completion_service.CompleteQueryRequest = None, + *, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> completion_service.CompleteQueryResponse: + r"""Completes the specified prefix with keyword + suggestions. + This feature is only available for users who have Retail + Search enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using + Retail Search. + + Args: + request (:class:`google.cloud.retail_v2.types.CompleteQueryRequest`): + The request object. Auto-complete parameters. + 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: + google.cloud.retail_v2.types.CompleteQueryResponse: + Response of the auto-complete query. + """ + # Create or coerce a protobuf request object. + request = completion_service.CompleteQueryRequest(request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method_async.wrap_method( + self._client._transport.complete_query, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("catalog", request.catalog),)), + ) + + # Send the request. + response = await rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + + async def import_completion_data( + self, + request: import_config.ImportCompletionDataRequest = None, + *, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operation_async.AsyncOperation: + r"""Bulk import of processed completion dataset. + Request processing may be synchronous. Partial updating + is not supported. + This feature is only available for users who have Retail + Search enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using + Retail Search. + + Args: + request (:class:`google.cloud.retail_v2.types.ImportCompletionDataRequest`): + The request object. Request message for + ImportCompletionData methods. + 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: + google.api_core.operation_async.AsyncOperation: + An object representing a long-running operation. + + The result type for the operation will be :class:`google.cloud.retail_v2.types.ImportCompletionDataResponse` Response of the + [ImportCompletionDataRequest][google.cloud.retail.v2.ImportCompletionDataRequest]. + If the long running operation is done, this message + is returned by the + google.longrunning.Operations.response field if the + operation is successful. + + """ + # Create or coerce a protobuf request object. + request = import_config.ImportCompletionDataRequest(request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method_async.wrap_method( + self._client._transport.import_completion_data, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("parent", request.parent),)), + ) + + # Send the request. + response = await rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Wrap the response in an operation future. + response = operation_async.from_gapic( + response, + self._client._transport.operations_client, + import_config.ImportCompletionDataResponse, + metadata_type=import_config.ImportMetadata, + ) + + # Done; return the response. + return response + + +try: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=pkg_resources.get_distribution("google-cloud-retail",).version, + ) +except pkg_resources.DistributionNotFound: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() + + +__all__ = ("CompletionServiceAsyncClient",) diff --git a/google/cloud/retail_v2/services/completion_service/client.py b/google/cloud/retail_v2/services/completion_service/client.py new file mode 100644 index 00000000..80dcbd85 --- /dev/null +++ b/google/cloud/retail_v2/services/completion_service/client.py @@ -0,0 +1,493 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from collections import OrderedDict +from distutils import util +import os +import re +from typing import Callable, Dict, Optional, Sequence, Tuple, Type, Union +import pkg_resources + +from google.api_core import client_options as client_options_lib # type: ignore +from google.api_core import exceptions as core_exceptions # type: ignore +from google.api_core import gapic_v1 # type: ignore +from google.api_core import retry as retries # type: ignore +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport import mtls # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.auth.exceptions import MutualTLSChannelError # type: ignore +from google.oauth2 import service_account # type: ignore + +from google.api_core import operation # type: ignore +from google.api_core import operation_async # type: ignore +from google.cloud.retail_v2.types import completion_service +from google.cloud.retail_v2.types import import_config +from .transports.base import CompletionServiceTransport, DEFAULT_CLIENT_INFO +from .transports.grpc import CompletionServiceGrpcTransport +from .transports.grpc_asyncio import CompletionServiceGrpcAsyncIOTransport + + +class CompletionServiceClientMeta(type): + """Metaclass for the CompletionService client. + + This provides class-level methods for building and retrieving + support objects (e.g. transport) without polluting the client instance + objects. + """ + + _transport_registry = ( + OrderedDict() + ) # type: Dict[str, Type[CompletionServiceTransport]] + _transport_registry["grpc"] = CompletionServiceGrpcTransport + _transport_registry["grpc_asyncio"] = CompletionServiceGrpcAsyncIOTransport + + def get_transport_class( + cls, label: str = None, + ) -> Type[CompletionServiceTransport]: + """Returns an appropriate transport class. + + Args: + label: The name of the desired transport. If none is + provided, then the first transport in the registry is used. + + Returns: + The transport class to use. + """ + # If a specific transport is requested, return that one. + if label: + return cls._transport_registry[label] + + # No transport is requested; return the default (that is, the first one + # in the dictionary). + return next(iter(cls._transport_registry.values())) + + +class CompletionServiceClient(metaclass=CompletionServiceClientMeta): + """Auto-completion service for retail. + This feature is only available for users who have Retail Search + enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using Retail + Search. + """ + + @staticmethod + def _get_default_mtls_endpoint(api_endpoint): + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + str: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + DEFAULT_ENDPOINT = "retail.googleapis.com" + DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_ENDPOINT + ) + + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials + info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + CompletionServiceClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_info(info) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """Creates an instance of this client using the provided credentials + file. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + CompletionServiceClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_file(filename) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + + from_service_account_json = from_service_account_file + + @property + def transport(self) -> CompletionServiceTransport: + """Returns the transport used by the client instance. + + Returns: + CompletionServiceTransport: The transport used by the client + instance. + """ + return self._transport + + @staticmethod + def catalog_path(project: str, location: str, catalog: str,) -> str: + """Returns a fully-qualified catalog string.""" + return "projects/{project}/locations/{location}/catalogs/{catalog}".format( + project=project, location=location, catalog=catalog, + ) + + @staticmethod + def parse_catalog_path(path: str) -> Dict[str, str]: + """Parses a catalog path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/catalogs/(?P.+?)$", + path, + ) + return m.groupdict() if m else {} + + @staticmethod + def common_billing_account_path(billing_account: str,) -> str: + """Returns a fully-qualified billing_account string.""" + return "billingAccounts/{billing_account}".format( + billing_account=billing_account, + ) + + @staticmethod + def parse_common_billing_account_path(path: str) -> Dict[str, str]: + """Parse a billing_account path into its component segments.""" + m = re.match(r"^billingAccounts/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_folder_path(folder: str,) -> str: + """Returns a fully-qualified folder string.""" + return "folders/{folder}".format(folder=folder,) + + @staticmethod + def parse_common_folder_path(path: str) -> Dict[str, str]: + """Parse a folder path into its component segments.""" + m = re.match(r"^folders/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_organization_path(organization: str,) -> str: + """Returns a fully-qualified organization string.""" + return "organizations/{organization}".format(organization=organization,) + + @staticmethod + def parse_common_organization_path(path: str) -> Dict[str, str]: + """Parse a organization path into its component segments.""" + m = re.match(r"^organizations/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_project_path(project: str,) -> str: + """Returns a fully-qualified project string.""" + return "projects/{project}".format(project=project,) + + @staticmethod + def parse_common_project_path(path: str) -> Dict[str, str]: + """Parse a project path into its component segments.""" + m = re.match(r"^projects/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_location_path(project: str, location: str,) -> str: + """Returns a fully-qualified location string.""" + return "projects/{project}/locations/{location}".format( + project=project, location=location, + ) + + @staticmethod + def parse_common_location_path(path: str) -> Dict[str, str]: + """Parse a location path into its component segments.""" + m = re.match(r"^projects/(?P.+?)/locations/(?P.+?)$", path) + return m.groupdict() if m else {} + + def __init__( + self, + *, + credentials: Optional[ga_credentials.Credentials] = None, + transport: Union[str, CompletionServiceTransport, None] = None, + client_options: Optional[client_options_lib.ClientOptions] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + ) -> None: + """Instantiates the completion service client. + + Args: + 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. + transport (Union[str, CompletionServiceTransport]): The + transport to use. If set to None, a transport is chosen + automatically. + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. It won't take effect if a ``transport`` instance is provided. + (1) The ``api_endpoint`` property can be used to override the + default endpoint provided by the client. GOOGLE_API_USE_MTLS_ENDPOINT + environment variable can also be used to override the endpoint: + "always" (always use the default mTLS endpoint), "never" (always + use the default regular endpoint) and "auto" (auto switch to the + default mTLS endpoint if client certificate is present, this is + the default value). However, the ``api_endpoint`` property takes + precedence if provided. + (2) If GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable + is "true", then the ``client_cert_source`` property can be used + to provide client certificate for mutual TLS transport. If + not provided, the default SSL client certificate will be used if + present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not + set, no client certificate will be used. + 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're developing + your own client library. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport + creation failed for any reason. + """ + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + if client_options is None: + client_options = client_options_lib.ClientOptions() + + # Create SSL credentials for mutual TLS if needed. + use_client_cert = bool( + util.strtobool(os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false")) + ) + + client_cert_source_func = None + is_mtls = False + if use_client_cert: + if client_options.client_cert_source: + is_mtls = True + client_cert_source_func = client_options.client_cert_source + else: + is_mtls = mtls.has_default_client_cert_source() + if is_mtls: + client_cert_source_func = mtls.default_client_cert_source() + else: + client_cert_source_func = None + + # Figure out which api endpoint to use. + if client_options.api_endpoint is not None: + api_endpoint = client_options.api_endpoint + else: + use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") + if use_mtls_env == "never": + api_endpoint = self.DEFAULT_ENDPOINT + elif use_mtls_env == "always": + api_endpoint = self.DEFAULT_MTLS_ENDPOINT + elif use_mtls_env == "auto": + if is_mtls: + api_endpoint = self.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = self.DEFAULT_ENDPOINT + else: + raise MutualTLSChannelError( + "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted " + "values: never, auto, always" + ) + + # Save or instantiate the transport. + # Ordinarily, we provide the transport, but allowing a custom transport + # instance provides an extensibility point for unusual situations. + if isinstance(transport, CompletionServiceTransport): + # transport is a CompletionServiceTransport instance. + if credentials or client_options.credentials_file: + raise ValueError( + "When providing a transport instance, " + "provide its credentials directly." + ) + if client_options.scopes: + raise ValueError( + "When providing a transport instance, provide its scopes " + "directly." + ) + self._transport = transport + else: + Transport = type(self).get_transport_class(transport) + self._transport = Transport( + credentials=credentials, + credentials_file=client_options.credentials_file, + host=api_endpoint, + scopes=client_options.scopes, + client_cert_source_for_mtls=client_cert_source_func, + quota_project_id=client_options.quota_project_id, + client_info=client_info, + always_use_jwt_access=( + Transport == type(self).get_transport_class("grpc") + or Transport == type(self).get_transport_class("grpc_asyncio") + ), + ) + + def complete_query( + self, + request: completion_service.CompleteQueryRequest = None, + *, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> completion_service.CompleteQueryResponse: + r"""Completes the specified prefix with keyword + suggestions. + This feature is only available for users who have Retail + Search enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using + Retail Search. + + Args: + request (google.cloud.retail_v2.types.CompleteQueryRequest): + The request object. Auto-complete parameters. + 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: + google.cloud.retail_v2.types.CompleteQueryResponse: + Response of the auto-complete query. + """ + # Create or coerce a protobuf request object. + # Minor optimization to avoid making a copy if the user passes + # in a completion_service.CompleteQueryRequest. + # There's no risk of modifying the input as we've already verified + # there are no flattened fields. + if not isinstance(request, completion_service.CompleteQueryRequest): + request = completion_service.CompleteQueryRequest(request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.complete_query] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("catalog", request.catalog),)), + ) + + # Send the request. + response = rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + + def import_completion_data( + self, + request: import_config.ImportCompletionDataRequest = None, + *, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operation.Operation: + r"""Bulk import of processed completion dataset. + Request processing may be synchronous. Partial updating + is not supported. + This feature is only available for users who have Retail + Search enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using + Retail Search. + + Args: + request (google.cloud.retail_v2.types.ImportCompletionDataRequest): + The request object. Request message for + ImportCompletionData methods. + 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: + google.api_core.operation.Operation: + An object representing a long-running operation. + + The result type for the operation will be :class:`google.cloud.retail_v2.types.ImportCompletionDataResponse` Response of the + [ImportCompletionDataRequest][google.cloud.retail.v2.ImportCompletionDataRequest]. + If the long running operation is done, this message + is returned by the + google.longrunning.Operations.response field if the + operation is successful. + + """ + # Create or coerce a protobuf request object. + # Minor optimization to avoid making a copy if the user passes + # in a import_config.ImportCompletionDataRequest. + # There's no risk of modifying the input as we've already verified + # there are no flattened fields. + if not isinstance(request, import_config.ImportCompletionDataRequest): + request = import_config.ImportCompletionDataRequest(request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.import_completion_data] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("parent", request.parent),)), + ) + + # Send the request. + response = rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Wrap the response in an operation future. + response = operation.from_gapic( + response, + self._transport.operations_client, + import_config.ImportCompletionDataResponse, + metadata_type=import_config.ImportMetadata, + ) + + # Done; return the response. + return response + + +try: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=pkg_resources.get_distribution("google-cloud-retail",).version, + ) +except pkg_resources.DistributionNotFound: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() + + +__all__ = ("CompletionServiceClient",) diff --git a/google/cloud/retail_v2/services/completion_service/transports/__init__.py b/google/cloud/retail_v2/services/completion_service/transports/__init__.py new file mode 100644 index 00000000..450444dd --- /dev/null +++ b/google/cloud/retail_v2/services/completion_service/transports/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from collections import OrderedDict +from typing import Dict, Type + +from .base import CompletionServiceTransport +from .grpc import CompletionServiceGrpcTransport +from .grpc_asyncio import CompletionServiceGrpcAsyncIOTransport + + +# Compile a registry of transports. +_transport_registry = OrderedDict() # type: Dict[str, Type[CompletionServiceTransport]] +_transport_registry["grpc"] = CompletionServiceGrpcTransport +_transport_registry["grpc_asyncio"] = CompletionServiceGrpcAsyncIOTransport + +__all__ = ( + "CompletionServiceTransport", + "CompletionServiceGrpcTransport", + "CompletionServiceGrpcAsyncIOTransport", +) diff --git a/google/cloud/retail_v2/services/completion_service/transports/base.py b/google/cloud/retail_v2/services/completion_service/transports/base.py new file mode 100644 index 00000000..a5953551 --- /dev/null +++ b/google/cloud/retail_v2/services/completion_service/transports/base.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import abc +from typing import Awaitable, Callable, Dict, Optional, Sequence, Union +import packaging.version +import pkg_resources + +import google.auth # type: ignore +import google.api_core # type: ignore +from google.api_core import exceptions as core_exceptions # type: ignore +from google.api_core import gapic_v1 # type: ignore +from google.api_core import retry as retries # type: ignore +from google.api_core import operations_v1 # type: ignore +from google.auth import credentials as ga_credentials # type: ignore +from google.oauth2 import service_account # type: ignore + +from google.cloud.retail_v2.types import completion_service +from google.cloud.retail_v2.types import import_config +from google.longrunning import operations_pb2 # type: ignore + +try: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=pkg_resources.get_distribution("google-cloud-retail",).version, + ) +except pkg_resources.DistributionNotFound: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() + +try: + # google.auth.__version__ was added in 1.26.0 + _GOOGLE_AUTH_VERSION = google.auth.__version__ +except AttributeError: + try: # try pkg_resources if it is available + _GOOGLE_AUTH_VERSION = pkg_resources.get_distribution("google-auth").version + except pkg_resources.DistributionNotFound: # pragma: NO COVER + _GOOGLE_AUTH_VERSION = None + + +class CompletionServiceTransport(abc.ABC): + """Abstract transport class for CompletionService.""" + + AUTH_SCOPES = ("https://www.googleapis.com/auth/cloud-platform",) + + DEFAULT_HOST: str = "retail.googleapis.com" + + def __init__( + self, + *, + host: str = DEFAULT_HOST, + credentials: ga_credentials.Credentials = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + **kwargs, + ) -> 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 mutually exclusive with credentials. + scopes (Optional[Sequence[str]]): A list of scopes. + 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're developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + """ + # Save the hostname. Default to port 443 (HTTPS) if none is specified. + if ":" not in host: + host += ":443" + self._host = host + + scopes_kwargs = self._get_scopes_kwargs(self._host, scopes) + + # Save the scopes. + self._scopes = scopes + + # If no credentials are provided, then determine the appropriate + # defaults. + if credentials and credentials_file: + raise core_exceptions.DuplicateCredentialArgs( + "'credentials_file' and 'credentials' are mutually exclusive" + ) + + if credentials_file is not None: + credentials, _ = google.auth.load_credentials_from_file( + credentials_file, **scopes_kwargs, quota_project_id=quota_project_id + ) + + elif credentials is None: + credentials, _ = google.auth.default( + **scopes_kwargs, quota_project_id=quota_project_id + ) + + # If the credentials is service account credentials, then always try to use self signed JWT. + if ( + always_use_jwt_access + and isinstance(credentials, service_account.Credentials) + and hasattr(service_account.Credentials, "with_always_use_jwt_access") + ): + credentials = credentials.with_always_use_jwt_access(True) + + # Save the credentials. + self._credentials = credentials + + # TODO(busunkim): This method is in the base transport + # to avoid duplicating code across the transport classes. These functions + # should be deleted once the minimum required versions of google-auth is increased. + + # TODO: Remove this function once google-auth >= 1.25.0 is required + @classmethod + def _get_scopes_kwargs( + cls, host: str, scopes: Optional[Sequence[str]] + ) -> Dict[str, Optional[Sequence[str]]]: + """Returns scopes kwargs to pass to google-auth methods depending on the google-auth version""" + + scopes_kwargs = {} + + if _GOOGLE_AUTH_VERSION and ( + packaging.version.parse(_GOOGLE_AUTH_VERSION) + >= packaging.version.parse("1.25.0") + ): + scopes_kwargs = {"scopes": scopes, "default_scopes": cls.AUTH_SCOPES} + else: + scopes_kwargs = {"scopes": scopes or cls.AUTH_SCOPES} + + return scopes_kwargs + + def _prep_wrapped_messages(self, client_info): + # Precompute the wrapped methods. + self._wrapped_methods = { + self.complete_query: gapic_v1.method.wrap_method( + self.complete_query, default_timeout=None, client_info=client_info, + ), + self.import_completion_data: gapic_v1.method.wrap_method( + self.import_completion_data, + default_timeout=None, + client_info=client_info, + ), + } + + @property + def operations_client(self) -> operations_v1.OperationsClient: + """Return the client designed to process long-running operations.""" + raise NotImplementedError() + + @property + def complete_query( + self, + ) -> Callable[ + [completion_service.CompleteQueryRequest], + Union[ + completion_service.CompleteQueryResponse, + Awaitable[completion_service.CompleteQueryResponse], + ], + ]: + raise NotImplementedError() + + @property + def import_completion_data( + self, + ) -> Callable[ + [import_config.ImportCompletionDataRequest], + Union[operations_pb2.Operation, Awaitable[operations_pb2.Operation]], + ]: + raise NotImplementedError() + + +__all__ = ("CompletionServiceTransport",) diff --git a/google/cloud/retail_v2/services/completion_service/transports/grpc.py b/google/cloud/retail_v2/services/completion_service/transports/grpc.py new file mode 100644 index 00000000..9b87675b --- /dev/null +++ b/google/cloud/retail_v2/services/completion_service/transports/grpc.py @@ -0,0 +1,319 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import warnings +from typing import Callable, Dict, Optional, Sequence, Tuple, Union + +from google.api_core import grpc_helpers # type: ignore +from google.api_core import operations_v1 # type: ignore +from google.api_core import gapic_v1 # type: ignore +import google.auth # type: ignore +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore + +import grpc # type: ignore + +from google.cloud.retail_v2.types import completion_service +from google.cloud.retail_v2.types import import_config +from google.longrunning import operations_pb2 # type: ignore +from .base import CompletionServiceTransport, DEFAULT_CLIENT_INFO + + +class CompletionServiceGrpcTransport(CompletionServiceTransport): + """gRPC backend transport for CompletionService. + + Auto-completion service for retail. + This feature is only available for users who have Retail Search + enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using Retail + Search. + + 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 protocol buffers over the wire using gRPC (which is built on + top of HTTP/2); the ``grpcio`` package must be installed. + """ + + _stubs: Dict[str, Callable] + + def __init__( + self, + *, + host: str = "retail.googleapis.com", + credentials: ga_credentials.Credentials = None, + credentials_file: str = None, + scopes: Sequence[str] = None, + channel: grpc.Channel = None, + api_mtls_endpoint: str = None, + client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, + ssl_channel_credentials: grpc.ChannelCredentials = None, + client_cert_source_for_mtls: 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, + ) -> 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. + This argument is ignored if ``channel`` is provided. + 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. + channel (Optional[grpc.Channel]): A ``Channel`` instance through + which to make calls. + api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint. + If provided, it overrides the ``host`` argument and tries to create + a mutual TLS channel with client SSL credentials from + ``client_cert_source`` or applicatin default SSL credentials. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): + Deprecated. A callback to provide client SSL certificate bytes and + private key bytes, both in PEM format. It is ignored if + ``api_mtls_endpoint`` is None. + ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials + for grpc channel. It is ignored if ``channel`` is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure mutual TLS channel. It is + ignored if ``channel`` or ``ssl_channel_credentials`` 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're developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport + creation failed for any reason. + google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` + and ``credentials_file`` are passed. + """ + self._grpc_channel = None + self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + self._operations_client = None + + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn("client_cert_source is deprecated", DeprecationWarning) + + if channel: + # Ignore credentials if a channel was passed. + credentials = False + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + self._ssl_channel_credentials = None + + else: + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + self._ssl_channel_credentials = SslCredentials().ssl_credentials + + else: + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + ) + + if not self._grpc_channel: + self._grpc_channel = type(self).create_channel( + self._host, + credentials=self._credentials, + credentials_file=credentials_file, + scopes=self._scopes, + ssl_credentials=self._ssl_channel_credentials, + quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + # Wrap messages. This must be done after self._grpc_channel exists + self._prep_wrapped_messages(client_info) + + @classmethod + def create_channel( + cls, + host: str = "retail.googleapis.com", + credentials: ga_credentials.Credentials = None, + credentials_file: str = None, + scopes: Optional[Sequence[str]] = None, + quota_project_id: Optional[str] = None, + **kwargs, + ) -> grpc.Channel: + """Create and return a gRPC channel object. + Args: + host (Optional[str]): The host for the channel to use. + credentials (Optional[~.Credentials]): The + authorization credentials to attach to requests. These + credentials identify this application to the service. If + none are specified, the client will attempt to ascertain + the credentials from the environment. + credentials_file (Optional[str]): A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is mutually exclusive with credentials. + scopes (Optional[Sequence[str]]): A optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + kwargs (Optional[dict]): Keyword arguments, which are passed to the + channel creation. + Returns: + grpc.Channel: A gRPC channel object. + + Raises: + google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` + and ``credentials_file`` are passed. + """ + + return grpc_helpers.create_channel( + host, + credentials=credentials, + credentials_file=credentials_file, + quota_project_id=quota_project_id, + default_scopes=cls.AUTH_SCOPES, + scopes=scopes, + default_host=cls.DEFAULT_HOST, + **kwargs, + ) + + @property + def grpc_channel(self) -> grpc.Channel: + """Return the channel designed to connect to this service. + """ + return self._grpc_channel + + @property + def operations_client(self) -> operations_v1.OperationsClient: + """Create the client designed to process long-running operations. + + This property caches on the instance; repeated calls return the same + client. + """ + # Sanity check: Only create a new client if we do not already have one. + if self._operations_client is None: + self._operations_client = operations_v1.OperationsClient(self.grpc_channel) + + # Return the client from cache. + return self._operations_client + + @property + def complete_query( + self, + ) -> Callable[ + [completion_service.CompleteQueryRequest], + completion_service.CompleteQueryResponse, + ]: + r"""Return a callable for the complete query method over gRPC. + + Completes the specified prefix with keyword + suggestions. + This feature is only available for users who have Retail + Search enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using + Retail Search. + + Returns: + Callable[[~.CompleteQueryRequest], + ~.CompleteQueryResponse]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "complete_query" not in self._stubs: + self._stubs["complete_query"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.CompletionService/CompleteQuery", + request_serializer=completion_service.CompleteQueryRequest.serialize, + response_deserializer=completion_service.CompleteQueryResponse.deserialize, + ) + return self._stubs["complete_query"] + + @property + def import_completion_data( + self, + ) -> Callable[ + [import_config.ImportCompletionDataRequest], operations_pb2.Operation + ]: + r"""Return a callable for the import completion data method over gRPC. + + Bulk import of processed completion dataset. + Request processing may be synchronous. Partial updating + is not supported. + This feature is only available for users who have Retail + Search enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using + Retail Search. + + Returns: + Callable[[~.ImportCompletionDataRequest], + ~.Operation]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "import_completion_data" not in self._stubs: + self._stubs["import_completion_data"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.CompletionService/ImportCompletionData", + request_serializer=import_config.ImportCompletionDataRequest.serialize, + response_deserializer=operations_pb2.Operation.FromString, + ) + return self._stubs["import_completion_data"] + + +__all__ = ("CompletionServiceGrpcTransport",) diff --git a/google/cloud/retail_v2/services/completion_service/transports/grpc_asyncio.py b/google/cloud/retail_v2/services/completion_service/transports/grpc_asyncio.py new file mode 100644 index 00000000..768f8d2a --- /dev/null +++ b/google/cloud/retail_v2/services/completion_service/transports/grpc_asyncio.py @@ -0,0 +1,324 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import warnings +from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union + +from google.api_core import gapic_v1 # type: ignore +from google.api_core import grpc_helpers_async # type: ignore +from google.api_core import operations_v1 # type: ignore +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +import packaging.version + +import grpc # type: ignore +from grpc.experimental import aio # type: ignore + +from google.cloud.retail_v2.types import completion_service +from google.cloud.retail_v2.types import import_config +from google.longrunning import operations_pb2 # type: ignore +from .base import CompletionServiceTransport, DEFAULT_CLIENT_INFO +from .grpc import CompletionServiceGrpcTransport + + +class CompletionServiceGrpcAsyncIOTransport(CompletionServiceTransport): + """gRPC AsyncIO backend transport for CompletionService. + + Auto-completion service for retail. + This feature is only available for users who have Retail Search + enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using Retail + Search. + + 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 protocol buffers over the wire using gRPC (which is built on + top of HTTP/2); the ``grpcio`` package must be installed. + """ + + _grpc_channel: aio.Channel + _stubs: Dict[str, Callable] = {} + + @classmethod + def create_channel( + cls, + host: str = "retail.googleapis.com", + credentials: ga_credentials.Credentials = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + quota_project_id: Optional[str] = None, + **kwargs, + ) -> aio.Channel: + """Create and return a gRPC AsyncIO channel object. + Args: + host (Optional[str]): The host for the channel to use. + credentials (Optional[~.Credentials]): The + authorization credentials to attach to requests. These + credentials identify this application to the service. If + none are specified, the client will attempt to ascertain + the credentials from the environment. + credentials_file (Optional[str]): A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. + scopes (Optional[Sequence[str]]): A optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + kwargs (Optional[dict]): Keyword arguments, which are passed to the + channel creation. + Returns: + aio.Channel: A gRPC AsyncIO channel object. + """ + + return grpc_helpers_async.create_channel( + host, + credentials=credentials, + credentials_file=credentials_file, + quota_project_id=quota_project_id, + default_scopes=cls.AUTH_SCOPES, + scopes=scopes, + default_host=cls.DEFAULT_HOST, + **kwargs, + ) + + def __init__( + self, + *, + host: str = "retail.googleapis.com", + credentials: ga_credentials.Credentials = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + channel: aio.Channel = None, + api_mtls_endpoint: str = None, + client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, + ssl_channel_credentials: grpc.ChannelCredentials = None, + client_cert_source_for_mtls: Callable[[], Tuple[bytes, bytes]] = None, + quota_project_id=None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + ) -> 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. + This argument is ignored if ``channel`` is provided. + 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 optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. + channel (Optional[aio.Channel]): A ``Channel`` instance through + which to make calls. + api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint. + If provided, it overrides the ``host`` argument and tries to create + a mutual TLS channel with client SSL credentials from + ``client_cert_source`` or applicatin default SSL credentials. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): + Deprecated. A callback to provide client SSL certificate bytes and + private key bytes, both in PEM format. It is ignored if + ``api_mtls_endpoint`` is None. + ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials + for grpc channel. It is ignored if ``channel`` is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure mutual TLS channel. It is + ignored if ``channel`` or ``ssl_channel_credentials`` 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're developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. + google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` + and ``credentials_file`` are passed. + """ + self._grpc_channel = None + self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + self._operations_client = None + + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn("client_cert_source is deprecated", DeprecationWarning) + + if channel: + # Ignore credentials if a channel was passed. + credentials = False + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + self._ssl_channel_credentials = None + else: + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + self._ssl_channel_credentials = SslCredentials().ssl_credentials + + else: + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + ) + + if not self._grpc_channel: + self._grpc_channel = type(self).create_channel( + self._host, + credentials=self._credentials, + credentials_file=credentials_file, + scopes=self._scopes, + ssl_credentials=self._ssl_channel_credentials, + quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + # Wrap messages. This must be done after self._grpc_channel exists + self._prep_wrapped_messages(client_info) + + @property + def grpc_channel(self) -> aio.Channel: + """Create the channel designed to connect to this service. + + This property caches on the instance; repeated calls return + the same channel. + """ + # Return the channel from cache. + return self._grpc_channel + + @property + def operations_client(self) -> operations_v1.OperationsAsyncClient: + """Create the client designed to process long-running operations. + + This property caches on the instance; repeated calls return the same + client. + """ + # Sanity check: Only create a new client if we do not already have one. + if self._operations_client is None: + self._operations_client = operations_v1.OperationsAsyncClient( + self.grpc_channel + ) + + # Return the client from cache. + return self._operations_client + + @property + def complete_query( + self, + ) -> Callable[ + [completion_service.CompleteQueryRequest], + Awaitable[completion_service.CompleteQueryResponse], + ]: + r"""Return a callable for the complete query method over gRPC. + + Completes the specified prefix with keyword + suggestions. + This feature is only available for users who have Retail + Search enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using + Retail Search. + + Returns: + Callable[[~.CompleteQueryRequest], + Awaitable[~.CompleteQueryResponse]]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "complete_query" not in self._stubs: + self._stubs["complete_query"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.CompletionService/CompleteQuery", + request_serializer=completion_service.CompleteQueryRequest.serialize, + response_deserializer=completion_service.CompleteQueryResponse.deserialize, + ) + return self._stubs["complete_query"] + + @property + def import_completion_data( + self, + ) -> Callable[ + [import_config.ImportCompletionDataRequest], Awaitable[operations_pb2.Operation] + ]: + r"""Return a callable for the import completion data method over gRPC. + + Bulk import of processed completion dataset. + Request processing may be synchronous. Partial updating + is not supported. + This feature is only available for users who have Retail + Search enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using + Retail Search. + + Returns: + Callable[[~.ImportCompletionDataRequest], + Awaitable[~.Operation]]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "import_completion_data" not in self._stubs: + self._stubs["import_completion_data"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.CompletionService/ImportCompletionData", + request_serializer=import_config.ImportCompletionDataRequest.serialize, + response_deserializer=operations_pb2.Operation.FromString, + ) + return self._stubs["import_completion_data"] + + +__all__ = ("CompletionServiceGrpcAsyncIOTransport",) diff --git a/google/cloud/retail_v2/services/product_service/async_client.py b/google/cloud/retail_v2/services/product_service/async_client.py index d18e7245..aec45efc 100644 --- a/google/cloud/retail_v2/services/product_service/async_client.py +++ b/google/cloud/retail_v2/services/product_service/async_client.py @@ -28,11 +28,13 @@ from google.api_core import operation # type: ignore from google.api_core import operation_async # type: ignore +from google.cloud.retail_v2.services.product_service import pagers from google.cloud.retail_v2.types import common from google.cloud.retail_v2.types import import_config from google.cloud.retail_v2.types import product from google.cloud.retail_v2.types import product as gcr_product from google.cloud.retail_v2.types import product_service +from google.protobuf import duration_pb2 # type: ignore from google.protobuf import field_mask_pb2 # type: ignore from google.protobuf import timestamp_pb2 # type: ignore from google.protobuf import wrappers_pb2 # type: ignore @@ -363,6 +365,95 @@ async def get_product( # Done; return the response. return response + async def list_products( + self, + request: product_service.ListProductsRequest = None, + *, + parent: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> pagers.ListProductsAsyncPager: + r"""Gets a list of [Product][google.cloud.retail.v2.Product]s. + + Args: + request (:class:`google.cloud.retail_v2.types.ListProductsRequest`): + The request object. Request message for + [ProductService.ListProducts][google.cloud.retail.v2.ProductService.ListProducts] + method. + parent (:class:`str`): + Required. The parent branch resource name, such as + ``projects/*/locations/global/catalogs/default_catalog/branches/0``. + Use ``default_branch`` as the branch ID, to list + products under the default branch. + + If the caller does not have permission to list + [Product][google.cloud.retail.v2.Product]s under this + branch, regardless of whether or not this branch exists, + a PERMISSION_DENIED error is returned. + + This corresponds to the ``parent`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + 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: + google.cloud.retail_v2.services.product_service.pagers.ListProductsAsyncPager: + Response message for + [ProductService.ListProducts][google.cloud.retail.v2.ProductService.ListProducts] + method. + + Iterating over this object will yield results and + resolve additional pages automatically. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([parent]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + request = product_service.ListProductsRequest(request) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if parent is not None: + request.parent = parent + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method_async.wrap_method( + self._client._transport.list_products, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("parent", request.parent),)), + ) + + # Send the request. + response = await rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # This method is paged; wrap the response in a pager, which provides + # an `__aiter__` convenience method. + response = pagers.ListProductsAsyncPager( + method=rpc, request=request, response=response, metadata=metadata, + ) + + # Done; return the response. + return response + async def update_product( self, request: product_service.UpdateProductRequest = None, @@ -388,7 +479,9 @@ async def update_product( returned. If the [Product][google.cloud.retail.v2.Product] to - update does not exist, a NOT_FOUND error is returned. + update does not exist and + [allow_missing][google.cloud.retail.v2.UpdateProductRequest.allow_missing] + is not set, a NOT_FOUND error is returned. This corresponds to the ``product`` field on the ``request`` instance; if ``request`` is provided, this @@ -488,6 +581,20 @@ async def delete_product( If the [Product][google.cloud.retail.v2.Product] to delete does not exist, a NOT_FOUND error is returned. + The [Product][google.cloud.retail.v2.Product] to delete + can neither be a + [Product.Type.COLLECTION][google.cloud.retail.v2.Product.Type.COLLECTION] + [Product][google.cloud.retail.v2.Product] member nor a + [Product.Type.PRIMARY][google.cloud.retail.v2.Product.Type.PRIMARY] + [Product][google.cloud.retail.v2.Product] with more than + one + [variants][google.cloud.retail.v2.Product.Type.VARIANT]. + Otherwise, an INVALID_ARGUMENT error is returned. + + All inventory information for the named + [Product][google.cloud.retail.v2.Product] will be + deleted. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -603,6 +710,401 @@ async def import_products( # Done; return the response. return response + async def set_inventory( + self, + request: product_service.SetInventoryRequest = None, + *, + inventory: product.Product = None, + set_mask: field_mask_pb2.FieldMask = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operation_async.AsyncOperation: + r"""Updates inventory information for a + [Product][google.cloud.retail.v2.Product] while respecting the + last update timestamps of each inventory field. + + This process is asynchronous and does not require the + [Product][google.cloud.retail.v2.Product] to exist before + updating fulfillment information. If the request is valid, the + update will be enqueued and processed downstream. As a + consequence, when a response is returned, updates are not + immediately manifested in the + [Product][google.cloud.retail.v2.Product] queried by + [GetProduct][google.cloud.retail.v2.ProductService.GetProduct] + or + [ListProducts][google.cloud.retail.v2.ProductService.ListProducts]. + + When inventory is updated with + [CreateProduct][google.cloud.retail.v2.ProductService.CreateProduct] + and + [UpdateProduct][google.cloud.retail.v2.ProductService.UpdateProduct], + the specified inventory field value(s) will overwrite any + existing value(s) while ignoring the last update time for this + field. Furthermore, the last update time for the specified + inventory fields will be overwritten to the time of the + [CreateProduct][google.cloud.retail.v2.ProductService.CreateProduct] + or + [UpdateProduct][google.cloud.retail.v2.ProductService.UpdateProduct] + request. + + If no inventory fields are set in + [CreateProductRequest.product][google.cloud.retail.v2.CreateProductRequest.product], + then any pre-existing inventory information for this product + will be used. + + If no inventory fields are set in + [UpdateProductRequest.set_mask][], then any existing inventory + information will be preserved. + + Pre-existing inventory information can only be updated with + [SetInventory][google.cloud.retail.v2.ProductService.SetInventory], + [AddFulfillmentPlaces][google.cloud.retail.v2.ProductService.AddFulfillmentPlaces], + and + [RemoveFulfillmentPlaces][google.cloud.retail.v2.ProductService.RemoveFulfillmentPlaces]. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Args: + request (:class:`google.cloud.retail_v2.types.SetInventoryRequest`): + The request object. Request message for [SetInventory][] + method. + inventory (:class:`google.cloud.retail_v2.types.Product`): + Required. The inventory information to update. The + allowable fields to update are: + + - [Product.price_info][google.cloud.retail.v2.Product.price_info] + - [Product.availability][google.cloud.retail.v2.Product.availability] + - [Product.available_quantity][google.cloud.retail.v2.Product.available_quantity] + - [Product.fulfillment_info][google.cloud.retail.v2.Product.fulfillment_info] + The updated inventory fields must be specified in + [SetInventoryRequest.set_mask][google.cloud.retail.v2.SetInventoryRequest.set_mask]. + + If [SetInventoryRequest.inventory.name][] is empty or + invalid, an INVALID_ARGUMENT error is returned. + + If the caller does not have permission to update the + [Product][google.cloud.retail.v2.Product] named in + [Product.name][google.cloud.retail.v2.Product.name], + regardless of whether or not it exists, a + PERMISSION_DENIED error is returned. + + If the [Product][google.cloud.retail.v2.Product] to + update does not have existing inventory information, the + provided inventory information will be inserted. + + If the [Product][google.cloud.retail.v2.Product] to + update has existing inventory information, the provided + inventory information will be merged while respecting + the last update time for each inventory field, using the + provided or default value for + [SetInventoryRequest.set_time][google.cloud.retail.v2.SetInventoryRequest.set_time]. + + The last update time is recorded for the following + inventory fields: + + - [Product.price_info][google.cloud.retail.v2.Product.price_info] + - [Product.availability][google.cloud.retail.v2.Product.availability] + - [Product.available_quantity][google.cloud.retail.v2.Product.available_quantity] + - [Product.fulfillment_info][google.cloud.retail.v2.Product.fulfillment_info] + + If a full overwrite of inventory information while + ignoring timestamps is needed, [UpdateProduct][] should + be invoked instead. + + This corresponds to the ``inventory`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + set_mask (:class:`google.protobuf.field_mask_pb2.FieldMask`): + Indicates which inventory fields in the provided + [Product][google.cloud.retail.v2.Product] to update. If + not set or set with empty paths, all inventory fields + will be updated. + + If an unsupported or unknown field is provided, an + INVALID_ARGUMENT error is returned and the entire update + will be ignored. + + This corresponds to the ``set_mask`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + 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: + google.api_core.operation_async.AsyncOperation: + An object representing a long-running operation. + + The result type for the operation will be :class:`google.cloud.retail_v2.types.SetInventoryResponse` Response of the SetInventoryRequest. Currently empty because + there is no meaningful response populated from the + [SetInventory][] method. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([inventory, set_mask]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + request = product_service.SetInventoryRequest(request) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if inventory is not None: + request.inventory = inventory + if set_mask is not None: + request.set_mask = set_mask + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method_async.wrap_method( + self._client._transport.set_inventory, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("inventory.name", request.inventory.name),) + ), + ) + + # Send the request. + response = await rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Wrap the response in an operation future. + response = operation_async.from_gapic( + response, + self._client._transport.operations_client, + product_service.SetInventoryResponse, + metadata_type=product_service.SetInventoryMetadata, + ) + + # Done; return the response. + return response + + async def add_fulfillment_places( + self, + request: product_service.AddFulfillmentPlacesRequest = None, + *, + product: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operation_async.AsyncOperation: + r"""Incrementally adds place IDs to + [Product.fulfillment_info.place_ids][google.cloud.retail.v2.FulfillmentInfo.place_ids]. + + This process is asynchronous and does not require the + [Product][google.cloud.retail.v2.Product] to exist before + updating fulfillment information. If the request is valid, the + update will be enqueued and processed downstream. As a + consequence, when a response is returned, the added place IDs + are not immediately manifested in the + [Product][google.cloud.retail.v2.Product] queried by + [GetProduct][google.cloud.retail.v2.ProductService.GetProduct] + or + [ListProducts][google.cloud.retail.v2.ProductService.ListProducts]. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Args: + request (:class:`google.cloud.retail_v2.types.AddFulfillmentPlacesRequest`): + The request object. Request message for + [AddFulfillmentPlaces][] method. + product (:class:`str`): + Required. Full resource name of + [Product][google.cloud.retail.v2.Product], such as + ``projects/*/locations/global/catalogs/default_catalog/branches/default_branch/products/some_product_id``. + + If the caller does not have permission to access the + [Product][google.cloud.retail.v2.Product], regardless of + whether or not it exists, a PERMISSION_DENIED error is + returned. + + This corresponds to the ``product`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + 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: + google.api_core.operation_async.AsyncOperation: + An object representing a long-running operation. + + The result type for the operation will be :class:`google.cloud.retail_v2.types.AddFulfillmentPlacesResponse` Response of the RemoveFulfillmentPlacesRequest. Currently empty because + there is no meaningful response populated from the + [AddFulfillmentPlaces][] method. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([product]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + request = product_service.AddFulfillmentPlacesRequest(request) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if product is not None: + request.product = product + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method_async.wrap_method( + self._client._transport.add_fulfillment_places, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("product", request.product),)), + ) + + # Send the request. + response = await rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Wrap the response in an operation future. + response = operation_async.from_gapic( + response, + self._client._transport.operations_client, + product_service.AddFulfillmentPlacesResponse, + metadata_type=product_service.AddFulfillmentPlacesMetadata, + ) + + # Done; return the response. + return response + + async def remove_fulfillment_places( + self, + request: product_service.RemoveFulfillmentPlacesRequest = None, + *, + product: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operation_async.AsyncOperation: + r"""Incrementally removes place IDs from a + [Product.fulfillment_info.place_ids][google.cloud.retail.v2.FulfillmentInfo.place_ids]. + + This process is asynchronous and does not require the + [Product][google.cloud.retail.v2.Product] to exist before + updating fulfillment information. If the request is valid, the + update will be enqueued and processed downstream. As a + consequence, when a response is returned, the removed place IDs + are not immediately manifested in the + [Product][google.cloud.retail.v2.Product] queried by + [GetProduct][google.cloud.retail.v2.ProductService.GetProduct] + or + [ListProducts][google.cloud.retail.v2.ProductService.ListProducts]. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Args: + request (:class:`google.cloud.retail_v2.types.RemoveFulfillmentPlacesRequest`): + The request object. Request message for + [RemoveFulfillmentPlaces][] method. + product (:class:`str`): + Required. Full resource name of + [Product][google.cloud.retail.v2.Product], such as + ``projects/*/locations/global/catalogs/default_catalog/branches/default_branch/products/some_product_id``. + + If the caller does not have permission to access the + [Product][google.cloud.retail.v2.Product], regardless of + whether or not it exists, a PERMISSION_DENIED error is + returned. + + This corresponds to the ``product`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + 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: + google.api_core.operation_async.AsyncOperation: + An object representing a long-running operation. + + The result type for the operation will be :class:`google.cloud.retail_v2.types.RemoveFulfillmentPlacesResponse` Response of the RemoveFulfillmentPlacesRequest. Currently empty because there + is no meaningful response populated from the + [RemoveFulfillmentPlaces][] method. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([product]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + request = product_service.RemoveFulfillmentPlacesRequest(request) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if product is not None: + request.product = product + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method_async.wrap_method( + self._client._transport.remove_fulfillment_places, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("product", request.product),)), + ) + + # Send the request. + response = await rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Wrap the response in an operation future. + response = operation_async.from_gapic( + response, + self._client._transport.operations_client, + product_service.RemoveFulfillmentPlacesResponse, + metadata_type=product_service.RemoveFulfillmentPlacesMetadata, + ) + + # Done; return the response. + return response + try: DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( diff --git a/google/cloud/retail_v2/services/product_service/client.py b/google/cloud/retail_v2/services/product_service/client.py index 07d17c01..44619e70 100644 --- a/google/cloud/retail_v2/services/product_service/client.py +++ b/google/cloud/retail_v2/services/product_service/client.py @@ -32,11 +32,13 @@ from google.api_core import operation # type: ignore from google.api_core import operation_async # type: ignore +from google.cloud.retail_v2.services.product_service import pagers from google.cloud.retail_v2.types import common from google.cloud.retail_v2.types import import_config from google.cloud.retail_v2.types import product from google.cloud.retail_v2.types import product as gcr_product from google.cloud.retail_v2.types import product_service +from google.protobuf import duration_pb2 # type: ignore from google.protobuf import field_mask_pb2 # type: ignore from google.protobuf import timestamp_pb2 # type: ignore from google.protobuf import wrappers_pb2 # type: ignore @@ -572,6 +574,95 @@ def get_product( # Done; return the response. return response + def list_products( + self, + request: product_service.ListProductsRequest = None, + *, + parent: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> pagers.ListProductsPager: + r"""Gets a list of [Product][google.cloud.retail.v2.Product]s. + + Args: + request (google.cloud.retail_v2.types.ListProductsRequest): + The request object. Request message for + [ProductService.ListProducts][google.cloud.retail.v2.ProductService.ListProducts] + method. + parent (str): + Required. The parent branch resource name, such as + ``projects/*/locations/global/catalogs/default_catalog/branches/0``. + Use ``default_branch`` as the branch ID, to list + products under the default branch. + + If the caller does not have permission to list + [Product][google.cloud.retail.v2.Product]s under this + branch, regardless of whether or not this branch exists, + a PERMISSION_DENIED error is returned. + + This corresponds to the ``parent`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + 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: + google.cloud.retail_v2.services.product_service.pagers.ListProductsPager: + Response message for + [ProductService.ListProducts][google.cloud.retail.v2.ProductService.ListProducts] + method. + + Iterating over this object will yield results and + resolve additional pages automatically. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([parent]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + # Minor optimization to avoid making a copy if the user passes + # in a product_service.ListProductsRequest. + # There's no risk of modifying the input as we've already verified + # there are no flattened fields. + if not isinstance(request, product_service.ListProductsRequest): + request = product_service.ListProductsRequest(request) + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if parent is not None: + request.parent = parent + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.list_products] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("parent", request.parent),)), + ) + + # Send the request. + response = rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # This method is paged; wrap the response in a pager, which provides + # an `__iter__` convenience method. + response = pagers.ListProductsPager( + method=rpc, request=request, response=response, metadata=metadata, + ) + + # Done; return the response. + return response + def update_product( self, request: product_service.UpdateProductRequest = None, @@ -597,7 +688,9 @@ def update_product( returned. If the [Product][google.cloud.retail.v2.Product] to - update does not exist, a NOT_FOUND error is returned. + update does not exist and + [allow_missing][google.cloud.retail.v2.UpdateProductRequest.allow_missing] + is not set, a NOT_FOUND error is returned. This corresponds to the ``product`` field on the ``request`` instance; if ``request`` is provided, this @@ -697,6 +790,20 @@ def delete_product( If the [Product][google.cloud.retail.v2.Product] to delete does not exist, a NOT_FOUND error is returned. + The [Product][google.cloud.retail.v2.Product] to delete + can neither be a + [Product.Type.COLLECTION][google.cloud.retail.v2.Product.Type.COLLECTION] + [Product][google.cloud.retail.v2.Product] member nor a + [Product.Type.PRIMARY][google.cloud.retail.v2.Product.Type.PRIMARY] + [Product][google.cloud.retail.v2.Product] with more than + one + [variants][google.cloud.retail.v2.Product.Type.VARIANT]. + Otherwise, an INVALID_ARGUMENT error is returned. + + All inventory information for the named + [Product][google.cloud.retail.v2.Product] will be + deleted. + This corresponds to the ``name`` field on the ``request`` instance; if ``request`` is provided, this should not be set. @@ -813,6 +920,403 @@ def import_products( # Done; return the response. return response + def set_inventory( + self, + request: product_service.SetInventoryRequest = None, + *, + inventory: product.Product = None, + set_mask: field_mask_pb2.FieldMask = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operation.Operation: + r"""Updates inventory information for a + [Product][google.cloud.retail.v2.Product] while respecting the + last update timestamps of each inventory field. + + This process is asynchronous and does not require the + [Product][google.cloud.retail.v2.Product] to exist before + updating fulfillment information. If the request is valid, the + update will be enqueued and processed downstream. As a + consequence, when a response is returned, updates are not + immediately manifested in the + [Product][google.cloud.retail.v2.Product] queried by + [GetProduct][google.cloud.retail.v2.ProductService.GetProduct] + or + [ListProducts][google.cloud.retail.v2.ProductService.ListProducts]. + + When inventory is updated with + [CreateProduct][google.cloud.retail.v2.ProductService.CreateProduct] + and + [UpdateProduct][google.cloud.retail.v2.ProductService.UpdateProduct], + the specified inventory field value(s) will overwrite any + existing value(s) while ignoring the last update time for this + field. Furthermore, the last update time for the specified + inventory fields will be overwritten to the time of the + [CreateProduct][google.cloud.retail.v2.ProductService.CreateProduct] + or + [UpdateProduct][google.cloud.retail.v2.ProductService.UpdateProduct] + request. + + If no inventory fields are set in + [CreateProductRequest.product][google.cloud.retail.v2.CreateProductRequest.product], + then any pre-existing inventory information for this product + will be used. + + If no inventory fields are set in + [UpdateProductRequest.set_mask][], then any existing inventory + information will be preserved. + + Pre-existing inventory information can only be updated with + [SetInventory][google.cloud.retail.v2.ProductService.SetInventory], + [AddFulfillmentPlaces][google.cloud.retail.v2.ProductService.AddFulfillmentPlaces], + and + [RemoveFulfillmentPlaces][google.cloud.retail.v2.ProductService.RemoveFulfillmentPlaces]. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Args: + request (google.cloud.retail_v2.types.SetInventoryRequest): + The request object. Request message for [SetInventory][] + method. + inventory (google.cloud.retail_v2.types.Product): + Required. The inventory information to update. The + allowable fields to update are: + + - [Product.price_info][google.cloud.retail.v2.Product.price_info] + - [Product.availability][google.cloud.retail.v2.Product.availability] + - [Product.available_quantity][google.cloud.retail.v2.Product.available_quantity] + - [Product.fulfillment_info][google.cloud.retail.v2.Product.fulfillment_info] + The updated inventory fields must be specified in + [SetInventoryRequest.set_mask][google.cloud.retail.v2.SetInventoryRequest.set_mask]. + + If [SetInventoryRequest.inventory.name][] is empty or + invalid, an INVALID_ARGUMENT error is returned. + + If the caller does not have permission to update the + [Product][google.cloud.retail.v2.Product] named in + [Product.name][google.cloud.retail.v2.Product.name], + regardless of whether or not it exists, a + PERMISSION_DENIED error is returned. + + If the [Product][google.cloud.retail.v2.Product] to + update does not have existing inventory information, the + provided inventory information will be inserted. + + If the [Product][google.cloud.retail.v2.Product] to + update has existing inventory information, the provided + inventory information will be merged while respecting + the last update time for each inventory field, using the + provided or default value for + [SetInventoryRequest.set_time][google.cloud.retail.v2.SetInventoryRequest.set_time]. + + The last update time is recorded for the following + inventory fields: + + - [Product.price_info][google.cloud.retail.v2.Product.price_info] + - [Product.availability][google.cloud.retail.v2.Product.availability] + - [Product.available_quantity][google.cloud.retail.v2.Product.available_quantity] + - [Product.fulfillment_info][google.cloud.retail.v2.Product.fulfillment_info] + + If a full overwrite of inventory information while + ignoring timestamps is needed, [UpdateProduct][] should + be invoked instead. + + This corresponds to the ``inventory`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + set_mask (google.protobuf.field_mask_pb2.FieldMask): + Indicates which inventory fields in the provided + [Product][google.cloud.retail.v2.Product] to update. If + not set or set with empty paths, all inventory fields + will be updated. + + If an unsupported or unknown field is provided, an + INVALID_ARGUMENT error is returned and the entire update + will be ignored. + + This corresponds to the ``set_mask`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + 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: + google.api_core.operation.Operation: + An object representing a long-running operation. + + The result type for the operation will be :class:`google.cloud.retail_v2.types.SetInventoryResponse` Response of the SetInventoryRequest. Currently empty because + there is no meaningful response populated from the + [SetInventory][] method. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([inventory, set_mask]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + # Minor optimization to avoid making a copy if the user passes + # in a product_service.SetInventoryRequest. + # There's no risk of modifying the input as we've already verified + # there are no flattened fields. + if not isinstance(request, product_service.SetInventoryRequest): + request = product_service.SetInventoryRequest(request) + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if inventory is not None: + request.inventory = inventory + if set_mask is not None: + request.set_mask = set_mask + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.set_inventory] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("inventory.name", request.inventory.name),) + ), + ) + + # Send the request. + response = rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Wrap the response in an operation future. + response = operation.from_gapic( + response, + self._transport.operations_client, + product_service.SetInventoryResponse, + metadata_type=product_service.SetInventoryMetadata, + ) + + # Done; return the response. + return response + + def add_fulfillment_places( + self, + request: product_service.AddFulfillmentPlacesRequest = None, + *, + product: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operation.Operation: + r"""Incrementally adds place IDs to + [Product.fulfillment_info.place_ids][google.cloud.retail.v2.FulfillmentInfo.place_ids]. + + This process is asynchronous and does not require the + [Product][google.cloud.retail.v2.Product] to exist before + updating fulfillment information. If the request is valid, the + update will be enqueued and processed downstream. As a + consequence, when a response is returned, the added place IDs + are not immediately manifested in the + [Product][google.cloud.retail.v2.Product] queried by + [GetProduct][google.cloud.retail.v2.ProductService.GetProduct] + or + [ListProducts][google.cloud.retail.v2.ProductService.ListProducts]. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Args: + request (google.cloud.retail_v2.types.AddFulfillmentPlacesRequest): + The request object. Request message for + [AddFulfillmentPlaces][] method. + product (str): + Required. Full resource name of + [Product][google.cloud.retail.v2.Product], such as + ``projects/*/locations/global/catalogs/default_catalog/branches/default_branch/products/some_product_id``. + + If the caller does not have permission to access the + [Product][google.cloud.retail.v2.Product], regardless of + whether or not it exists, a PERMISSION_DENIED error is + returned. + + This corresponds to the ``product`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + 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: + google.api_core.operation.Operation: + An object representing a long-running operation. + + The result type for the operation will be :class:`google.cloud.retail_v2.types.AddFulfillmentPlacesResponse` Response of the RemoveFulfillmentPlacesRequest. Currently empty because + there is no meaningful response populated from the + [AddFulfillmentPlaces][] method. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([product]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + # Minor optimization to avoid making a copy if the user passes + # in a product_service.AddFulfillmentPlacesRequest. + # There's no risk of modifying the input as we've already verified + # there are no flattened fields. + if not isinstance(request, product_service.AddFulfillmentPlacesRequest): + request = product_service.AddFulfillmentPlacesRequest(request) + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if product is not None: + request.product = product + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.add_fulfillment_places] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("product", request.product),)), + ) + + # Send the request. + response = rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Wrap the response in an operation future. + response = operation.from_gapic( + response, + self._transport.operations_client, + product_service.AddFulfillmentPlacesResponse, + metadata_type=product_service.AddFulfillmentPlacesMetadata, + ) + + # Done; return the response. + return response + + def remove_fulfillment_places( + self, + request: product_service.RemoveFulfillmentPlacesRequest = None, + *, + product: str = None, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operation.Operation: + r"""Incrementally removes place IDs from a + [Product.fulfillment_info.place_ids][google.cloud.retail.v2.FulfillmentInfo.place_ids]. + + This process is asynchronous and does not require the + [Product][google.cloud.retail.v2.Product] to exist before + updating fulfillment information. If the request is valid, the + update will be enqueued and processed downstream. As a + consequence, when a response is returned, the removed place IDs + are not immediately manifested in the + [Product][google.cloud.retail.v2.Product] queried by + [GetProduct][google.cloud.retail.v2.ProductService.GetProduct] + or + [ListProducts][google.cloud.retail.v2.ProductService.ListProducts]. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Args: + request (google.cloud.retail_v2.types.RemoveFulfillmentPlacesRequest): + The request object. Request message for + [RemoveFulfillmentPlaces][] method. + product (str): + Required. Full resource name of + [Product][google.cloud.retail.v2.Product], such as + ``projects/*/locations/global/catalogs/default_catalog/branches/default_branch/products/some_product_id``. + + If the caller does not have permission to access the + [Product][google.cloud.retail.v2.Product], regardless of + whether or not it exists, a PERMISSION_DENIED error is + returned. + + This corresponds to the ``product`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + 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: + google.api_core.operation.Operation: + An object representing a long-running operation. + + The result type for the operation will be :class:`google.cloud.retail_v2.types.RemoveFulfillmentPlacesResponse` Response of the RemoveFulfillmentPlacesRequest. Currently empty because there + is no meaningful response populated from the + [RemoveFulfillmentPlaces][] method. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([product]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + # Minor optimization to avoid making a copy if the user passes + # in a product_service.RemoveFulfillmentPlacesRequest. + # There's no risk of modifying the input as we've already verified + # there are no flattened fields. + if not isinstance(request, product_service.RemoveFulfillmentPlacesRequest): + request = product_service.RemoveFulfillmentPlacesRequest(request) + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if product is not None: + request.product = product + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[ + self._transport.remove_fulfillment_places + ] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("product", request.product),)), + ) + + # Send the request. + response = rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Wrap the response in an operation future. + response = operation.from_gapic( + response, + self._transport.operations_client, + product_service.RemoveFulfillmentPlacesResponse, + metadata_type=product_service.RemoveFulfillmentPlacesMetadata, + ) + + # Done; return the response. + return response + try: DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( diff --git a/google/cloud/retail_v2/services/product_service/pagers.py b/google/cloud/retail_v2/services/product_service/pagers.py new file mode 100644 index 00000000..28c88e34 --- /dev/null +++ b/google/cloud/retail_v2/services/product_service/pagers.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from typing import ( + Any, + AsyncIterable, + Awaitable, + Callable, + Iterable, + Sequence, + Tuple, + Optional, +) + +from google.cloud.retail_v2.types import product +from google.cloud.retail_v2.types import product_service + + +class ListProductsPager: + """A pager for iterating through ``list_products`` requests. + + This class thinly wraps an initial + :class:`google.cloud.retail_v2.types.ListProductsResponse` object, and + provides an ``__iter__`` method to iterate through its + ``products`` field. + + If there are more pages, the ``__iter__`` method will make additional + ``ListProducts`` requests and continue to iterate + through the ``products`` field on the + corresponding responses. + + All the usual :class:`google.cloud.retail_v2.types.ListProductsResponse` + attributes are available on the pager. If multiple requests are made, only + the most recent response is retained, and thus used for attribute lookup. + """ + + def __init__( + self, + method: Callable[..., product_service.ListProductsResponse], + request: product_service.ListProductsRequest, + response: product_service.ListProductsResponse, + *, + metadata: Sequence[Tuple[str, str]] = () + ): + """Instantiate the pager. + + Args: + method (Callable): The method that was originally called, and + which instantiated this pager. + request (google.cloud.retail_v2.types.ListProductsRequest): + The initial request object. + response (google.cloud.retail_v2.types.ListProductsResponse): + The initial response object. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + """ + self._method = method + self._request = product_service.ListProductsRequest(request) + self._response = response + self._metadata = metadata + + def __getattr__(self, name: str) -> Any: + return getattr(self._response, name) + + @property + def pages(self) -> Iterable[product_service.ListProductsResponse]: + yield self._response + while self._response.next_page_token: + self._request.page_token = self._response.next_page_token + self._response = self._method(self._request, metadata=self._metadata) + yield self._response + + def __iter__(self) -> Iterable[product.Product]: + for page in self.pages: + yield from page.products + + def __repr__(self) -> str: + return "{0}<{1!r}>".format(self.__class__.__name__, self._response) + + +class ListProductsAsyncPager: + """A pager for iterating through ``list_products`` requests. + + This class thinly wraps an initial + :class:`google.cloud.retail_v2.types.ListProductsResponse` object, and + provides an ``__aiter__`` method to iterate through its + ``products`` field. + + If there are more pages, the ``__aiter__`` method will make additional + ``ListProducts`` requests and continue to iterate + through the ``products`` field on the + corresponding responses. + + All the usual :class:`google.cloud.retail_v2.types.ListProductsResponse` + attributes are available on the pager. If multiple requests are made, only + the most recent response is retained, and thus used for attribute lookup. + """ + + def __init__( + self, + method: Callable[..., Awaitable[product_service.ListProductsResponse]], + request: product_service.ListProductsRequest, + response: product_service.ListProductsResponse, + *, + metadata: Sequence[Tuple[str, str]] = () + ): + """Instantiates the pager. + + Args: + method (Callable): The method that was originally called, and + which instantiated this pager. + request (google.cloud.retail_v2.types.ListProductsRequest): + The initial request object. + response (google.cloud.retail_v2.types.ListProductsResponse): + The initial response object. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + """ + self._method = method + self._request = product_service.ListProductsRequest(request) + self._response = response + self._metadata = metadata + + def __getattr__(self, name: str) -> Any: + return getattr(self._response, name) + + @property + async def pages(self) -> AsyncIterable[product_service.ListProductsResponse]: + yield self._response + while self._response.next_page_token: + self._request.page_token = self._response.next_page_token + self._response = await self._method(self._request, metadata=self._metadata) + yield self._response + + def __aiter__(self) -> AsyncIterable[product.Product]: + async def async_generator(): + async for page in self.pages: + for response in page.products: + yield response + + return async_generator() + + def __repr__(self) -> str: + return "{0}<{1!r}>".format(self.__class__.__name__, self._response) diff --git a/google/cloud/retail_v2/services/product_service/transports/base.py b/google/cloud/retail_v2/services/product_service/transports/base.py index 6ede9933..75a390f2 100644 --- a/google/cloud/retail_v2/services/product_service/transports/base.py +++ b/google/cloud/retail_v2/services/product_service/transports/base.py @@ -164,6 +164,9 @@ def _prep_wrapped_messages(self, client_info): self.get_product: gapic_v1.method.wrap_method( self.get_product, default_timeout=None, client_info=client_info, ), + self.list_products: gapic_v1.method.wrap_method( + self.list_products, default_timeout=None, client_info=client_info, + ), self.update_product: gapic_v1.method.wrap_method( self.update_product, default_timeout=None, client_info=client_info, ), @@ -173,6 +176,19 @@ def _prep_wrapped_messages(self, client_info): self.import_products: gapic_v1.method.wrap_method( self.import_products, default_timeout=None, client_info=client_info, ), + self.set_inventory: gapic_v1.method.wrap_method( + self.set_inventory, default_timeout=None, client_info=client_info, + ), + self.add_fulfillment_places: gapic_v1.method.wrap_method( + self.add_fulfillment_places, + default_timeout=None, + client_info=client_info, + ), + self.remove_fulfillment_places: gapic_v1.method.wrap_method( + self.remove_fulfillment_places, + default_timeout=None, + client_info=client_info, + ), } @property @@ -198,6 +214,18 @@ def get_product( ]: raise NotImplementedError() + @property + def list_products( + self, + ) -> Callable[ + [product_service.ListProductsRequest], + Union[ + product_service.ListProductsResponse, + Awaitable[product_service.ListProductsResponse], + ], + ]: + raise NotImplementedError() + @property def update_product( self, @@ -225,5 +253,32 @@ def import_products( ]: raise NotImplementedError() + @property + def set_inventory( + self, + ) -> Callable[ + [product_service.SetInventoryRequest], + Union[operations_pb2.Operation, Awaitable[operations_pb2.Operation]], + ]: + raise NotImplementedError() + + @property + def add_fulfillment_places( + self, + ) -> Callable[ + [product_service.AddFulfillmentPlacesRequest], + Union[operations_pb2.Operation, Awaitable[operations_pb2.Operation]], + ]: + raise NotImplementedError() + + @property + def remove_fulfillment_places( + self, + ) -> Callable[ + [product_service.RemoveFulfillmentPlacesRequest], + Union[operations_pb2.Operation, Awaitable[operations_pb2.Operation]], + ]: + raise NotImplementedError() + __all__ = ("ProductServiceTransport",) diff --git a/google/cloud/retail_v2/services/product_service/transports/grpc.py b/google/cloud/retail_v2/services/product_service/transports/grpc.py index 0e2f3e7b..8599fc3f 100644 --- a/google/cloud/retail_v2/services/product_service/transports/grpc.py +++ b/google/cloud/retail_v2/services/product_service/transports/grpc.py @@ -299,6 +299,34 @@ def get_product( ) return self._stubs["get_product"] + @property + def list_products( + self, + ) -> Callable[ + [product_service.ListProductsRequest], product_service.ListProductsResponse + ]: + r"""Return a callable for the list products method over gRPC. + + Gets a list of [Product][google.cloud.retail.v2.Product]s. + + Returns: + Callable[[~.ListProductsRequest], + ~.ListProductsResponse]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "list_products" not in self._stubs: + self._stubs["list_products"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.ProductService/ListProducts", + request_serializer=product_service.ListProductsRequest.serialize, + response_deserializer=product_service.ListProductsResponse.deserialize, + ) + return self._stubs["list_products"] + @property def update_product( self, @@ -385,5 +413,167 @@ def import_products( ) return self._stubs["import_products"] + @property + def set_inventory( + self, + ) -> Callable[[product_service.SetInventoryRequest], operations_pb2.Operation]: + r"""Return a callable for the set inventory method over gRPC. + + Updates inventory information for a + [Product][google.cloud.retail.v2.Product] while respecting the + last update timestamps of each inventory field. + + This process is asynchronous and does not require the + [Product][google.cloud.retail.v2.Product] to exist before + updating fulfillment information. If the request is valid, the + update will be enqueued and processed downstream. As a + consequence, when a response is returned, updates are not + immediately manifested in the + [Product][google.cloud.retail.v2.Product] queried by + [GetProduct][google.cloud.retail.v2.ProductService.GetProduct] + or + [ListProducts][google.cloud.retail.v2.ProductService.ListProducts]. + + When inventory is updated with + [CreateProduct][google.cloud.retail.v2.ProductService.CreateProduct] + and + [UpdateProduct][google.cloud.retail.v2.ProductService.UpdateProduct], + the specified inventory field value(s) will overwrite any + existing value(s) while ignoring the last update time for this + field. Furthermore, the last update time for the specified + inventory fields will be overwritten to the time of the + [CreateProduct][google.cloud.retail.v2.ProductService.CreateProduct] + or + [UpdateProduct][google.cloud.retail.v2.ProductService.UpdateProduct] + request. + + If no inventory fields are set in + [CreateProductRequest.product][google.cloud.retail.v2.CreateProductRequest.product], + then any pre-existing inventory information for this product + will be used. + + If no inventory fields are set in + [UpdateProductRequest.set_mask][], then any existing inventory + information will be preserved. + + Pre-existing inventory information can only be updated with + [SetInventory][google.cloud.retail.v2.ProductService.SetInventory], + [AddFulfillmentPlaces][google.cloud.retail.v2.ProductService.AddFulfillmentPlaces], + and + [RemoveFulfillmentPlaces][google.cloud.retail.v2.ProductService.RemoveFulfillmentPlaces]. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Returns: + Callable[[~.SetInventoryRequest], + ~.Operation]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "set_inventory" not in self._stubs: + self._stubs["set_inventory"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.ProductService/SetInventory", + request_serializer=product_service.SetInventoryRequest.serialize, + response_deserializer=operations_pb2.Operation.FromString, + ) + return self._stubs["set_inventory"] + + @property + def add_fulfillment_places( + self, + ) -> Callable[ + [product_service.AddFulfillmentPlacesRequest], operations_pb2.Operation + ]: + r"""Return a callable for the add fulfillment places method over gRPC. + + Incrementally adds place IDs to + [Product.fulfillment_info.place_ids][google.cloud.retail.v2.FulfillmentInfo.place_ids]. + + This process is asynchronous and does not require the + [Product][google.cloud.retail.v2.Product] to exist before + updating fulfillment information. If the request is valid, the + update will be enqueued and processed downstream. As a + consequence, when a response is returned, the added place IDs + are not immediately manifested in the + [Product][google.cloud.retail.v2.Product] queried by + [GetProduct][google.cloud.retail.v2.ProductService.GetProduct] + or + [ListProducts][google.cloud.retail.v2.ProductService.ListProducts]. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Returns: + Callable[[~.AddFulfillmentPlacesRequest], + ~.Operation]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "add_fulfillment_places" not in self._stubs: + self._stubs["add_fulfillment_places"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.ProductService/AddFulfillmentPlaces", + request_serializer=product_service.AddFulfillmentPlacesRequest.serialize, + response_deserializer=operations_pb2.Operation.FromString, + ) + return self._stubs["add_fulfillment_places"] + + @property + def remove_fulfillment_places( + self, + ) -> Callable[ + [product_service.RemoveFulfillmentPlacesRequest], operations_pb2.Operation + ]: + r"""Return a callable for the remove fulfillment places method over gRPC. + + Incrementally removes place IDs from a + [Product.fulfillment_info.place_ids][google.cloud.retail.v2.FulfillmentInfo.place_ids]. + + This process is asynchronous and does not require the + [Product][google.cloud.retail.v2.Product] to exist before + updating fulfillment information. If the request is valid, the + update will be enqueued and processed downstream. As a + consequence, when a response is returned, the removed place IDs + are not immediately manifested in the + [Product][google.cloud.retail.v2.Product] queried by + [GetProduct][google.cloud.retail.v2.ProductService.GetProduct] + or + [ListProducts][google.cloud.retail.v2.ProductService.ListProducts]. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Returns: + Callable[[~.RemoveFulfillmentPlacesRequest], + ~.Operation]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "remove_fulfillment_places" not in self._stubs: + self._stubs["remove_fulfillment_places"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.ProductService/RemoveFulfillmentPlaces", + request_serializer=product_service.RemoveFulfillmentPlacesRequest.serialize, + response_deserializer=operations_pb2.Operation.FromString, + ) + return self._stubs["remove_fulfillment_places"] + __all__ = ("ProductServiceGrpcTransport",) diff --git a/google/cloud/retail_v2/services/product_service/transports/grpc_asyncio.py b/google/cloud/retail_v2/services/product_service/transports/grpc_asyncio.py index 0e08444e..5ef2127f 100644 --- a/google/cloud/retail_v2/services/product_service/transports/grpc_asyncio.py +++ b/google/cloud/retail_v2/services/product_service/transports/grpc_asyncio.py @@ -306,6 +306,35 @@ def get_product( ) return self._stubs["get_product"] + @property + def list_products( + self, + ) -> Callable[ + [product_service.ListProductsRequest], + Awaitable[product_service.ListProductsResponse], + ]: + r"""Return a callable for the list products method over gRPC. + + Gets a list of [Product][google.cloud.retail.v2.Product]s. + + Returns: + Callable[[~.ListProductsRequest], + Awaitable[~.ListProductsResponse]]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "list_products" not in self._stubs: + self._stubs["list_products"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.ProductService/ListProducts", + request_serializer=product_service.ListProductsRequest.serialize, + response_deserializer=product_service.ListProductsResponse.deserialize, + ) + return self._stubs["list_products"] + @property def update_product( self, @@ -396,5 +425,171 @@ def import_products( ) return self._stubs["import_products"] + @property + def set_inventory( + self, + ) -> Callable[ + [product_service.SetInventoryRequest], Awaitable[operations_pb2.Operation] + ]: + r"""Return a callable for the set inventory method over gRPC. + + Updates inventory information for a + [Product][google.cloud.retail.v2.Product] while respecting the + last update timestamps of each inventory field. + + This process is asynchronous and does not require the + [Product][google.cloud.retail.v2.Product] to exist before + updating fulfillment information. If the request is valid, the + update will be enqueued and processed downstream. As a + consequence, when a response is returned, updates are not + immediately manifested in the + [Product][google.cloud.retail.v2.Product] queried by + [GetProduct][google.cloud.retail.v2.ProductService.GetProduct] + or + [ListProducts][google.cloud.retail.v2.ProductService.ListProducts]. + + When inventory is updated with + [CreateProduct][google.cloud.retail.v2.ProductService.CreateProduct] + and + [UpdateProduct][google.cloud.retail.v2.ProductService.UpdateProduct], + the specified inventory field value(s) will overwrite any + existing value(s) while ignoring the last update time for this + field. Furthermore, the last update time for the specified + inventory fields will be overwritten to the time of the + [CreateProduct][google.cloud.retail.v2.ProductService.CreateProduct] + or + [UpdateProduct][google.cloud.retail.v2.ProductService.UpdateProduct] + request. + + If no inventory fields are set in + [CreateProductRequest.product][google.cloud.retail.v2.CreateProductRequest.product], + then any pre-existing inventory information for this product + will be used. + + If no inventory fields are set in + [UpdateProductRequest.set_mask][], then any existing inventory + information will be preserved. + + Pre-existing inventory information can only be updated with + [SetInventory][google.cloud.retail.v2.ProductService.SetInventory], + [AddFulfillmentPlaces][google.cloud.retail.v2.ProductService.AddFulfillmentPlaces], + and + [RemoveFulfillmentPlaces][google.cloud.retail.v2.ProductService.RemoveFulfillmentPlaces]. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Returns: + Callable[[~.SetInventoryRequest], + Awaitable[~.Operation]]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "set_inventory" not in self._stubs: + self._stubs["set_inventory"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.ProductService/SetInventory", + request_serializer=product_service.SetInventoryRequest.serialize, + response_deserializer=operations_pb2.Operation.FromString, + ) + return self._stubs["set_inventory"] + + @property + def add_fulfillment_places( + self, + ) -> Callable[ + [product_service.AddFulfillmentPlacesRequest], + Awaitable[operations_pb2.Operation], + ]: + r"""Return a callable for the add fulfillment places method over gRPC. + + Incrementally adds place IDs to + [Product.fulfillment_info.place_ids][google.cloud.retail.v2.FulfillmentInfo.place_ids]. + + This process is asynchronous and does not require the + [Product][google.cloud.retail.v2.Product] to exist before + updating fulfillment information. If the request is valid, the + update will be enqueued and processed downstream. As a + consequence, when a response is returned, the added place IDs + are not immediately manifested in the + [Product][google.cloud.retail.v2.Product] queried by + [GetProduct][google.cloud.retail.v2.ProductService.GetProduct] + or + [ListProducts][google.cloud.retail.v2.ProductService.ListProducts]. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Returns: + Callable[[~.AddFulfillmentPlacesRequest], + Awaitable[~.Operation]]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "add_fulfillment_places" not in self._stubs: + self._stubs["add_fulfillment_places"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.ProductService/AddFulfillmentPlaces", + request_serializer=product_service.AddFulfillmentPlacesRequest.serialize, + response_deserializer=operations_pb2.Operation.FromString, + ) + return self._stubs["add_fulfillment_places"] + + @property + def remove_fulfillment_places( + self, + ) -> Callable[ + [product_service.RemoveFulfillmentPlacesRequest], + Awaitable[operations_pb2.Operation], + ]: + r"""Return a callable for the remove fulfillment places method over gRPC. + + Incrementally removes place IDs from a + [Product.fulfillment_info.place_ids][google.cloud.retail.v2.FulfillmentInfo.place_ids]. + + This process is asynchronous and does not require the + [Product][google.cloud.retail.v2.Product] to exist before + updating fulfillment information. If the request is valid, the + update will be enqueued and processed downstream. As a + consequence, when a response is returned, the removed place IDs + are not immediately manifested in the + [Product][google.cloud.retail.v2.Product] queried by + [GetProduct][google.cloud.retail.v2.ProductService.GetProduct] + or + [ListProducts][google.cloud.retail.v2.ProductService.ListProducts]. + + This feature is only available for users who have Retail Search + enabled. Contact Retail Support + (retail-search-support@google.com) if you are interested in + using Retail Search. + + Returns: + Callable[[~.RemoveFulfillmentPlacesRequest], + Awaitable[~.Operation]]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "remove_fulfillment_places" not in self._stubs: + self._stubs["remove_fulfillment_places"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.ProductService/RemoveFulfillmentPlaces", + request_serializer=product_service.RemoveFulfillmentPlacesRequest.serialize, + response_deserializer=operations_pb2.Operation.FromString, + ) + return self._stubs["remove_fulfillment_places"] + __all__ = ("ProductServiceGrpcAsyncIOTransport",) diff --git a/google/cloud/retail_v2/services/search_service/__init__.py b/google/cloud/retail_v2/services/search_service/__init__.py new file mode 100644 index 00000000..af9d3c76 --- /dev/null +++ b/google/cloud/retail_v2/services/search_service/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from .client import SearchServiceClient +from .async_client import SearchServiceAsyncClient + +__all__ = ( + "SearchServiceClient", + "SearchServiceAsyncClient", +) diff --git a/google/cloud/retail_v2/services/search_service/async_client.py b/google/cloud/retail_v2/services/search_service/async_client.py new file mode 100644 index 00000000..0d29b142 --- /dev/null +++ b/google/cloud/retail_v2/services/search_service/async_client.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from collections import OrderedDict +import functools +import re +from typing import Dict, Sequence, Tuple, Type, Union +import pkg_resources + +import google.api_core.client_options as ClientOptions # type: ignore +from google.api_core import exceptions as core_exceptions # type: ignore +from google.api_core import gapic_v1 # type: ignore +from google.api_core import retry as retries # type: ignore +from google.auth import credentials as ga_credentials # type: ignore +from google.oauth2 import service_account # type: ignore + +from google.cloud.retail_v2.services.search_service import pagers +from google.cloud.retail_v2.types import search_service +from .transports.base import SearchServiceTransport, DEFAULT_CLIENT_INFO +from .transports.grpc_asyncio import SearchServiceGrpcAsyncIOTransport +from .client import SearchServiceClient + + +class SearchServiceAsyncClient: + """Service for search. + This feature is only available for users who have Retail Search + enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using Retail + Search. + """ + + _client: SearchServiceClient + + DEFAULT_ENDPOINT = SearchServiceClient.DEFAULT_ENDPOINT + DEFAULT_MTLS_ENDPOINT = SearchServiceClient.DEFAULT_MTLS_ENDPOINT + + branch_path = staticmethod(SearchServiceClient.branch_path) + parse_branch_path = staticmethod(SearchServiceClient.parse_branch_path) + product_path = staticmethod(SearchServiceClient.product_path) + parse_product_path = staticmethod(SearchServiceClient.parse_product_path) + common_billing_account_path = staticmethod( + SearchServiceClient.common_billing_account_path + ) + parse_common_billing_account_path = staticmethod( + SearchServiceClient.parse_common_billing_account_path + ) + common_folder_path = staticmethod(SearchServiceClient.common_folder_path) + parse_common_folder_path = staticmethod( + SearchServiceClient.parse_common_folder_path + ) + common_organization_path = staticmethod( + SearchServiceClient.common_organization_path + ) + parse_common_organization_path = staticmethod( + SearchServiceClient.parse_common_organization_path + ) + common_project_path = staticmethod(SearchServiceClient.common_project_path) + parse_common_project_path = staticmethod( + SearchServiceClient.parse_common_project_path + ) + common_location_path = staticmethod(SearchServiceClient.common_location_path) + parse_common_location_path = staticmethod( + SearchServiceClient.parse_common_location_path + ) + + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials + info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + SearchServiceAsyncClient: The constructed client. + """ + return SearchServiceClient.from_service_account_info.__func__(SearchServiceAsyncClient, info, *args, **kwargs) # type: ignore + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """Creates an instance of this client using the provided credentials + file. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + SearchServiceAsyncClient: The constructed client. + """ + return SearchServiceClient.from_service_account_file.__func__(SearchServiceAsyncClient, filename, *args, **kwargs) # type: ignore + + from_service_account_json = from_service_account_file + + @property + def transport(self) -> SearchServiceTransport: + """Returns the transport used by the client instance. + + Returns: + SearchServiceTransport: The transport used by the client instance. + """ + return self._client.transport + + get_transport_class = functools.partial( + type(SearchServiceClient).get_transport_class, type(SearchServiceClient) + ) + + def __init__( + self, + *, + credentials: ga_credentials.Credentials = None, + transport: Union[str, SearchServiceTransport] = "grpc_asyncio", + client_options: ClientOptions = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + ) -> None: + """Instantiates the search service client. + + Args: + 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. + transport (Union[str, ~.SearchServiceTransport]): The + transport to use. If set to None, a transport is chosen + automatically. + client_options (ClientOptions): Custom options for the client. It + won't take effect if a ``transport`` instance is provided. + (1) The ``api_endpoint`` property can be used to override the + default endpoint provided by the client. GOOGLE_API_USE_MTLS_ENDPOINT + environment variable can also be used to override the endpoint: + "always" (always use the default mTLS endpoint), "never" (always + use the default regular endpoint) and "auto" (auto switch to the + default mTLS endpoint if client certificate is present, this is + the default value). However, the ``api_endpoint`` property takes + precedence if provided. + (2) If GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable + is "true", then the ``client_cert_source`` property can be used + to provide client certificate for mutual TLS transport. If + not provided, the default SSL client certificate will be used if + present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not + set, no client certificate will be used. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. + """ + self._client = SearchServiceClient( + credentials=credentials, + transport=transport, + client_options=client_options, + client_info=client_info, + ) + + async def search( + self, + request: search_service.SearchRequest = None, + *, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> pagers.SearchAsyncPager: + r"""Performs a search. + This feature is only available for users who have Retail + Search enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using + Retail Search. + + Args: + request (:class:`google.cloud.retail_v2.types.SearchRequest`): + The request object. Request message for + [SearchService.Search][google.cloud.retail.v2.SearchService.Search] + method. + 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: + google.cloud.retail_v2.services.search_service.pagers.SearchAsyncPager: + Response message for + [SearchService.Search][google.cloud.retail.v2.SearchService.Search] + method. + + Iterating over this object will yield results and + resolve additional pages automatically. + + """ + # Create or coerce a protobuf request object. + request = search_service.SearchRequest(request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method_async.wrap_method( + self._client._transport.search, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("placement", request.placement),) + ), + ) + + # Send the request. + response = await rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # This method is paged; wrap the response in a pager, which provides + # an `__aiter__` convenience method. + response = pagers.SearchAsyncPager( + method=rpc, request=request, response=response, metadata=metadata, + ) + + # Done; return the response. + return response + + +try: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=pkg_resources.get_distribution("google-cloud-retail",).version, + ) +except pkg_resources.DistributionNotFound: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() + + +__all__ = ("SearchServiceAsyncClient",) diff --git a/google/cloud/retail_v2/services/search_service/client.py b/google/cloud/retail_v2/services/search_service/client.py new file mode 100644 index 00000000..deb8f6c8 --- /dev/null +++ b/google/cloud/retail_v2/services/search_service/client.py @@ -0,0 +1,454 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from collections import OrderedDict +from distutils import util +import os +import re +from typing import Callable, Dict, Optional, Sequence, Tuple, Type, Union +import pkg_resources + +from google.api_core import client_options as client_options_lib # type: ignore +from google.api_core import exceptions as core_exceptions # type: ignore +from google.api_core import gapic_v1 # type: ignore +from google.api_core import retry as retries # type: ignore +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport import mtls # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.auth.exceptions import MutualTLSChannelError # type: ignore +from google.oauth2 import service_account # type: ignore + +from google.cloud.retail_v2.services.search_service import pagers +from google.cloud.retail_v2.types import search_service +from .transports.base import SearchServiceTransport, DEFAULT_CLIENT_INFO +from .transports.grpc import SearchServiceGrpcTransport +from .transports.grpc_asyncio import SearchServiceGrpcAsyncIOTransport + + +class SearchServiceClientMeta(type): + """Metaclass for the SearchService client. + + This provides class-level methods for building and retrieving + support objects (e.g. transport) without polluting the client instance + objects. + """ + + _transport_registry = OrderedDict() # type: Dict[str, Type[SearchServiceTransport]] + _transport_registry["grpc"] = SearchServiceGrpcTransport + _transport_registry["grpc_asyncio"] = SearchServiceGrpcAsyncIOTransport + + def get_transport_class(cls, label: str = None,) -> Type[SearchServiceTransport]: + """Returns an appropriate transport class. + + Args: + label: The name of the desired transport. If none is + provided, then the first transport in the registry is used. + + Returns: + The transport class to use. + """ + # If a specific transport is requested, return that one. + if label: + return cls._transport_registry[label] + + # No transport is requested; return the default (that is, the first one + # in the dictionary). + return next(iter(cls._transport_registry.values())) + + +class SearchServiceClient(metaclass=SearchServiceClientMeta): + """Service for search. + This feature is only available for users who have Retail Search + enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using Retail + Search. + """ + + @staticmethod + def _get_default_mtls_endpoint(api_endpoint): + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + str: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + DEFAULT_ENDPOINT = "retail.googleapis.com" + DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_ENDPOINT + ) + + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials + info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + SearchServiceClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_info(info) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """Creates an instance of this client using the provided credentials + file. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + SearchServiceClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_file(filename) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + + from_service_account_json = from_service_account_file + + @property + def transport(self) -> SearchServiceTransport: + """Returns the transport used by the client instance. + + Returns: + SearchServiceTransport: The transport used by the client + instance. + """ + return self._transport + + @staticmethod + def branch_path(project: str, location: str, catalog: str, branch: str,) -> str: + """Returns a fully-qualified branch string.""" + return "projects/{project}/locations/{location}/catalogs/{catalog}/branches/{branch}".format( + project=project, location=location, catalog=catalog, branch=branch, + ) + + @staticmethod + def parse_branch_path(path: str) -> Dict[str, str]: + """Parses a branch path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/catalogs/(?P.+?)/branches/(?P.+?)$", + path, + ) + return m.groupdict() if m else {} + + @staticmethod + def product_path( + project: str, location: str, catalog: str, branch: str, product: str, + ) -> str: + """Returns a fully-qualified product string.""" + return "projects/{project}/locations/{location}/catalogs/{catalog}/branches/{branch}/products/{product}".format( + project=project, + location=location, + catalog=catalog, + branch=branch, + product=product, + ) + + @staticmethod + def parse_product_path(path: str) -> Dict[str, str]: + """Parses a product path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/catalogs/(?P.+?)/branches/(?P.+?)/products/(?P.+?)$", + path, + ) + return m.groupdict() if m else {} + + @staticmethod + def common_billing_account_path(billing_account: str,) -> str: + """Returns a fully-qualified billing_account string.""" + return "billingAccounts/{billing_account}".format( + billing_account=billing_account, + ) + + @staticmethod + def parse_common_billing_account_path(path: str) -> Dict[str, str]: + """Parse a billing_account path into its component segments.""" + m = re.match(r"^billingAccounts/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_folder_path(folder: str,) -> str: + """Returns a fully-qualified folder string.""" + return "folders/{folder}".format(folder=folder,) + + @staticmethod + def parse_common_folder_path(path: str) -> Dict[str, str]: + """Parse a folder path into its component segments.""" + m = re.match(r"^folders/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_organization_path(organization: str,) -> str: + """Returns a fully-qualified organization string.""" + return "organizations/{organization}".format(organization=organization,) + + @staticmethod + def parse_common_organization_path(path: str) -> Dict[str, str]: + """Parse a organization path into its component segments.""" + m = re.match(r"^organizations/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_project_path(project: str,) -> str: + """Returns a fully-qualified project string.""" + return "projects/{project}".format(project=project,) + + @staticmethod + def parse_common_project_path(path: str) -> Dict[str, str]: + """Parse a project path into its component segments.""" + m = re.match(r"^projects/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_location_path(project: str, location: str,) -> str: + """Returns a fully-qualified location string.""" + return "projects/{project}/locations/{location}".format( + project=project, location=location, + ) + + @staticmethod + def parse_common_location_path(path: str) -> Dict[str, str]: + """Parse a location path into its component segments.""" + m = re.match(r"^projects/(?P.+?)/locations/(?P.+?)$", path) + return m.groupdict() if m else {} + + def __init__( + self, + *, + credentials: Optional[ga_credentials.Credentials] = None, + transport: Union[str, SearchServiceTransport, None] = None, + client_options: Optional[client_options_lib.ClientOptions] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + ) -> None: + """Instantiates the search service client. + + Args: + 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. + transport (Union[str, SearchServiceTransport]): The + transport to use. If set to None, a transport is chosen + automatically. + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. It won't take effect if a ``transport`` instance is provided. + (1) The ``api_endpoint`` property can be used to override the + default endpoint provided by the client. GOOGLE_API_USE_MTLS_ENDPOINT + environment variable can also be used to override the endpoint: + "always" (always use the default mTLS endpoint), "never" (always + use the default regular endpoint) and "auto" (auto switch to the + default mTLS endpoint if client certificate is present, this is + the default value). However, the ``api_endpoint`` property takes + precedence if provided. + (2) If GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable + is "true", then the ``client_cert_source`` property can be used + to provide client certificate for mutual TLS transport. If + not provided, the default SSL client certificate will be used if + present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not + set, no client certificate will be used. + 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're developing + your own client library. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport + creation failed for any reason. + """ + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + if client_options is None: + client_options = client_options_lib.ClientOptions() + + # Create SSL credentials for mutual TLS if needed. + use_client_cert = bool( + util.strtobool(os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false")) + ) + + client_cert_source_func = None + is_mtls = False + if use_client_cert: + if client_options.client_cert_source: + is_mtls = True + client_cert_source_func = client_options.client_cert_source + else: + is_mtls = mtls.has_default_client_cert_source() + if is_mtls: + client_cert_source_func = mtls.default_client_cert_source() + else: + client_cert_source_func = None + + # Figure out which api endpoint to use. + if client_options.api_endpoint is not None: + api_endpoint = client_options.api_endpoint + else: + use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") + if use_mtls_env == "never": + api_endpoint = self.DEFAULT_ENDPOINT + elif use_mtls_env == "always": + api_endpoint = self.DEFAULT_MTLS_ENDPOINT + elif use_mtls_env == "auto": + if is_mtls: + api_endpoint = self.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = self.DEFAULT_ENDPOINT + else: + raise MutualTLSChannelError( + "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted " + "values: never, auto, always" + ) + + # Save or instantiate the transport. + # Ordinarily, we provide the transport, but allowing a custom transport + # instance provides an extensibility point for unusual situations. + if isinstance(transport, SearchServiceTransport): + # transport is a SearchServiceTransport instance. + if credentials or client_options.credentials_file: + raise ValueError( + "When providing a transport instance, " + "provide its credentials directly." + ) + if client_options.scopes: + raise ValueError( + "When providing a transport instance, provide its scopes " + "directly." + ) + self._transport = transport + else: + Transport = type(self).get_transport_class(transport) + self._transport = Transport( + credentials=credentials, + credentials_file=client_options.credentials_file, + host=api_endpoint, + scopes=client_options.scopes, + client_cert_source_for_mtls=client_cert_source_func, + quota_project_id=client_options.quota_project_id, + client_info=client_info, + always_use_jwt_access=( + Transport == type(self).get_transport_class("grpc") + or Transport == type(self).get_transport_class("grpc_asyncio") + ), + ) + + def search( + self, + request: search_service.SearchRequest = None, + *, + retry: retries.Retry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> pagers.SearchPager: + r"""Performs a search. + This feature is only available for users who have Retail + Search enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using + Retail Search. + + Args: + request (google.cloud.retail_v2.types.SearchRequest): + The request object. Request message for + [SearchService.Search][google.cloud.retail.v2.SearchService.Search] + method. + 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: + google.cloud.retail_v2.services.search_service.pagers.SearchPager: + Response message for + [SearchService.Search][google.cloud.retail.v2.SearchService.Search] + method. + + Iterating over this object will yield results and + resolve additional pages automatically. + + """ + # Create or coerce a protobuf request object. + # Minor optimization to avoid making a copy if the user passes + # in a search_service.SearchRequest. + # There's no risk of modifying the input as we've already verified + # there are no flattened fields. + if not isinstance(request, search_service.SearchRequest): + request = search_service.SearchRequest(request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.search] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("placement", request.placement),) + ), + ) + + # Send the request. + response = rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # This method is paged; wrap the response in a pager, which provides + # an `__iter__` convenience method. + response = pagers.SearchPager( + method=rpc, request=request, response=response, metadata=metadata, + ) + + # Done; return the response. + return response + + +try: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=pkg_resources.get_distribution("google-cloud-retail",).version, + ) +except pkg_resources.DistributionNotFound: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() + + +__all__ = ("SearchServiceClient",) diff --git a/google/cloud/retail_v2/services/search_service/pagers.py b/google/cloud/retail_v2/services/search_service/pagers.py new file mode 100644 index 00000000..c13fb69b --- /dev/null +++ b/google/cloud/retail_v2/services/search_service/pagers.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from typing import ( + Any, + AsyncIterable, + Awaitable, + Callable, + Iterable, + Sequence, + Tuple, + Optional, +) + +from google.cloud.retail_v2.types import search_service + + +class SearchPager: + """A pager for iterating through ``search`` requests. + + This class thinly wraps an initial + :class:`google.cloud.retail_v2.types.SearchResponse` object, and + provides an ``__iter__`` method to iterate through its + ``results`` field. + + If there are more pages, the ``__iter__`` method will make additional + ``Search`` requests and continue to iterate + through the ``results`` field on the + corresponding responses. + + All the usual :class:`google.cloud.retail_v2.types.SearchResponse` + attributes are available on the pager. If multiple requests are made, only + the most recent response is retained, and thus used for attribute lookup. + """ + + def __init__( + self, + method: Callable[..., search_service.SearchResponse], + request: search_service.SearchRequest, + response: search_service.SearchResponse, + *, + metadata: Sequence[Tuple[str, str]] = () + ): + """Instantiate the pager. + + Args: + method (Callable): The method that was originally called, and + which instantiated this pager. + request (google.cloud.retail_v2.types.SearchRequest): + The initial request object. + response (google.cloud.retail_v2.types.SearchResponse): + The initial response object. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + """ + self._method = method + self._request = search_service.SearchRequest(request) + self._response = response + self._metadata = metadata + + def __getattr__(self, name: str) -> Any: + return getattr(self._response, name) + + @property + def pages(self) -> Iterable[search_service.SearchResponse]: + yield self._response + while self._response.next_page_token: + self._request.page_token = self._response.next_page_token + self._response = self._method(self._request, metadata=self._metadata) + yield self._response + + def __iter__(self) -> Iterable[search_service.SearchResponse.SearchResult]: + for page in self.pages: + yield from page.results + + def __repr__(self) -> str: + return "{0}<{1!r}>".format(self.__class__.__name__, self._response) + + +class SearchAsyncPager: + """A pager for iterating through ``search`` requests. + + This class thinly wraps an initial + :class:`google.cloud.retail_v2.types.SearchResponse` object, and + provides an ``__aiter__`` method to iterate through its + ``results`` field. + + If there are more pages, the ``__aiter__`` method will make additional + ``Search`` requests and continue to iterate + through the ``results`` field on the + corresponding responses. + + All the usual :class:`google.cloud.retail_v2.types.SearchResponse` + attributes are available on the pager. If multiple requests are made, only + the most recent response is retained, and thus used for attribute lookup. + """ + + def __init__( + self, + method: Callable[..., Awaitable[search_service.SearchResponse]], + request: search_service.SearchRequest, + response: search_service.SearchResponse, + *, + metadata: Sequence[Tuple[str, str]] = () + ): + """Instantiates the pager. + + Args: + method (Callable): The method that was originally called, and + which instantiated this pager. + request (google.cloud.retail_v2.types.SearchRequest): + The initial request object. + response (google.cloud.retail_v2.types.SearchResponse): + The initial response object. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + """ + self._method = method + self._request = search_service.SearchRequest(request) + self._response = response + self._metadata = metadata + + def __getattr__(self, name: str) -> Any: + return getattr(self._response, name) + + @property + async def pages(self) -> AsyncIterable[search_service.SearchResponse]: + yield self._response + while self._response.next_page_token: + self._request.page_token = self._response.next_page_token + self._response = await self._method(self._request, metadata=self._metadata) + yield self._response + + def __aiter__(self) -> AsyncIterable[search_service.SearchResponse.SearchResult]: + async def async_generator(): + async for page in self.pages: + for response in page.results: + yield response + + return async_generator() + + def __repr__(self) -> str: + return "{0}<{1!r}>".format(self.__class__.__name__, self._response) diff --git a/google/cloud/retail_v2/services/search_service/transports/__init__.py b/google/cloud/retail_v2/services/search_service/transports/__init__.py new file mode 100644 index 00000000..2a28e5d5 --- /dev/null +++ b/google/cloud/retail_v2/services/search_service/transports/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from collections import OrderedDict +from typing import Dict, Type + +from .base import SearchServiceTransport +from .grpc import SearchServiceGrpcTransport +from .grpc_asyncio import SearchServiceGrpcAsyncIOTransport + + +# Compile a registry of transports. +_transport_registry = OrderedDict() # type: Dict[str, Type[SearchServiceTransport]] +_transport_registry["grpc"] = SearchServiceGrpcTransport +_transport_registry["grpc_asyncio"] = SearchServiceGrpcAsyncIOTransport + +__all__ = ( + "SearchServiceTransport", + "SearchServiceGrpcTransport", + "SearchServiceGrpcAsyncIOTransport", +) diff --git a/google/cloud/retail_v2/services/search_service/transports/base.py b/google/cloud/retail_v2/services/search_service/transports/base.py new file mode 100644 index 00000000..91afda49 --- /dev/null +++ b/google/cloud/retail_v2/services/search_service/transports/base.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import abc +from typing import Awaitable, Callable, Dict, Optional, Sequence, Union +import packaging.version +import pkg_resources + +import google.auth # type: ignore +import google.api_core # type: ignore +from google.api_core import exceptions as core_exceptions # type: ignore +from google.api_core import gapic_v1 # type: ignore +from google.api_core import retry as retries # type: ignore +from google.auth import credentials as ga_credentials # type: ignore +from google.oauth2 import service_account # type: ignore + +from google.cloud.retail_v2.types import search_service + +try: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=pkg_resources.get_distribution("google-cloud-retail",).version, + ) +except pkg_resources.DistributionNotFound: + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo() + +try: + # google.auth.__version__ was added in 1.26.0 + _GOOGLE_AUTH_VERSION = google.auth.__version__ +except AttributeError: + try: # try pkg_resources if it is available + _GOOGLE_AUTH_VERSION = pkg_resources.get_distribution("google-auth").version + except pkg_resources.DistributionNotFound: # pragma: NO COVER + _GOOGLE_AUTH_VERSION = None + + +class SearchServiceTransport(abc.ABC): + """Abstract transport class for SearchService.""" + + AUTH_SCOPES = ("https://www.googleapis.com/auth/cloud-platform",) + + DEFAULT_HOST: str = "retail.googleapis.com" + + def __init__( + self, + *, + host: str = DEFAULT_HOST, + credentials: ga_credentials.Credentials = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + **kwargs, + ) -> 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 mutually exclusive with credentials. + scopes (Optional[Sequence[str]]): A list of scopes. + 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're developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + """ + # Save the hostname. Default to port 443 (HTTPS) if none is specified. + if ":" not in host: + host += ":443" + self._host = host + + scopes_kwargs = self._get_scopes_kwargs(self._host, scopes) + + # Save the scopes. + self._scopes = scopes + + # If no credentials are provided, then determine the appropriate + # defaults. + if credentials and credentials_file: + raise core_exceptions.DuplicateCredentialArgs( + "'credentials_file' and 'credentials' are mutually exclusive" + ) + + if credentials_file is not None: + credentials, _ = google.auth.load_credentials_from_file( + credentials_file, **scopes_kwargs, quota_project_id=quota_project_id + ) + + elif credentials is None: + credentials, _ = google.auth.default( + **scopes_kwargs, quota_project_id=quota_project_id + ) + + # If the credentials is service account credentials, then always try to use self signed JWT. + if ( + always_use_jwt_access + and isinstance(credentials, service_account.Credentials) + and hasattr(service_account.Credentials, "with_always_use_jwt_access") + ): + credentials = credentials.with_always_use_jwt_access(True) + + # Save the credentials. + self._credentials = credentials + + # TODO(busunkim): This method is in the base transport + # to avoid duplicating code across the transport classes. These functions + # should be deleted once the minimum required versions of google-auth is increased. + + # TODO: Remove this function once google-auth >= 1.25.0 is required + @classmethod + def _get_scopes_kwargs( + cls, host: str, scopes: Optional[Sequence[str]] + ) -> Dict[str, Optional[Sequence[str]]]: + """Returns scopes kwargs to pass to google-auth methods depending on the google-auth version""" + + scopes_kwargs = {} + + if _GOOGLE_AUTH_VERSION and ( + packaging.version.parse(_GOOGLE_AUTH_VERSION) + >= packaging.version.parse("1.25.0") + ): + scopes_kwargs = {"scopes": scopes, "default_scopes": cls.AUTH_SCOPES} + else: + scopes_kwargs = {"scopes": scopes or cls.AUTH_SCOPES} + + return scopes_kwargs + + def _prep_wrapped_messages(self, client_info): + # Precompute the wrapped methods. + self._wrapped_methods = { + self.search: gapic_v1.method.wrap_method( + self.search, default_timeout=None, client_info=client_info, + ), + } + + @property + def search( + self, + ) -> Callable[ + [search_service.SearchRequest], + Union[search_service.SearchResponse, Awaitable[search_service.SearchResponse]], + ]: + raise NotImplementedError() + + +__all__ = ("SearchServiceTransport",) diff --git a/google/cloud/retail_v2/services/search_service/transports/grpc.py b/google/cloud/retail_v2/services/search_service/transports/grpc.py new file mode 100644 index 00000000..ab60cf74 --- /dev/null +++ b/google/cloud/retail_v2/services/search_service/transports/grpc.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import warnings +from typing import Callable, Dict, Optional, Sequence, Tuple, Union + +from google.api_core import grpc_helpers # type: ignore +from google.api_core import gapic_v1 # type: ignore +import google.auth # type: ignore +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore + +import grpc # type: ignore + +from google.cloud.retail_v2.types import search_service +from .base import SearchServiceTransport, DEFAULT_CLIENT_INFO + + +class SearchServiceGrpcTransport(SearchServiceTransport): + """gRPC backend transport for SearchService. + + Service for search. + This feature is only available for users who have Retail Search + enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using Retail + Search. + + 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 protocol buffers over the wire using gRPC (which is built on + top of HTTP/2); the ``grpcio`` package must be installed. + """ + + _stubs: Dict[str, Callable] + + def __init__( + self, + *, + host: str = "retail.googleapis.com", + credentials: ga_credentials.Credentials = None, + credentials_file: str = None, + scopes: Sequence[str] = None, + channel: grpc.Channel = None, + api_mtls_endpoint: str = None, + client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, + ssl_channel_credentials: grpc.ChannelCredentials = None, + client_cert_source_for_mtls: 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, + ) -> 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. + This argument is ignored if ``channel`` is provided. + 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. + channel (Optional[grpc.Channel]): A ``Channel`` instance through + which to make calls. + api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint. + If provided, it overrides the ``host`` argument and tries to create + a mutual TLS channel with client SSL credentials from + ``client_cert_source`` or applicatin default SSL credentials. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): + Deprecated. A callback to provide client SSL certificate bytes and + private key bytes, both in PEM format. It is ignored if + ``api_mtls_endpoint`` is None. + ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials + for grpc channel. It is ignored if ``channel`` is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure mutual TLS channel. It is + ignored if ``channel`` or ``ssl_channel_credentials`` 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're developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport + creation failed for any reason. + google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` + and ``credentials_file`` are passed. + """ + self._grpc_channel = None + self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn("client_cert_source is deprecated", DeprecationWarning) + + if channel: + # Ignore credentials if a channel was passed. + credentials = False + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + self._ssl_channel_credentials = None + + else: + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + self._ssl_channel_credentials = SslCredentials().ssl_credentials + + else: + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + ) + + if not self._grpc_channel: + self._grpc_channel = type(self).create_channel( + self._host, + credentials=self._credentials, + credentials_file=credentials_file, + scopes=self._scopes, + ssl_credentials=self._ssl_channel_credentials, + quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + # Wrap messages. This must be done after self._grpc_channel exists + self._prep_wrapped_messages(client_info) + + @classmethod + def create_channel( + cls, + host: str = "retail.googleapis.com", + credentials: ga_credentials.Credentials = None, + credentials_file: str = None, + scopes: Optional[Sequence[str]] = None, + quota_project_id: Optional[str] = None, + **kwargs, + ) -> grpc.Channel: + """Create and return a gRPC channel object. + Args: + host (Optional[str]): The host for the channel to use. + credentials (Optional[~.Credentials]): The + authorization credentials to attach to requests. These + credentials identify this application to the service. If + none are specified, the client will attempt to ascertain + the credentials from the environment. + credentials_file (Optional[str]): A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is mutually exclusive with credentials. + scopes (Optional[Sequence[str]]): A optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + kwargs (Optional[dict]): Keyword arguments, which are passed to the + channel creation. + Returns: + grpc.Channel: A gRPC channel object. + + Raises: + google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` + and ``credentials_file`` are passed. + """ + + return grpc_helpers.create_channel( + host, + credentials=credentials, + credentials_file=credentials_file, + quota_project_id=quota_project_id, + default_scopes=cls.AUTH_SCOPES, + scopes=scopes, + default_host=cls.DEFAULT_HOST, + **kwargs, + ) + + @property + def grpc_channel(self) -> grpc.Channel: + """Return the channel designed to connect to this service. + """ + return self._grpc_channel + + @property + def search( + self, + ) -> Callable[[search_service.SearchRequest], search_service.SearchResponse]: + r"""Return a callable for the search method over gRPC. + + Performs a search. + This feature is only available for users who have Retail + Search enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using + Retail Search. + + Returns: + Callable[[~.SearchRequest], + ~.SearchResponse]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "search" not in self._stubs: + self._stubs["search"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.SearchService/Search", + request_serializer=search_service.SearchRequest.serialize, + response_deserializer=search_service.SearchResponse.deserialize, + ) + return self._stubs["search"] + + +__all__ = ("SearchServiceGrpcTransport",) diff --git a/google/cloud/retail_v2/services/search_service/transports/grpc_asyncio.py b/google/cloud/retail_v2/services/search_service/transports/grpc_asyncio.py new file mode 100644 index 00000000..81d4e709 --- /dev/null +++ b/google/cloud/retail_v2/services/search_service/transports/grpc_asyncio.py @@ -0,0 +1,268 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import warnings +from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union + +from google.api_core import gapic_v1 # type: ignore +from google.api_core import grpc_helpers_async # type: ignore +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +import packaging.version + +import grpc # type: ignore +from grpc.experimental import aio # type: ignore + +from google.cloud.retail_v2.types import search_service +from .base import SearchServiceTransport, DEFAULT_CLIENT_INFO +from .grpc import SearchServiceGrpcTransport + + +class SearchServiceGrpcAsyncIOTransport(SearchServiceTransport): + """gRPC AsyncIO backend transport for SearchService. + + Service for search. + This feature is only available for users who have Retail Search + enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using Retail + Search. + + 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 protocol buffers over the wire using gRPC (which is built on + top of HTTP/2); the ``grpcio`` package must be installed. + """ + + _grpc_channel: aio.Channel + _stubs: Dict[str, Callable] = {} + + @classmethod + def create_channel( + cls, + host: str = "retail.googleapis.com", + credentials: ga_credentials.Credentials = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + quota_project_id: Optional[str] = None, + **kwargs, + ) -> aio.Channel: + """Create and return a gRPC AsyncIO channel object. + Args: + host (Optional[str]): The host for the channel to use. + credentials (Optional[~.Credentials]): The + authorization credentials to attach to requests. These + credentials identify this application to the service. If + none are specified, the client will attempt to ascertain + the credentials from the environment. + credentials_file (Optional[str]): A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. + scopes (Optional[Sequence[str]]): A optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + kwargs (Optional[dict]): Keyword arguments, which are passed to the + channel creation. + Returns: + aio.Channel: A gRPC AsyncIO channel object. + """ + + return grpc_helpers_async.create_channel( + host, + credentials=credentials, + credentials_file=credentials_file, + quota_project_id=quota_project_id, + default_scopes=cls.AUTH_SCOPES, + scopes=scopes, + default_host=cls.DEFAULT_HOST, + **kwargs, + ) + + def __init__( + self, + *, + host: str = "retail.googleapis.com", + credentials: ga_credentials.Credentials = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + channel: aio.Channel = None, + api_mtls_endpoint: str = None, + client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, + ssl_channel_credentials: grpc.ChannelCredentials = None, + client_cert_source_for_mtls: Callable[[], Tuple[bytes, bytes]] = None, + quota_project_id=None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + ) -> 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. + This argument is ignored if ``channel`` is provided. + 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 optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. + channel (Optional[aio.Channel]): A ``Channel`` instance through + which to make calls. + api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint. + If provided, it overrides the ``host`` argument and tries to create + a mutual TLS channel with client SSL credentials from + ``client_cert_source`` or applicatin default SSL credentials. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): + Deprecated. A callback to provide client SSL certificate bytes and + private key bytes, both in PEM format. It is ignored if + ``api_mtls_endpoint`` is None. + ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials + for grpc channel. It is ignored if ``channel`` is provided. + client_cert_source_for_mtls (Optional[Callable[[], Tuple[bytes, bytes]]]): + A callback to provide client certificate bytes and private key bytes, + both in PEM format. It is used to configure mutual TLS channel. It is + ignored if ``channel`` or ``ssl_channel_credentials`` 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're developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. + google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials`` + and ``credentials_file`` are passed. + """ + self._grpc_channel = None + self._ssl_channel_credentials = ssl_channel_credentials + self._stubs: Dict[str, Callable] = {} + + if api_mtls_endpoint: + warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) + if client_cert_source: + warnings.warn("client_cert_source is deprecated", DeprecationWarning) + + if channel: + # Ignore credentials if a channel was passed. + credentials = False + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + self._ssl_channel_credentials = None + else: + if api_mtls_endpoint: + host = api_mtls_endpoint + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + self._ssl_channel_credentials = SslCredentials().ssl_credentials + + else: + if client_cert_source_for_mtls and not ssl_channel_credentials: + cert, key = client_cert_source_for_mtls() + self._ssl_channel_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + + # The base transport sets the host, credentials and scopes + super().__init__( + host=host, + credentials=credentials, + credentials_file=credentials_file, + scopes=scopes, + quota_project_id=quota_project_id, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + ) + + if not self._grpc_channel: + self._grpc_channel = type(self).create_channel( + self._host, + credentials=self._credentials, + credentials_file=credentials_file, + scopes=self._scopes, + ssl_credentials=self._ssl_channel_credentials, + quota_project_id=quota_project_id, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + # Wrap messages. This must be done after self._grpc_channel exists + self._prep_wrapped_messages(client_info) + + @property + def grpc_channel(self) -> aio.Channel: + """Create the channel designed to connect to this service. + + This property caches on the instance; repeated calls return + the same channel. + """ + # Return the channel from cache. + return self._grpc_channel + + @property + def search( + self, + ) -> Callable[ + [search_service.SearchRequest], Awaitable[search_service.SearchResponse] + ]: + r"""Return a callable for the search method over gRPC. + + Performs a search. + This feature is only available for users who have Retail + Search enabled. Contact Retail Support (retail-search- + support@google.com) if you are interested in using + Retail Search. + + Returns: + Callable[[~.SearchRequest], + Awaitable[~.SearchResponse]]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "search" not in self._stubs: + self._stubs["search"] = self.grpc_channel.unary_unary( + "/google.cloud.retail.v2.SearchService/Search", + request_serializer=search_service.SearchRequest.serialize, + response_deserializer=search_service.SearchResponse.deserialize, + ) + return self._stubs["search"] + + +__all__ = ("SearchServiceGrpcAsyncIOTransport",) diff --git a/google/cloud/retail_v2/services/user_event_service/async_client.py b/google/cloud/retail_v2/services/user_event_service/async_client.py index 8d973740..118181f8 100644 --- a/google/cloud/retail_v2/services/user_event_service/async_client.py +++ b/google/cloud/retail_v2/services/user_event_service/async_client.py @@ -51,6 +51,8 @@ class UserEventServiceAsyncClient: DEFAULT_ENDPOINT = UserEventServiceClient.DEFAULT_ENDPOINT DEFAULT_MTLS_ENDPOINT = UserEventServiceClient.DEFAULT_MTLS_ENDPOINT + catalog_path = staticmethod(UserEventServiceClient.catalog_path) + parse_catalog_path = staticmethod(UserEventServiceClient.parse_catalog_path) product_path = staticmethod(UserEventServiceClient.product_path) parse_product_path = staticmethod(UserEventServiceClient.parse_product_path) common_billing_account_path = staticmethod( diff --git a/google/cloud/retail_v2/services/user_event_service/client.py b/google/cloud/retail_v2/services/user_event_service/client.py index 0fd64873..4bbe726a 100644 --- a/google/cloud/retail_v2/services/user_event_service/client.py +++ b/google/cloud/retail_v2/services/user_event_service/client.py @@ -165,6 +165,22 @@ def transport(self) -> UserEventServiceTransport: """ return self._transport + @staticmethod + def catalog_path(project: str, location: str, catalog: str,) -> str: + """Returns a fully-qualified catalog string.""" + return "projects/{project}/locations/{location}/catalogs/{catalog}".format( + project=project, location=location, catalog=catalog, + ) + + @staticmethod + def parse_catalog_path(path: str) -> Dict[str, str]: + """Parses a catalog path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/catalogs/(?P.+?)$", + path, + ) + return m.groupdict() if m else {} + @staticmethod def product_path( project: str, location: str, catalog: str, branch: str, product: str, diff --git a/google/cloud/retail_v2/types/__init__.py b/google/cloud/retail_v2/types/__init__.py index 7c105452..ff9672a7 100644 --- a/google/cloud/retail_v2/types/__init__.py +++ b/google/cloud/retail_v2/types/__init__.py @@ -18,19 +18,35 @@ ProductLevelConfig, ) from .catalog_service import ( + GetDefaultBranchRequest, + GetDefaultBranchResponse, ListCatalogsRequest, ListCatalogsResponse, + SetDefaultBranchRequest, UpdateCatalogRequest, ) from .common import ( + Audience, + ColorInfo, CustomAttribute, + FulfillmentInfo, Image, + Interval, PriceInfo, + Promotion, + Rating, UserInfo, ) +from .completion_service import ( + CompleteQueryRequest, + CompleteQueryResponse, +) from .import_config import ( BigQuerySource, + CompletionDataInputConfig, GcsSource, + ImportCompletionDataRequest, + ImportCompletionDataResponse, ImportErrorsConfig, ImportMetadata, ImportProductsRequest, @@ -49,9 +65,20 @@ ) from .product import Product from .product_service import ( + AddFulfillmentPlacesMetadata, + AddFulfillmentPlacesRequest, + AddFulfillmentPlacesResponse, CreateProductRequest, DeleteProductRequest, GetProductRequest, + ListProductsRequest, + ListProductsResponse, + RemoveFulfillmentPlacesMetadata, + RemoveFulfillmentPlacesRequest, + RemoveFulfillmentPlacesResponse, + SetInventoryMetadata, + SetInventoryRequest, + SetInventoryResponse, UpdateProductRequest, ) from .purge_config import ( @@ -59,7 +86,12 @@ PurgeUserEventsRequest, PurgeUserEventsResponse, ) +from .search_service import ( + SearchRequest, + SearchResponse, +) from .user_event import ( + CompletionDetail, ProductDetail, PurchaseTransaction, UserEvent, @@ -75,15 +107,29 @@ __all__ = ( "Catalog", "ProductLevelConfig", + "GetDefaultBranchRequest", + "GetDefaultBranchResponse", "ListCatalogsRequest", "ListCatalogsResponse", + "SetDefaultBranchRequest", "UpdateCatalogRequest", + "Audience", + "ColorInfo", "CustomAttribute", + "FulfillmentInfo", "Image", + "Interval", "PriceInfo", + "Promotion", + "Rating", "UserInfo", + "CompleteQueryRequest", + "CompleteQueryResponse", "BigQuerySource", + "CompletionDataInputConfig", "GcsSource", + "ImportCompletionDataRequest", + "ImportCompletionDataResponse", "ImportErrorsConfig", "ImportMetadata", "ImportProductsRequest", @@ -98,13 +144,27 @@ "PredictRequest", "PredictResponse", "Product", + "AddFulfillmentPlacesMetadata", + "AddFulfillmentPlacesRequest", + "AddFulfillmentPlacesResponse", "CreateProductRequest", "DeleteProductRequest", "GetProductRequest", + "ListProductsRequest", + "ListProductsResponse", + "RemoveFulfillmentPlacesMetadata", + "RemoveFulfillmentPlacesRequest", + "RemoveFulfillmentPlacesResponse", + "SetInventoryMetadata", + "SetInventoryRequest", + "SetInventoryResponse", "UpdateProductRequest", "PurgeMetadata", "PurgeUserEventsRequest", "PurgeUserEventsResponse", + "SearchRequest", + "SearchResponse", + "CompletionDetail", "ProductDetail", "PurchaseTransaction", "UserEvent", diff --git a/google/cloud/retail_v2/types/catalog.py b/google/cloud/retail_v2/types/catalog.py index d4557adc..a0d42016 100644 --- a/google/cloud/retail_v2/types/catalog.py +++ b/google/cloud/retail_v2/types/catalog.py @@ -51,8 +51,8 @@ class ProductLevelConfig(proto.Message): [merchant_center_product_id_field][google.cloud.retail.v2.ProductLevelConfig.merchant_center_product_id_field] is ``itemGroupId``, an INVALID_ARGUMENT error is returned. - See `Using catalog - levels `__ + See `Using product + levels `__ for more details. merchant_center_product_id_field (str): Which field of `Merchant Center @@ -75,8 +75,8 @@ class ProductLevelConfig(proto.Message): [ingestion_product_type][google.cloud.retail.v2.ProductLevelConfig.ingestion_product_type] is ``variant``, an INVALID_ARGUMENT error is returned. - See `Using catalog - levels `__ + See `Using product + levels `__ for more details. """ diff --git a/google/cloud/retail_v2/types/catalog_service.py b/google/cloud/retail_v2/types/catalog_service.py index 66151d09..4611d996 100644 --- a/google/cloud/retail_v2/types/catalog_service.py +++ b/google/cloud/retail_v2/types/catalog_service.py @@ -17,11 +17,19 @@ from google.cloud.retail_v2.types import catalog as gcr_catalog from google.protobuf import field_mask_pb2 # type: ignore +from google.protobuf import timestamp_pb2 # type: ignore __protobuf__ = proto.module( package="google.cloud.retail.v2", - manifest={"ListCatalogsRequest", "ListCatalogsResponse", "UpdateCatalogRequest",}, + manifest={ + "ListCatalogsRequest", + "ListCatalogsResponse", + "UpdateCatalogRequest", + "SetDefaultBranchRequest", + "GetDefaultBranchRequest", + "GetDefaultBranchResponse", + }, ) @@ -109,11 +117,7 @@ class UpdateCatalogRequest(proto.Message): does not exist, a NOT_FOUND error is returned. update_mask (google.protobuf.field_mask_pb2.FieldMask): Indicates which fields in the provided - [Catalog][google.cloud.retail.v2.Catalog] to update. If not - set, will only update the - [Catalog.product_level_config][google.cloud.retail.v2.Catalog.product_level_config] - field, which is also the only currently supported field to - update. + [Catalog][google.cloud.retail.v2.Catalog] to update. If an unsupported or unknown field is provided, an INVALID_ARGUMENT error is returned. @@ -125,4 +129,64 @@ class UpdateCatalogRequest(proto.Message): ) +class SetDefaultBranchRequest(proto.Message): + r"""Request message to set a specified branch as new default_branch. + Attributes: + catalog (str): + Full resource name of the catalog, such as + ``projects/*/locations/global/catalogs/default_catalog``. + branch_id (str): + The final component of the resource name of a branch. + + This field must be one of "0", "1" or "2". Otherwise, an + INVALID_ARGUMENT error is returned. + note (str): + Some note on this request, this can be retrieved by + [CatalogService.GetDefaultBranch][google.cloud.retail.v2.CatalogService.GetDefaultBranch] + before next valid default branch set occurs. + + This field must be a UTF-8 encoded string with a length + limit of 1,000 characters. Otherwise, an INVALID_ARGUMENT + error is returned. + """ + + catalog = proto.Field(proto.STRING, number=1,) + branch_id = proto.Field(proto.STRING, number=2,) + note = proto.Field(proto.STRING, number=3,) + + +class GetDefaultBranchRequest(proto.Message): + r"""Request message to show which branch is currently the default + branch. + + Attributes: + catalog (str): + The parent catalog resource name, such as + ``projects/*/locations/global/catalogs/default_catalog``. + """ + + catalog = proto.Field(proto.STRING, number=1,) + + +class GetDefaultBranchResponse(proto.Message): + r"""Response message of + [CatalogService.GetDefaultBranch][google.cloud.retail.v2.CatalogService.GetDefaultBranch]. + + Attributes: + branch (str): + Full resource name of the branch id currently + set as default branch. + set_time (google.protobuf.timestamp_pb2.Timestamp): + The time when this branch is set to default. + note (str): + This corresponds to + [SetDefaultBranchRequest.note][google.cloud.retail.v2.SetDefaultBranchRequest.note] + field, when this branch was set as default. + """ + + branch = proto.Field(proto.STRING, number=1,) + set_time = proto.Field(proto.MESSAGE, number=2, message=timestamp_pb2.Timestamp,) + note = proto.Field(proto.STRING, number=3,) + + __all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/retail_v2/types/common.py b/google/cloud/retail_v2/types/common.py index 092508b7..b0584707 100644 --- a/google/cloud/retail_v2/types/common.py +++ b/google/cloud/retail_v2/types/common.py @@ -15,16 +15,110 @@ # import proto # type: ignore +from google.protobuf import timestamp_pb2 # type: ignore + __protobuf__ = proto.module( package="google.cloud.retail.v2", - manifest={"CustomAttribute", "Image", "PriceInfo", "UserInfo",}, + manifest={ + "Audience", + "ColorInfo", + "CustomAttribute", + "FulfillmentInfo", + "Image", + "Interval", + "PriceInfo", + "Rating", + "UserInfo", + "Promotion", + }, ) +class Audience(proto.Message): + r"""An intended audience of the + [Product][google.cloud.retail.v2.Product] for whom it's sold. + + Attributes: + genders (Sequence[str]): + The genders of the audience. Strongly encouraged to use the + standard values: "male", "female", "unisex". + + At most 5 values are allowed. Each value must be a UTF-8 + encoded string with a length limit of 128 characters. + Otherwise, an INVALID_ARGUMENT error is returned. + + Google Merchant Center property + `gender `__. + Schema.org property + `Product.audience.suggestedGender `__. + age_groups (Sequence[str]): + The age groups of the audience. Strongly encouraged to use + the standard values: "newborn" (up to 3 months old), + "infant" (3–12 months old), "toddler" (1–5 years old), + "kids" (5–13 years old), "adult" (typically teens or older). + + At most 5 values are allowed. Each value must be a UTF-8 + encoded string with a length limit of 128 characters. + Otherwise, an INVALID_ARGUMENT error is returned. + + Google Merchant Center property + `age_group `__. + Schema.org property + `Product.audience.suggestedMinAge `__ + and + `Product.audience.suggestedMaxAge `__. + """ + + genders = proto.RepeatedField(proto.STRING, number=1,) + age_groups = proto.RepeatedField(proto.STRING, number=2,) + + +class ColorInfo(proto.Message): + r"""The color information of a + [Product][google.cloud.retail.v2.Product]. + + Attributes: + color_families (Sequence[str]): + The standard color families. Strongly recommended to use the + following standard color groups: "Red", "Pink", "Orange", + "Yellow", "Purple", "Green", "Cyan", "Blue", "Brown", + "White", "Gray", "Black" and "Mixed". Normally it is + expected to have only 1 color family. May consider using + single "Mixed" instead of multiple values. + + A maximum of 5 values are allowed. Each value must be a + UTF-8 encoded string with a length limit of 128 characters. + Otherwise, an INVALID_ARGUMENT error is returned. + + Google Merchant Center property + `color `__. + Schema.org property + `Product.color `__. + colors (Sequence[str]): + The color display names, which may be different from + standard color family names, such as the color aliases used + in the website frontend. Normally it is expected to have + only 1 color. May consider using single "Mixed" instead of + multiple values. + + A maximum of 5 colors are allowed. Each value must be a + UTF-8 encoded string with a length limit of 128 characters. + Otherwise, an INVALID_ARGUMENT error is returned. + + Google Merchant Center property + `color `__. + Schema.org property + `Product.color `__. + """ + + color_families = proto.RepeatedField(proto.STRING, number=1,) + colors = proto.RepeatedField(proto.STRING, number=2,) + + class CustomAttribute(proto.Message): r"""A custom attribute that is not explicitly modeled in - [Product][google.cloud.retail.v2.Product]]. + [Product][google.cloud.retail.v2.Product]. Attributes: text (Sequence[str]): @@ -53,10 +147,80 @@ class CustomAttribute(proto.Message): [numbers][google.cloud.retail.v2.CustomAttribute.numbers] should be set. Otherwise, an INVALID_ARGUMENT error is returned. + searchable (bool): + If true, custom attribute values are searchable by text + queries in + [SearchService.Search][google.cloud.retail.v2.SearchService.Search]. + + This field is ignored in a + [UserEvent][google.cloud.retail.v2.UserEvent]. + + Only set if type + [text][google.cloud.retail.v2.CustomAttribute.text] is set. + Otherwise, a INVALID_ARGUMENT error is returned. + indexable (bool): + If true, custom attribute values are indexed, so that it can + be filtered, faceted or boosted in + [SearchService.Search][google.cloud.retail.v2.SearchService.Search]. + + This field is ignored in a + [UserEvent][google.cloud.retail.v2.UserEvent]. + + See + [SearchRequest.filter][google.cloud.retail.v2.SearchRequest.filter], + [SearchRequest.facet_specs][google.cloud.retail.v2.SearchRequest.facet_specs] + and + [SearchRequest.boost_spec][google.cloud.retail.v2.SearchRequest.boost_spec] + for more details. """ text = proto.RepeatedField(proto.STRING, number=1,) numbers = proto.RepeatedField(proto.DOUBLE, number=2,) + searchable = proto.Field(proto.BOOL, number=3, optional=True,) + indexable = proto.Field(proto.BOOL, number=4, optional=True,) + + +class FulfillmentInfo(proto.Message): + r"""Fulfillment information, such as the store IDs for in-store + pickup or region IDs for different shipping methods. + + Attributes: + type_ (str): + The fulfillment type, including commonly used types (such as + pickup in store and same day delivery), and custom types. + Customers have to map custom types to their display names + before rendering UI. + + Supported values: + + - "pickup-in-store" + - "ship-to-store" + - "same-day-delivery" + - "next-day-delivery" + - "custom-type-1" + - "custom-type-2" + - "custom-type-3" + - "custom-type-4" + - "custom-type-5" + + If this field is set to an invalid value other than these, + an INVALID_ARGUMENT error is returned. + place_ids (Sequence[str]): + The IDs for this + [type][google.cloud.retail.v2.FulfillmentInfo.type], such as + the store IDs for + [FulfillmentInfo.type.pickup-in-store][google.cloud.retail.v2.FulfillmentInfo.type] + or the region IDs for + [FulfillmentInfo.type.same-day-delivery][google.cloud.retail.v2.FulfillmentInfo.type]. + + A maximum of 2000 values are allowed. Each value must be a + string with a length limit of 10 characters, matching the + pattern [a-zA-Z0-9\_-]+, such as "store1" or "REGION-2". + Otherwise, an INVALID_ARGUMENT error is returned. + """ + + type_ = proto.Field(proto.STRING, number=1,) + place_ids = proto.RepeatedField(proto.STRING, number=2,) class Image(proto.Message): @@ -90,6 +254,25 @@ class Image(proto.Message): width = proto.Field(proto.INT32, number=3,) +class Interval(proto.Message): + r"""A floating point interval. + Attributes: + minimum (float): + Inclusive lower bound. + exclusive_minimum (float): + Exclusive lower bound. + maximum (float): + Inclusive upper bound. + exclusive_maximum (float): + Exclusive upper bound. + """ + + minimum = proto.Field(proto.DOUBLE, number=1, oneof="min",) + exclusive_minimum = proto.Field(proto.DOUBLE, number=2, oneof="min",) + maximum = proto.Field(proto.DOUBLE, number=3, oneof="max",) + exclusive_maximum = proto.Field(proto.DOUBLE, number=4, oneof="max",) + + class PriceInfo(proto.Message): r"""The price information of a [Product][google.cloud.retail.v2.Product]. @@ -101,6 +284,14 @@ class PriceInfo(proto.Message): If this field is an unrecognizable currency code, an INVALID_ARGUMENT error is returned. + + The + [Product.Type.VARIANT][google.cloud.retail.v2.Product.Type.VARIANT] + [Product][google.cloud.retail.v2.Product]s with the same + [Product.primary_product_id][google.cloud.retail.v2.Product.primary_product_id] + must share the same + [currency_code][google.cloud.retail.v2.PriceInfo.currency_code]. + Otherwise, a FAILED_PRECONDITION error is returned. price (float): Price of the product. @@ -121,12 +312,127 @@ class PriceInfo(proto.Message): Google Merchant Center property `cost_of_goods_sold `__. + price_effective_time (google.protobuf.timestamp_pb2.Timestamp): + The timestamp when the + [price][google.cloud.retail.v2.PriceInfo.price] starts to be + effective. This can be set as a future timestamp, and the + [price][google.cloud.retail.v2.PriceInfo.price] is only used + for search after + [price_effective_time][google.cloud.retail.v2.PriceInfo.price_effective_time]. + If so, the + [original_price][google.cloud.retail.v2.PriceInfo.original_price] + must be set and + [original_price][google.cloud.retail.v2.PriceInfo.original_price] + is used before + [price_effective_time][google.cloud.retail.v2.PriceInfo.price_effective_time]. + + Do not set if + [price][google.cloud.retail.v2.PriceInfo.price] is always + effective because it will cause additional latency during + search. + price_expire_time (google.protobuf.timestamp_pb2.Timestamp): + The timestamp when the + [price][google.cloud.retail.v2.PriceInfo.price] stops to be + effective. The + [price][google.cloud.retail.v2.PriceInfo.price] is used for + search before + [price_expire_time][google.cloud.retail.v2.PriceInfo.price_expire_time]. + If this field is set, the + [original_price][google.cloud.retail.v2.PriceInfo.original_price] + must be set and + [original_price][google.cloud.retail.v2.PriceInfo.original_price] + is used after + [price_expire_time][google.cloud.retail.v2.PriceInfo.price_expire_time]. + + Do not set if + [price][google.cloud.retail.v2.PriceInfo.price] is always + effective because it will cause additional latency during + search. + price_range (google.cloud.retail_v2.types.PriceInfo.PriceRange): + Output only. The price range of all the child + [Product.Type.VARIANT][google.cloud.retail.v2.Product.Type.VARIANT] + [Product][google.cloud.retail.v2.Product]s grouped together + on the + [Product.Type.PRIMARY][google.cloud.retail.v2.Product.Type.PRIMARY] + [Product][google.cloud.retail.v2.Product]. Only populated + for + [Product.Type.PRIMARY][google.cloud.retail.v2.Product.Type.PRIMARY] + [Product][google.cloud.retail.v2.Product]s. + + Note: This field is OUTPUT_ONLY for + [ProductService.GetProduct][google.cloud.retail.v2.ProductService.GetProduct]. + Do not set this field in API requests. """ + class PriceRange(proto.Message): + r"""The price range of all + [variant][google.cloud.retail.v2.Product.Type.VARIANT] + [Product][google.cloud.retail.v2.Product] having the same + [Product.primary_product_id][google.cloud.retail.v2.Product.primary_product_id]. + + Attributes: + price (google.cloud.retail_v2.types.Interval): + The inclusive + [Product.pricing_info.price][google.cloud.retail.v2.PriceInfo.price] + interval of all + [variant][google.cloud.retail.v2.Product.Type.VARIANT] + [Product][google.cloud.retail.v2.Product] having the same + [Product.primary_product_id][google.cloud.retail.v2.Product.primary_product_id]. + original_price (google.cloud.retail_v2.types.Interval): + The inclusive + [Product.pricing_info.original_price][google.cloud.retail.v2.PriceInfo.original_price] + internal of all + [variant][google.cloud.retail.v2.Product.Type.VARIANT] + [Product][google.cloud.retail.v2.Product] having the same + [Product.primary_product_id][google.cloud.retail.v2.Product.primary_product_id]. + """ + + price = proto.Field(proto.MESSAGE, number=1, message="Interval",) + original_price = proto.Field(proto.MESSAGE, number=2, message="Interval",) + currency_code = proto.Field(proto.STRING, number=1,) price = proto.Field(proto.FLOAT, number=2,) original_price = proto.Field(proto.FLOAT, number=3,) cost = proto.Field(proto.FLOAT, number=4,) + price_effective_time = proto.Field( + proto.MESSAGE, number=5, message=timestamp_pb2.Timestamp, + ) + price_expire_time = proto.Field( + proto.MESSAGE, number=6, message=timestamp_pb2.Timestamp, + ) + price_range = proto.Field(proto.MESSAGE, number=7, message=PriceRange,) + + +class Rating(proto.Message): + r"""The rating of a [Product][google.cloud.retail.v2.Product]. + Attributes: + rating_count (int): + The total number of ratings. This value is independent of + the value of + [rating_histogram][google.cloud.retail.v2.Rating.rating_histogram]. + + This value must be nonnegative. Otherwise, an + INVALID_ARGUMENT error is returned. + average_rating (float): + The average rating of the + [Product][google.cloud.retail.v2.Product]. + + The rating is scaled at 1-5. Otherwise, an INVALID_ARGUMENT + error is returned. + rating_histogram (Sequence[int]): + List of rating counts per rating value (index = rating - 1). + The list is empty if there is no rating. If the list is + non-empty, its size is always 5. Otherwise, an + INVALID_ARGUMENT error is returned. + + For example, [41, 14, 13, 47, 303]. It means that the + [Product][google.cloud.retail.v2.Product] got 41 ratings + with 1 star, 14 ratings with 2 star, and so on. + """ + + rating_count = proto.Field(proto.INT32, number=1,) + average_rating = proto.Field(proto.FLOAT, number=2,) + rating_histogram = proto.RepeatedField(proto.INT32, number=3,) class UserInfo(proto.Message): @@ -140,8 +446,10 @@ class UserInfo(proto.Message): of 128 characters. Otherwise, an INVALID_ARGUMENT error is returned. ip_address (str): - The end user's IP address. This field is used to extract - location information for personalization. + The end user's IP address. Required for getting + [SearchResponse.sponsored_results][google.cloud.retail.v2.SearchResponse.sponsored_results]. + This field is used to extract location information for + personalization. This field must be either an IPv4 address (e.g. "104.133.9.80") or an IPv6 address (e.g. @@ -154,7 +462,9 @@ class UserInfo(proto.Message): [direct_user_request][google.cloud.retail.v2.UserInfo.direct_user_request] is set. user_agent (str): - User agent as included in the HTTP header. + User agent as included in the HTTP header. Required for + getting + [SearchResponse.sponsored_results][google.cloud.retail.v2.SearchResponse.sponsored_results]. The field must be a UTF-8 encoded string with a length limit of 1,000 characters. Otherwise, an INVALID_ARGUMENT error is @@ -186,4 +496,23 @@ class UserInfo(proto.Message): direct_user_request = proto.Field(proto.BOOL, number=4,) +class Promotion(proto.Message): + r"""Promotion information. + Attributes: + promotion_id (str): + ID of the promotion. For example, "free gift". + + The value value must be a UTF-8 encoded string with a length + limit of 128 characters, and match the pattern: + [a-zA-Z][a-zA-Z0-9\_]*. For example, id0LikeThis or + ID_1_LIKE_THIS. Otherwise, an INVALID_ARGUMENT error is + returned. + + Google Merchant Center property + `promotion `__. + """ + + promotion_id = proto.Field(proto.STRING, number=1,) + + __all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/retail_v2/types/completion_service.py b/google/cloud/retail_v2/types/completion_service.py new file mode 100644 index 00000000..a28dac47 --- /dev/null +++ b/google/cloud/retail_v2/types/completion_service.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import proto # type: ignore + +from google.cloud.retail_v2.types import common + + +__protobuf__ = proto.module( + package="google.cloud.retail.v2", + manifest={"CompleteQueryRequest", "CompleteQueryResponse",}, +) + + +class CompleteQueryRequest(proto.Message): + r"""Auto-complete parameters. + Attributes: + catalog (str): + Required. Catalog for which the completion is performed. + + Full resource name of catalog, such as + ``projects/*/locations/global/catalogs/default_catalog``. + query (str): + Required. The query used to generate + suggestions. + The maximum number of allowed characters is 255. + visitor_id (str): + A unique identifier for tracking visitors. For example, this + could be implemented with an HTTP cookie, which should be + able to uniquely identify a visitor on a single device. This + unique identifier should not change if the visitor logs in + or out of the website. + + The field must be a UTF-8 encoded string with a length limit + of 128 characters. Otherwise, an INVALID_ARGUMENT error is + returned. + language_codes (Sequence[str]): + The list of languages of the query. This is the BCP-47 + language code, such as "en-US" or "sr-Latn". For more + information, see `Tags for Identifying + Languages `__. + + The maximum number of allowed characters is 255. Only + "en-US" is currently supported. + device_type (str): + The device type context for completion suggestions. It is + useful to apply different suggestions on different device + types, e.g. DESKTOP, MOBILE. If it is empty, the suggestions + are across all device types. + + Supported formats: + + - UNKNOWN_DEVICE_TYPE + + - DESKTOP + + - MOBILE + + - A customized string starts with OTHER\_, e.g. + OTHER_IPHONE. + dataset (str): + Determines which dataset to use for fetching completion. + "user-data" will use the imported dataset through + [CompletionService.ImportCompletionData][google.cloud.retail.v2.CompletionService.ImportCompletionData]. + "cloud-retail" will use the dataset generated by cloud + retail based on user events. If leave empty, it will use the + "user-data". + + Current supported values: + + - user-data + + - cloud-retail This option is not automatically enabled. + Before using cloud-retail, contact + retail-search-support@google.com first. + max_suggestions (int): + Completion max suggestions. + The maximum allowed max suggestions is 20. The + default value is 20. + """ + + catalog = proto.Field(proto.STRING, number=1,) + query = proto.Field(proto.STRING, number=2,) + visitor_id = proto.Field(proto.STRING, number=7,) + language_codes = proto.RepeatedField(proto.STRING, number=3,) + device_type = proto.Field(proto.STRING, number=4,) + dataset = proto.Field(proto.STRING, number=6,) + max_suggestions = proto.Field(proto.INT32, number=5,) + + +class CompleteQueryResponse(proto.Message): + r"""Response of the auto-complete query. + Attributes: + completion_results (Sequence[google.cloud.retail_v2.types.CompleteQueryResponse.CompletionResult]): + Results of the matching suggestions. The + result list is ordered and the first result is + top suggestion. + attribution_token (str): + A unique complete token. This should be included in the + [SearchRequest][google.cloud.retail.v2.SearchRequest] + resulting from this completion, which enables accurate + attribution of complete model performance. + recent_search_results (Sequence[google.cloud.retail_v2.types.CompleteQueryResponse.RecentSearchResult]): + Matched recent searches of this user. This field is a + restricted feature. Contact Retail Support + (retail-search-support@google.com) if you are interested in + enabling it. + + This feature is only available when + [CompleteQueryRequest.visitor_id][google.cloud.retail.v2.CompleteQueryRequest.visitor_id] + field is set and + [UserEvent][google.cloud.retail.v2.UserEvent] is imported. + The recent searches satisfy the follow rules: + + - They are ordered from latest to oldest. + - They are matched with + [CompleteQueryRequest.query][google.cloud.retail.v2.CompleteQueryRequest.query] + case insensitively. + - They are transformed to lower cases. + - They are UTF-8 safe. + + Recent searches are deduplicated. More recent searches will + be reserved when duplication happens. + """ + + class CompletionResult(proto.Message): + r"""Resource that represents completion results. + Attributes: + suggestion (str): + The suggestion for the query. + attributes (Sequence[google.cloud.retail_v2.types.CompleteQueryResponse.CompletionResult.AttributesEntry]): + Additional custom attributes ingested through + BigQuery. + """ + + suggestion = proto.Field(proto.STRING, number=1,) + attributes = proto.MapField( + proto.STRING, proto.MESSAGE, number=2, message=common.CustomAttribute, + ) + + class RecentSearchResult(proto.Message): + r"""Recent search of this user. + Attributes: + recent_search (str): + The recent search query. + """ + + recent_search = proto.Field(proto.STRING, number=1,) + + completion_results = proto.RepeatedField( + proto.MESSAGE, number=1, message=CompletionResult, + ) + attribution_token = proto.Field(proto.STRING, number=2,) + recent_search_results = proto.RepeatedField( + proto.MESSAGE, number=3, message=RecentSearchResult, + ) + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/retail_v2/types/import_config.py b/google/cloud/retail_v2/types/import_config.py index 14ca4172..d56995db 100644 --- a/google/cloud/retail_v2/types/import_config.py +++ b/google/cloud/retail_v2/types/import_config.py @@ -20,6 +20,7 @@ from google.protobuf import field_mask_pb2 # type: ignore from google.protobuf import timestamp_pb2 # type: ignore from google.rpc import status_pb2 # type: ignore +from google.type import date_pb2 # type: ignore __protobuf__ = proto.module( @@ -32,12 +33,15 @@ "ImportErrorsConfig", "ImportProductsRequest", "ImportUserEventsRequest", + "ImportCompletionDataRequest", "ProductInputConfig", "UserEventInputConfig", + "CompletionDataInputConfig", "ImportMetadata", "ImportProductsResponse", "ImportUserEventsResponse", "UserEventImportSummary", + "ImportCompletionDataResponse", }, ) @@ -55,7 +59,7 @@ class GcsSource(proto.Message): one or more files, such as ``gs://bucket/directory/*.json``. A request can contain at most 100 files, and each file can be up to 2 GB. See `Importing product - information `__ + information `__ for the expected file format and setup instructions. data_schema (str): The schema to use when parsing the data from the source. @@ -75,7 +79,7 @@ class GcsSource(proto.Message): - ``user_event`` (default): One JSON [UserEvent][google.cloud.retail.v2.UserEvent] per line. - ``user_event_ga360``: Using - https://support.google.com/analytics/answer/3437719?hl=en. + https://support.google.com/analytics/answer/3437719. """ input_uris = proto.RepeatedField(proto.STRING, number=1,) @@ -85,11 +89,18 @@ class GcsSource(proto.Message): class BigQuerySource(proto.Message): r"""BigQuery source import data from. Attributes: + partition_date (google.type.date_pb2.Date): + BigQuery time partitioned table's \_PARTITIONDATE in + YYYY-MM-DD format. + + Only supported when + [ImportProductsRequest.reconciliation_mode][google.cloud.retail.v2.ImportProductsRequest.reconciliation_mode] + is set to ``FULL``. project_id (str): - The project id (can be project # or id) that + The project ID (can be project # or ID) that the BigQuery source is in with a length limit of 128 characters. If not specified, inherits the - project id from the parent request. + project ID from the parent request. dataset_id (str): Required. The BigQuery data set to copy the data from with a length limit of 1,024 @@ -121,9 +132,12 @@ class BigQuerySource(proto.Message): - ``user_event`` (default): One JSON [UserEvent][google.cloud.retail.v2.UserEvent] per line. - ``user_event_ga360``: Using - https://support.google.com/analytics/answer/3437719?hl=en. + https://support.google.com/analytics/answer/3437719. """ + partition_date = proto.Field( + proto.MESSAGE, number=6, oneof="partition", message=date_pb2.Date, + ) project_id = proto.Field(proto.STRING, number=5,) dataset_id = proto.Field(proto.STRING, number=1,) table_id = proto.Field(proto.STRING, number=2,) @@ -140,7 +154,7 @@ class ProductInlineSource(proto.Message): Required. A list of products to update/create. Each product must have a valid [Product.id][google.cloud.retail.v2.Product.id]. Recommended - max of 10k items. + max of 100 items. """ products = proto.RepeatedField(proto.MESSAGE, number=1, message=product.Product,) @@ -184,6 +198,17 @@ class ImportProductsRequest(proto.Message): If no updateMask is specified, requires products.create permission. If updateMask is specified, requires products.update permission. + request_id (str): + Unique identifier provided by client, within the ancestor + dataset scope. Ensures idempotency and used for request + deduplication. Server-generated if unspecified. Up to 128 + characters long and must match the pattern: "[a-zA-Z0-9\_]+". + This is returned as [Operation.name][] in + [ImportMetadata][google.cloud.retail.v2.ImportMetadata]. + + Only supported when + [ImportProductsRequest.reconciliation_mode][google.cloud.retail.v2.ImportProductsRequest.reconciliation_mode] + is set to ``FULL``. input_config (google.cloud.retail_v2.types.ProductInputConfig): Required. The desired input location of the data. @@ -194,14 +219,40 @@ class ImportProductsRequest(proto.Message): Indicates which fields in the provided imported 'products' to update. If not set, will by default update all fields. + reconciliation_mode (google.cloud.retail_v2.types.ImportProductsRequest.ReconciliationMode): + The mode of reconciliation between existing products and the + products to be imported. Defaults to + [ReconciliationMode.INCREMENTAL][google.cloud.retail.v2.ImportProductsRequest.ReconciliationMode.INCREMENTAL]. + notification_pubsub_topic (str): + Pub/Sub topic for receiving notification. If this field is + set, when the import is finished, a notification will be + sent to specified Pub/Sub topic. The message data will be + JSON string of a [Operation][google.longrunning.Operation]. + Format of the Pub/Sub topic is + ``projects/{project}/topics/{topic}``. + + Only supported when + [ImportProductsRequest.reconciliation_mode][google.cloud.retail.v2.ImportProductsRequest.reconciliation_mode] + is set to ``FULL``. """ + class ReconciliationMode(proto.Enum): + r"""Indicates how imported products are reconciled with the + existing products created or imported before. + """ + RECONCILIATION_MODE_UNSPECIFIED = 0 + INCREMENTAL = 1 + FULL = 2 + parent = proto.Field(proto.STRING, number=1,) + request_id = proto.Field(proto.STRING, number=6,) input_config = proto.Field(proto.MESSAGE, number=2, message="ProductInputConfig",) errors_config = proto.Field(proto.MESSAGE, number=3, message="ImportErrorsConfig",) update_mask = proto.Field( proto.MESSAGE, number=4, message=field_mask_pb2.FieldMask, ) + reconciliation_mode = proto.Field(proto.ENUM, number=5, enum=ReconciliationMode,) + notification_pubsub_topic = proto.Field(proto.STRING, number=7,) class ImportUserEventsRequest(proto.Message): @@ -224,6 +275,34 @@ class ImportUserEventsRequest(proto.Message): errors_config = proto.Field(proto.MESSAGE, number=3, message="ImportErrorsConfig",) +class ImportCompletionDataRequest(proto.Message): + r"""Request message for ImportCompletionData methods. + Attributes: + parent (str): + Required. The catalog which the suggestions dataset belongs + to. + + Format: + ``projects/1234/locations/global/catalogs/default_catalog``. + input_config (google.cloud.retail_v2.types.CompletionDataInputConfig): + Required. The desired input location of the + data. + notification_pubsub_topic (str): + Pub/Sub topic for receiving notification. If this field is + set, when the import is finished, a notification will be + sent to specified Pub/Sub topic. The message data will be + JSON string of a [Operation][google.longrunning.Operation]. + Format of the Pub/Sub topic is + ``projects/{project}/topics/{topic}``. + """ + + parent = proto.Field(proto.STRING, number=1,) + input_config = proto.Field( + proto.MESSAGE, number=2, message="CompletionDataInputConfig", + ) + notification_pubsub_topic = proto.Field(proto.STRING, number=3,) + + class ProductInputConfig(proto.Message): r"""The input config source for products. Attributes: @@ -272,6 +351,22 @@ class UserEventInputConfig(proto.Message): ) +class CompletionDataInputConfig(proto.Message): + r"""The input config source for completion data. + Attributes: + big_query_source (google.cloud.retail_v2.types.BigQuerySource): + Required. BigQuery input source. + Add the IAM permission “BigQuery Data Viewer” + for cloud-retail-customer-data- + access@system.gserviceaccount.com before using + this feature otherwise an error is thrown. + """ + + big_query_source = proto.Field( + proto.MESSAGE, number=1, oneof="source", message="BigQuerySource", + ) + + class ImportMetadata(proto.Message): r"""Metadata related to the progress of the Import operation. This will be returned by the @@ -289,12 +384,25 @@ class ImportMetadata(proto.Message): failure_count (int): Count of entries that encountered errors while processing. + request_id (str): + Id of the request / operation. This is + parroting back the requestId that was passed in + the request. + notification_pubsub_topic (str): + Pub/Sub topic for receiving notification. If this field is + set, when the import is finished, a notification will be + sent to specified Pub/Sub topic. The message data will be + JSON string of a [Operation][google.longrunning.Operation]. + Format of the Pub/Sub topic is + ``projects/{project}/topics/{topic}``. """ create_time = proto.Field(proto.MESSAGE, number=1, message=timestamp_pb2.Timestamp,) update_time = proto.Field(proto.MESSAGE, number=2, message=timestamp_pb2.Timestamp,) success_count = proto.Field(proto.INT64, number=3,) failure_count = proto.Field(proto.INT64, number=4,) + request_id = proto.Field(proto.STRING, number=5,) + notification_pubsub_topic = proto.Field(proto.STRING, number=6,) class ImportProductsResponse(proto.Message): @@ -364,4 +472,22 @@ class UserEventImportSummary(proto.Message): unjoined_events_count = proto.Field(proto.INT64, number=2,) +class ImportCompletionDataResponse(proto.Message): + r"""Response of the + [ImportCompletionDataRequest][google.cloud.retail.v2.ImportCompletionDataRequest]. + If the long running operation is done, this message is returned by + the google.longrunning.Operations.response field if the operation is + successful. + + Attributes: + error_samples (Sequence[google.rpc.status_pb2.Status]): + A sample of errors encountered while + processing the request. + """ + + error_samples = proto.RepeatedField( + proto.MESSAGE, number=1, message=status_pb2.Status, + ) + + __all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/retail_v2/types/prediction_service.py b/google/cloud/retail_v2/types/prediction_service.py index f659ea1b..f6880776 100644 --- a/google/cloud/retail_v2/types/prediction_service.py +++ b/google/cloud/retail_v2/types/prediction_service.py @@ -30,35 +30,11 @@ class PredictRequest(proto.Message): placement (str): Required. Full resource name of the format: {name=projects/*/locations/global/catalogs/default_catalog/placements/*} - The id of the recommendation engine placement. This id is - used to identify the set of models that will be used to make - the prediction. - - We currently support three placements with the following IDs - by default: - - - ``shopping_cart``: Predicts products frequently bought - together with one or more products in the same shopping - session. Commonly displayed after ``add-to-cart`` events, - on product detail pages, or on the shopping cart page. - - - ``home_page``: Predicts the next product that a user will - most likely engage with or purchase based on the shopping - or viewing history of the specified ``userId`` or - ``visitorId``. For example - Recommendations for you. - - - ``product_detail``: Predicts the next product that a user - will most likely engage with or purchase. The prediction - is based on the shopping or viewing history of the - specified ``userId`` or ``visitorId`` and its relevance - to a specified ``CatalogItem``. Typically used on product - detail pages. For example - More products like this. - - - ``recently_viewed_default``: Returns up to 75 products - recently viewed by the specified ``userId`` or - ``visitorId``, most recent ones first. Returns nothing if - neither of them has viewed any products yet. For example - - Recently viewed. + The ID of the Recommendations AI placement. Before you can + request predictions from your model, you must create at + least one placement for it. For more information, see + `Managing + placements `__. The full list of available placements can be seen at https://console.cloud.google.com/recommendation/catalogs/default_catalog/placements @@ -91,6 +67,9 @@ class PredictRequest(proto.Message): quoted UTF-8 encoded strings with a size limit of 1,000 characters. + Note: "Recently viewed" models don't support tag + filtering at the moment. + - filterOutOfStockItems. Restricts predictions to products that do not have a stockState value of OUT_OF_STOCK. @@ -129,20 +108,39 @@ class PredictRequest(proto.Message): false, the service will return generic (unfiltered) popular products instead of empty if your filter blocks all prediction results. + - ``priceRerankLevel``: String. Default empty. If set to be + non-empty, then it needs to be one of + {'no-price-reranking', 'low-price-reranking', + 'medium-price-reranking', 'high-price-reranking'}. This + gives request-level control and adjusts prediction + results based on product price. + - ``diversityLevel``: String. Default empty. If set to be + non-empty, then it needs to be one of {'no-diversity', + 'low-diversity', 'medium-diversity', 'high-diversity', + 'auto-diversity'}. This gives request-level control and + adjusts prediction results based on product category. labels (Sequence[google.cloud.retail_v2.types.PredictRequest.LabelsEntry]): - The labels for the predict request. - - - Label keys can contain lowercase letters, digits and - hyphens, must start with a letter, and must end with a - letter or digit. - - Non-zero label values can contain lowercase letters, - digits and hyphens, must start with a letter, and must - end with a letter or digit. - - No more than 64 labels can be associated with a given - request. - - See https://goo.gl/xmQnxf for more information on and - examples of labels. + The labels applied to a resource must meet the following + requirements: + + - Each resource can have multiple labels, up to a maximum + of 64. + - Each label must be a key-value pair. + - Keys have a minimum length of 1 character and a maximum + length of 63 characters, and cannot be empty. Values can + be empty, and have a maximum length of 63 characters. + - Keys and values can contain only lowercase letters, + numeric characters, underscores, and dashes. All + characters must use UTF-8 encoding, and international + characters are allowed. + - The key portion of a label must be unique. However, you + can use the same key with multiple resources. + - Keys must start with a lowercase letter or international + character. + + See `Google Cloud + Document `__ + for more details. """ placement = proto.Field(proto.STRING, number=1,) diff --git a/google/cloud/retail_v2/types/product.py b/google/cloud/retail_v2/types/product.py index 97a47bb7..50d5416a 100644 --- a/google/cloud/retail_v2/types/product.py +++ b/google/cloud/retail_v2/types/product.py @@ -16,6 +16,8 @@ import proto # type: ignore from google.cloud.retail_v2.types import common +from google.protobuf import duration_pb2 # type: ignore +from google.protobuf import field_mask_pb2 # type: ignore from google.protobuf import timestamp_pb2 # type: ignore from google.protobuf import wrappers_pb2 # type: ignore @@ -28,6 +30,42 @@ class Product(proto.Message): recommended or searched. Attributes: + expire_time (google.protobuf.timestamp_pb2.Timestamp): + The timestamp when this product becomes unavailable for + [SearchService.Search][google.cloud.retail.v2.SearchService.Search]. + + If it is set, the [Product][google.cloud.retail.v2.Product] + is not available for + [SearchService.Search][google.cloud.retail.v2.SearchService.Search] + after + [expire_time][google.cloud.retail.v2.Product.expire_time]. + However, the product can still be retrieved by + [ProductService.GetProduct][google.cloud.retail.v2.ProductService.GetProduct] + and + [ProductService.ListProducts][google.cloud.retail.v2.ProductService.ListProducts]. + + Google Merchant Center property + `expiration_date `__. + ttl (google.protobuf.duration_pb2.Duration): + Input only. The TTL (time to live) of the product. + + If it is set, + [expire_time][google.cloud.retail.v2.Product.expire_time] is + set as current timestamp plus + [ttl][google.cloud.retail.v2.Product.ttl]. The derived + [expire_time][google.cloud.retail.v2.Product.expire_time] is + returned in the output and + [ttl][google.cloud.retail.v2.Product.ttl] is left blank when + retrieving the [Product][google.cloud.retail.v2.Product]. + + If it is set, the product is not available for + [SearchService.Search][google.cloud.retail.v2.SearchService.Search] + after current timestamp plus + [ttl][google.cloud.retail.v2.Product.ttl]. However, the + product can still be retrieved by + [ProductService.GetProduct][google.cloud.retail.v2.ProductService.GetProduct] + and + [ProductService.ListProducts][google.cloud.retail.v2.ProductService.ListProducts]. name (str): Immutable. Full resource name of the product, such as ``projects/*/locations/global/catalogs/default_catalog/branches/default_branch/products/product_id``. @@ -50,8 +88,9 @@ class Product(proto.Message): Schema.org Property `Product.sku `__. type_ (google.cloud.retail_v2.types.Product.Type): - Immutable. The type of the product. This - field is output-only. + Immutable. The type of the product. Default to + [Catalog.product_level_config.ingestion_product_type][google.cloud.retail.v2.ProductLevelConfig.ingestion_product_type] + if unset. primary_product_id (str): Variant group identifier. Must be an [id][google.cloud.retail.v2.Product.id], with the same @@ -78,6 +117,32 @@ class Product(proto.Message): This field must be enabled before it can be used. `Learn more `__. + collection_member_ids (Sequence[str]): + The [id][google.cloud.retail.v2.Product.id] of the + collection members when + [type][google.cloud.retail.v2.Product.type] is + [Type.COLLECTION][google.cloud.retail.v2.Product.Type.COLLECTION]. + + Should not set it for other types. A maximum of 1000 values + are allowed. Otherwise, an INVALID_ARGUMENT error is return. + gtin (str): + The Global Trade Item Number (GTIN) of the product. + + This field must be a UTF-8 encoded string with a length + limit of 128 characters. Otherwise, an INVALID_ARGUMENT + error is returned. + + Google Merchant Center property + `gtin `__. + Schema.org property + `Product.isbn `__ or + `Product.gtin8 `__ or + `Product.gtin12 `__ or + `Product.gtin13 `__ or + `Product.gtin14 `__. + + If the value is not a valid GTIN, an INVALID_ARGUMENT error + is returned. categories (Sequence[str]): Product categories. This field is repeated for supporting one product belonging to several parallel categories. @@ -118,13 +183,25 @@ class Product(proto.Message): Required. Product title. This field must be a UTF-8 encoded string with a length - limit of 128 characters. Otherwise, an INVALID_ARGUMENT + limit of 1,000 characters. Otherwise, an INVALID_ARGUMENT error is returned. Google Merchant Center property `title `__. Schema.org property `Product.name `__. + brands (Sequence[str]): + The brands of the product. + + A maximum of 30 brands are allowed. Each brand must be a + UTF-8 encoded string with a length limit of 1,000 + characters. Otherwise, an INVALID_ARGUMENT error is + returned. + + Google Merchant Center property + `brand `__. + Schema.org property + `Product.brand `__. description (str): Product description. @@ -136,6 +213,21 @@ class Product(proto.Message): `description `__. schema.org property `Product.description `__. + language_code (str): + Language of the title/description and other string + attributes. Use language tags defined by [BCP + 47][https://www.rfc-editor.org/rfc/bcp/bcp47.txt]. + + For product prediction, this field is ignored and the model + automatically detects the text language. The + [Product][google.cloud.retail.v2.Product] can include text + in different languages, but duplicating + [Product][google.cloud.retail.v2.Product]s to provide text + in multiple languages can result in degraded model + performance. + + For product search this field is in use. It defaults to + "en-US" if unset. attributes (Sequence[google.cloud.retail_v2.types.Product.AttributesEntry]): Highly encouraged. Extra product attributes to be included. For example, for products, this could include the store @@ -154,12 +246,19 @@ class Product(proto.Message): For example: ``{ "vendor": {"text": ["vendor123", "vendor456"]}, "lengths_cm": {"numbers":[2.3, 15.4]}, "heights_cm": {"numbers":[8.1, 6.4]} }``. - A maximum of 150 attributes are allowed. Otherwise, an - INVALID_ARGUMENT error is returned. - - The key must be a UTF-8 encoded string with a length limit - of 5,000 characters. Otherwise, an INVALID_ARGUMENT error is - returned. + This field needs to pass all below criteria, otherwise an + INVALID_ARGUMENT error is returned: + + - Max entries count: 200 by default; 100 for + [Type.VARIANT][google.cloud.retail.v2.Product.Type.VARIANT]. + - The key must be a UTF-8 encoded string with a length + limit of 128 characters. + - Max indexable entries count: 200 by default; 40 for + [Type.VARIANT][google.cloud.retail.v2.Product.Type.VARIANT]. + - Max searchable entries count: 30. + - For indexable attribute, the key must match the pattern: + [a-zA-Z0-9][a-zA-Z0-9\_]*. For example, key0LikeThis or + KEY_1_LIKE_THIS. tags (Sequence[str]): Custom tags associated with the product. @@ -180,10 +279,13 @@ class Product(proto.Message): Google Merchant Center property `price `__. + rating (google.cloud.retail_v2.types.Rating): + The rating of this product. available_time (google.protobuf.timestamp_pb2.Timestamp): The timestamp when this [Product][google.cloud.retail.v2.Product] becomes available - recommendation and search. + for + [SearchService.Search][google.cloud.retail.v2.SearchService.Search]. availability (google.cloud.retail_v2.types.Product.Availability): The online availability of the [Product][google.cloud.retail.v2.Product]. Default to @@ -195,9 +297,20 @@ class Product(proto.Message): `Offer.availability `__. available_quantity (google.protobuf.wrappers_pb2.Int32Value): The available quantity of the item. + fulfillment_info (Sequence[google.cloud.retail_v2.types.FulfillmentInfo]): + Fulfillment information, such as the store IDs for in-store + pickup or region IDs for different shipping methods. + + All the elements must have distinct + [FulfillmentInfo.type][google.cloud.retail.v2.FulfillmentInfo.type]. + Otherwise, an INVALID_ARGUMENT error is returned. uri (str): Canonical URL directly linking to the product detail page. + It is strongly recommended to provide a valid uri for the + product, otherwise the service performance could be + significantly degraded. + This field must be a UTF-8 encoded string with a length limit of 5,000 characters. Otherwise, an INVALID_ARGUMENT error is returned. @@ -206,7 +319,8 @@ class Product(proto.Message): `link `__. Schema.org property `Offer.url `__. images (Sequence[google.cloud.retail_v2.types.Image]): - Product images for the product. + Product images for the product.Highly recommended to put the + main image to the first. A maximum of 300 images are allowed. @@ -214,6 +328,168 @@ class Product(proto.Message): `image_link `__. Schema.org property `Product.image `__. + audience (google.cloud.retail_v2.types.Audience): + The target group associated with a given + audience (e.g. male, veterans, car owners, + musicians, etc.) of the product. + color_info (google.cloud.retail_v2.types.ColorInfo): + The color of the product. + + Google Merchant Center property + `color `__. + Schema.org property + `Product.color `__. + sizes (Sequence[str]): + The size of the product. To represent different size systems + or size types, consider using this format: + [[[size_system:]size_type:]size_value]. + + For example, in "US:MENS:M", "US" represents size system; + "MENS" represents size type; "M" represents size value. In + "GIRLS:27", size system is empty; "GIRLS" represents size + type; "27" represents size value. In "32 inches", both size + system and size type are empty, while size value is "32 + inches". + + A maximum of 20 values are allowed per + [Product][google.cloud.retail.v2.Product]. Each value must + be a UTF-8 encoded string with a length limit of 128 + characters. Otherwise, an INVALID_ARGUMENT error is + returned. + + Google Merchant Center property + `size `__, + `size_type `__ + and + `size_system `__. + Schema.org property + `Product.size `__. + materials (Sequence[str]): + The material of the product. For example, "leather", + "wooden". + + A maximum of 5 values are allowed. Each value must be a + UTF-8 encoded string with a length limit of 128 characters. + Otherwise, an INVALID_ARGUMENT error is returned. + + Google Merchant Center property + `material `__. + Schema.org property + `Product.material `__. + patterns (Sequence[str]): + The pattern or graphic print of the product. For example, + "striped", "polka dot", "paisley". + + A maximum of 5 values are allowed per + [Product][google.cloud.retail.v2.Product]. Each value must + be a UTF-8 encoded string with a length limit of 128 + characters. Otherwise, an INVALID_ARGUMENT error is + returned. + + Google Merchant Center property + `pattern `__. + Schema.org property + `Product.pattern `__. + conditions (Sequence[str]): + The condition of the product. Strongly encouraged to use the + standard values: "new", "refurbished", "used". + + A maximum of 5 values are allowed per + [Product][google.cloud.retail.v2.Product]. Each value must + be a UTF-8 encoded string with a length limit of 128 + characters. Otherwise, an INVALID_ARGUMENT error is + returned. + + Google Merchant Center property + `condition `__. + Schema.org property + `Offer.itemCondition `__. + promotions (Sequence[google.cloud.retail_v2.types.Promotion]): + The promotions applied to the product. A maximum of 10 + values are allowed per + [Product][google.cloud.retail.v2.Product]. + publish_time (google.protobuf.timestamp_pb2.Timestamp): + The timestamp when the product is published by the retailer + for the first time, which indicates the freshness of the + products. Note that this field is different from + [available_time][google.cloud.retail.v2.Product.available_time], + given it purely describes product freshness regardless of + when it is available on search and recommendation. + retrievable_fields (google.protobuf.field_mask_pb2.FieldMask): + Indicates which fields in the + [Product][google.cloud.retail.v2.Product]s are returned in + [SearchResponse][google.cloud.retail.v2.SearchResponse]. + + Supported fields for all + [type][google.cloud.retail.v2.Product.type]s: + + - [audience][google.cloud.retail.v2.Product.audience] + - [availability][google.cloud.retail.v2.Product.availability] + - [brands][google.cloud.retail.v2.Product.brands] + - [color_info][google.cloud.retail.v2.Product.color_info] + - [conditions][google.cloud.retail.v2.Product.conditions] + - [gtin][google.cloud.retail.v2.Product.gtin] + - [materials][google.cloud.retail.v2.Product.materials] + - [name][google.cloud.retail.v2.Product.name] + - [patterns][google.cloud.retail.v2.Product.patterns] + - [price_info][google.cloud.retail.v2.Product.price_info] + - [rating][google.cloud.retail.v2.Product.rating] + - [sizes][google.cloud.retail.v2.Product.sizes] + - [title][google.cloud.retail.v2.Product.title] + - [uri][google.cloud.retail.v2.Product.uri] + + Supported fields only for + [Type.PRIMARY][google.cloud.retail.v2.Product.Type.PRIMARY] + and + [Type.COLLECTION][google.cloud.retail.v2.Product.Type.COLLECTION]: + + - [categories][google.cloud.retail.v2.Product.categories] + - [description][google.cloud.retail.v2.Product.description] + - [images][google.cloud.retail.v2.Product.images] + + Supported fields only for + [Type.VARIANT][google.cloud.retail.v2.Product.Type.VARIANT]: + + - Only the first image in + [images][google.cloud.retail.v2.Product.images] + + To mark + [attributes][google.cloud.retail.v2.Product.attributes] as + retrievable, include paths of the form "attributes.key" + where "key" is the key of a custom attribute, as specified + in [attributes][google.cloud.retail.v2.Product.attributes]. + + For + [Type.PRIMARY][google.cloud.retail.v2.Product.Type.PRIMARY] + and + [Type.COLLECTION][google.cloud.retail.v2.Product.Type.COLLECTION], + the following fields are always returned in + [SearchResponse][google.cloud.retail.v2.SearchResponse] by + default: + + - [name][google.cloud.retail.v2.Product.name] + + For + [Type.VARIANT][google.cloud.retail.v2.Product.Type.VARIANT], + the following fields are always returned in by default: + + - [name][google.cloud.retail.v2.Product.name] + - [color_info][google.cloud.retail.v2.Product.color_info] + + Maximum number of paths is 20. Otherwise, an + INVALID_ARGUMENT error is returned. + variants (Sequence[google.cloud.retail_v2.types.Product]): + Output only. Product variants grouped together on primary + product which share similar product attributes. It's + automatically grouped by + [primary_product_id][google.cloud.retail.v2.Product.primary_product_id] + for all the product variants. Only populated for + [Type.PRIMARY][google.cloud.retail.v2.Product.Type.PRIMARY] + [Product][google.cloud.retail.v2.Product]s. + + Note: This field is OUTPUT_ONLY for + [ProductService.GetProduct][google.cloud.retail.v2.ProductService.GetProduct]. + Do not set this field in API requests. """ class Type(proto.Enum): @@ -233,18 +509,29 @@ class Availability(proto.Enum): PREORDER = 3 BACKORDER = 4 + expire_time = proto.Field( + proto.MESSAGE, number=16, oneof="expiration", message=timestamp_pb2.Timestamp, + ) + ttl = proto.Field( + proto.MESSAGE, number=17, oneof="expiration", message=duration_pb2.Duration, + ) name = proto.Field(proto.STRING, number=1,) id = proto.Field(proto.STRING, number=2,) type_ = proto.Field(proto.ENUM, number=3, enum=Type,) primary_product_id = proto.Field(proto.STRING, number=4,) + collection_member_ids = proto.RepeatedField(proto.STRING, number=5,) + gtin = proto.Field(proto.STRING, number=6,) categories = proto.RepeatedField(proto.STRING, number=7,) title = proto.Field(proto.STRING, number=8,) + brands = proto.RepeatedField(proto.STRING, number=9,) description = proto.Field(proto.STRING, number=10,) + language_code = proto.Field(proto.STRING, number=11,) attributes = proto.MapField( proto.STRING, proto.MESSAGE, number=12, message=common.CustomAttribute, ) tags = proto.RepeatedField(proto.STRING, number=13,) price_info = proto.Field(proto.MESSAGE, number=14, message=common.PriceInfo,) + rating = proto.Field(proto.MESSAGE, number=15, message=common.Rating,) available_time = proto.Field( proto.MESSAGE, number=18, message=timestamp_pb2.Timestamp, ) @@ -252,8 +539,27 @@ class Availability(proto.Enum): available_quantity = proto.Field( proto.MESSAGE, number=20, message=wrappers_pb2.Int32Value, ) + fulfillment_info = proto.RepeatedField( + proto.MESSAGE, number=21, message=common.FulfillmentInfo, + ) uri = proto.Field(proto.STRING, number=22,) images = proto.RepeatedField(proto.MESSAGE, number=23, message=common.Image,) + audience = proto.Field(proto.MESSAGE, number=24, message=common.Audience,) + color_info = proto.Field(proto.MESSAGE, number=25, message=common.ColorInfo,) + sizes = proto.RepeatedField(proto.STRING, number=26,) + materials = proto.RepeatedField(proto.STRING, number=27,) + patterns = proto.RepeatedField(proto.STRING, number=28,) + conditions = proto.RepeatedField(proto.STRING, number=29,) + promotions = proto.RepeatedField( + proto.MESSAGE, number=34, message=common.Promotion, + ) + publish_time = proto.Field( + proto.MESSAGE, number=33, message=timestamp_pb2.Timestamp, + ) + retrievable_fields = proto.Field( + proto.MESSAGE, number=30, message=field_mask_pb2.FieldMask, + ) + variants = proto.RepeatedField(proto.MESSAGE, number=31, message="Product",) __all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/retail_v2/types/product_service.py b/google/cloud/retail_v2/types/product_service.py index ea2dfef9..61a9d6ae 100644 --- a/google/cloud/retail_v2/types/product_service.py +++ b/google/cloud/retail_v2/types/product_service.py @@ -17,6 +17,7 @@ from google.cloud.retail_v2.types import product as gcr_product from google.protobuf import field_mask_pb2 # type: ignore +from google.protobuf import timestamp_pb2 # type: ignore __protobuf__ = proto.module( @@ -26,6 +27,17 @@ "GetProductRequest", "UpdateProductRequest", "DeleteProductRequest", + "ListProductsRequest", + "ListProductsResponse", + "SetInventoryRequest", + "SetInventoryMetadata", + "SetInventoryResponse", + "AddFulfillmentPlacesRequest", + "AddFulfillmentPlacesMetadata", + "AddFulfillmentPlacesResponse", + "RemoveFulfillmentPlacesRequest", + "RemoveFulfillmentPlacesMetadata", + "RemoveFulfillmentPlacesResponse", }, ) @@ -97,7 +109,9 @@ class UpdateProductRequest(proto.Message): returned. If the [Product][google.cloud.retail.v2.Product] to update - does not exist, a NOT_FOUND error is returned. + does not exist and + [allow_missing][google.cloud.retail.v2.UpdateProductRequest.allow_missing] + is not set, a NOT_FOUND error is returned. update_mask (google.protobuf.field_mask_pb2.FieldMask): Indicates which fields in the provided [Product][google.cloud.retail.v2.Product] to update. The @@ -107,12 +121,18 @@ class UpdateProductRequest(proto.Message): If an unsupported or unknown field is provided, an INVALID_ARGUMENT error is returned. + allow_missing (bool): + If set to true, and the + [Product][google.cloud.retail.v2.Product] is not found, a + new [Product][google.cloud.retail.v2.Product] will be + created. In this situation, ``update_mask`` is ignored. """ product = proto.Field(proto.MESSAGE, number=1, message=gcr_product.Product,) update_mask = proto.Field( proto.MESSAGE, number=2, message=field_mask_pb2.FieldMask, ) + allow_missing = proto.Field(proto.BOOL, number=3,) class DeleteProductRequest(proto.Message): @@ -130,9 +150,399 @@ class DeleteProductRequest(proto.Message): If the [Product][google.cloud.retail.v2.Product] to delete does not exist, a NOT_FOUND error is returned. + + The [Product][google.cloud.retail.v2.Product] to delete can + neither be a + [Product.Type.COLLECTION][google.cloud.retail.v2.Product.Type.COLLECTION] + [Product][google.cloud.retail.v2.Product] member nor a + [Product.Type.PRIMARY][google.cloud.retail.v2.Product.Type.PRIMARY] + [Product][google.cloud.retail.v2.Product] with more than one + [variants][google.cloud.retail.v2.Product.Type.VARIANT]. + Otherwise, an INVALID_ARGUMENT error is returned. + + All inventory information for the named + [Product][google.cloud.retail.v2.Product] will be deleted. """ name = proto.Field(proto.STRING, number=1,) +class ListProductsRequest(proto.Message): + r"""Request message for + [ProductService.ListProducts][google.cloud.retail.v2.ProductService.ListProducts] + method. + + Attributes: + parent (str): + Required. The parent branch resource name, such as + ``projects/*/locations/global/catalogs/default_catalog/branches/0``. + Use ``default_branch`` as the branch ID, to list products + under the default branch. + + If the caller does not have permission to list + [Product][google.cloud.retail.v2.Product]s under this + branch, regardless of whether or not this branch exists, a + PERMISSION_DENIED error is returned. + page_size (int): + Maximum number of [Product][google.cloud.retail.v2.Product]s + to return. If unspecified, defaults to 100. The maximum + allowed value is 1000. Values above 1000 will be coerced to + 1000. + + If this field is negative, an INVALID_ARGUMENT error is + returned. + page_token (str): + A page token + [ListProductsResponse.next_page_token][google.cloud.retail.v2.ListProductsResponse.next_page_token], + received from a previous + [ProductService.ListProducts][google.cloud.retail.v2.ProductService.ListProducts] + call. Provide this to retrieve the subsequent page. + + When paginating, all other parameters provided to + [ProductService.ListProducts][google.cloud.retail.v2.ProductService.ListProducts] + must match the call that provided the page token. Otherwise, + an INVALID_ARGUMENT error is returned. + filter (str): + A filter to apply on the list results. Supported features: + + - List all the products under the parent branch if + [filter][google.cloud.retail.v2.ListProductsRequest.filter] + is unset. + - List + [Product.Type.VARIANT][google.cloud.retail.v2.Product.Type.VARIANT] + [Product][google.cloud.retail.v2.Product]s sharing the + same + [Product.Type.PRIMARY][google.cloud.retail.v2.Product.Type.PRIMARY] + [Product][google.cloud.retail.v2.Product]. For example: + ``primary_product_id = "some_product_id"`` + - List [Product][google.cloud.retail.v2.Product]s bundled + in a + [Product.Type.COLLECTION][google.cloud.retail.v2.Product.Type.COLLECTION] + [Product][google.cloud.retail.v2.Product]. For example: + ``collection_product_id = "some_product_id"`` + - List [Product][google.cloud.retail.v2.Product]s with a + partibular type. For example: ``type = "PRIMARY"`` + ``type = "VARIANT"`` ``type = "COLLECTION"`` + + If the field is unrecognizable, an INVALID_ARGUMENT error is + returned. + + If the specified + [Product.Type.PRIMARY][google.cloud.retail.v2.Product.Type.PRIMARY] + [Product][google.cloud.retail.v2.Product] or + [Product.Type.COLLECTION][google.cloud.retail.v2.Product.Type.COLLECTION] + [Product][google.cloud.retail.v2.Product] does not exist, a + NOT_FOUND error is returned. + read_mask (google.protobuf.field_mask_pb2.FieldMask): + The fields of [Product][google.cloud.retail.v2.Product] to + return in the responses. If not set or empty, the following + fields are returned: + + - [Product.name][google.cloud.retail.v2.Product.name] + - [Product.id][google.cloud.retail.v2.Product.id] + - [Product.title][google.cloud.retail.v2.Product.title] + - [Product.uri][google.cloud.retail.v2.Product.uri] + - [Product.images][google.cloud.retail.v2.Product.images] + - [Product.price_info][google.cloud.retail.v2.Product.price_info] + - [Product.brands][google.cloud.retail.v2.Product.brands] + + If "*" is provided, all fields are returned. + [Product.name][google.cloud.retail.v2.Product.name] is + always returned no matter what mask is set. + + If an unsupported or unknown field is provided, an + INVALID_ARGUMENT error is returned. + """ + + parent = proto.Field(proto.STRING, number=1,) + page_size = proto.Field(proto.INT32, number=2,) + page_token = proto.Field(proto.STRING, number=3,) + filter = proto.Field(proto.STRING, number=4,) + read_mask = proto.Field(proto.MESSAGE, number=5, message=field_mask_pb2.FieldMask,) + + +class ListProductsResponse(proto.Message): + r"""Response message for + [ProductService.ListProducts][google.cloud.retail.v2.ProductService.ListProducts] + method. + + Attributes: + products (Sequence[google.cloud.retail_v2.types.Product]): + The [Product][google.cloud.retail.v2.Product]s. + next_page_token (str): + A token that can be sent as + [ListProductsRequest.page_token][google.cloud.retail.v2.ListProductsRequest.page_token] + to retrieve the next page. If this field is omitted, there + are no subsequent pages. + """ + + @property + def raw_page(self): + return self + + products = proto.RepeatedField( + proto.MESSAGE, number=1, message=gcr_product.Product, + ) + next_page_token = proto.Field(proto.STRING, number=2,) + + +class SetInventoryRequest(proto.Message): + r"""Request message for [SetInventory][] method. + Attributes: + inventory (google.cloud.retail_v2.types.Product): + Required. The inventory information to update. The allowable + fields to update are: + + - [Product.price_info][google.cloud.retail.v2.Product.price_info] + - [Product.availability][google.cloud.retail.v2.Product.availability] + - [Product.available_quantity][google.cloud.retail.v2.Product.available_quantity] + - [Product.fulfillment_info][google.cloud.retail.v2.Product.fulfillment_info] + The updated inventory fields must be specified in + [SetInventoryRequest.set_mask][google.cloud.retail.v2.SetInventoryRequest.set_mask]. + + If [SetInventoryRequest.inventory.name][] is empty or + invalid, an INVALID_ARGUMENT error is returned. + + If the caller does not have permission to update the + [Product][google.cloud.retail.v2.Product] named in + [Product.name][google.cloud.retail.v2.Product.name], + regardless of whether or not it exists, a PERMISSION_DENIED + error is returned. + + If the [Product][google.cloud.retail.v2.Product] to update + does not have existing inventory information, the provided + inventory information will be inserted. + + If the [Product][google.cloud.retail.v2.Product] to update + has existing inventory information, the provided inventory + information will be merged while respecting the last update + time for each inventory field, using the provided or default + value for + [SetInventoryRequest.set_time][google.cloud.retail.v2.SetInventoryRequest.set_time]. + + The last update time is recorded for the following inventory + fields: + + - [Product.price_info][google.cloud.retail.v2.Product.price_info] + - [Product.availability][google.cloud.retail.v2.Product.availability] + - [Product.available_quantity][google.cloud.retail.v2.Product.available_quantity] + - [Product.fulfillment_info][google.cloud.retail.v2.Product.fulfillment_info] + + If a full overwrite of inventory information while ignoring + timestamps is needed, [UpdateProduct][] should be invoked + instead. + set_mask (google.protobuf.field_mask_pb2.FieldMask): + Indicates which inventory fields in the provided + [Product][google.cloud.retail.v2.Product] to update. If not + set or set with empty paths, all inventory fields will be + updated. + + If an unsupported or unknown field is provided, an + INVALID_ARGUMENT error is returned and the entire update + will be ignored. + set_time (google.protobuf.timestamp_pb2.Timestamp): + The time when the request is issued, used to + prevent out-of-order updates on inventory fields + with the last update time recorded. If not + provided, the internal system time will be used. + allow_missing (bool): + If set to true, and the + [Product][google.cloud.retail.v2.Product] with name + [Product.name][google.cloud.retail.v2.Product.name] is not + found, the inventory update will still be processed and + retained for at most 1 day until the + [Product][google.cloud.retail.v2.Product] is created. If set + to false, an INVALID_ARGUMENT error is returned if the + [Product][google.cloud.retail.v2.Product] is not found. + """ + + inventory = proto.Field(proto.MESSAGE, number=1, message=gcr_product.Product,) + set_mask = proto.Field(proto.MESSAGE, number=2, message=field_mask_pb2.FieldMask,) + set_time = proto.Field(proto.MESSAGE, number=3, message=timestamp_pb2.Timestamp,) + allow_missing = proto.Field(proto.BOOL, number=4,) + + +class SetInventoryMetadata(proto.Message): + r"""Metadata related to the progress of the SetInventory operation. + Currently empty because there is no meaningful metadata populated + from the [SetInventory][] method. + """ + + +class SetInventoryResponse(proto.Message): + r"""Response of the SetInventoryRequest. Currently empty because there + is no meaningful response populated from the [SetInventory][] + method. + """ + + +class AddFulfillmentPlacesRequest(proto.Message): + r"""Request message for [AddFulfillmentPlaces][] method. + Attributes: + product (str): + Required. Full resource name of + [Product][google.cloud.retail.v2.Product], such as + ``projects/*/locations/global/catalogs/default_catalog/branches/default_branch/products/some_product_id``. + + If the caller does not have permission to access the + [Product][google.cloud.retail.v2.Product], regardless of + whether or not it exists, a PERMISSION_DENIED error is + returned. + type_ (str): + Required. The fulfillment type, including commonly used + types (such as pickup in store and same day delivery), and + custom types. + + Supported values: + + - "pickup-in-store" + - "ship-to-store" + - "same-day-delivery" + - "next-day-delivery" + - "custom-type-1" + - "custom-type-2" + - "custom-type-3" + - "custom-type-4" + - "custom-type-5" + + If this field is set to an invalid value other than these, + an INVALID_ARGUMENT error is returned. + + This field directly corresponds to + [Product.fulfillment_info.type][]. + place_ids (Sequence[str]): + Required. The IDs for this + [type][google.cloud.retail.v2.AddFulfillmentPlacesRequest.type], + such as the store IDs for "pickup-in-store" or the region + IDs for "same-day-delivery" to be added for this + [type][google.cloud.retail.v2.AddFulfillmentPlacesRequest.type]. + Duplicate IDs will be automatically ignored. + + At least 1 value is required, and a maximum of 2000 values + are allowed. Each value must be a string with a length limit + of 10 characters, matching the pattern [a-zA-Z0-9\_-]+, such + as "store1" or "REGION-2". Otherwise, an INVALID_ARGUMENT + error is returned. + + If the total number of place IDs exceeds 2000 for this + [type][google.cloud.retail.v2.AddFulfillmentPlacesRequest.type] + after adding, then the update will be rejected. + add_time (google.protobuf.timestamp_pb2.Timestamp): + The time when the fulfillment updates are + issued, used to prevent out-of-order updates on + fulfillment information. If not provided, the + internal system time will be used. + allow_missing (bool): + If set to true, and the + [Product][google.cloud.retail.v2.Product] is not found, the + fulfillment information will still be processed and retained + for at most 1 day and processed once the + [Product][google.cloud.retail.v2.Product] is created. If set + to false, an INVALID_ARGUMENT error is returned if the + [Product][google.cloud.retail.v2.Product] is not found. + """ + + product = proto.Field(proto.STRING, number=1,) + type_ = proto.Field(proto.STRING, number=2,) + place_ids = proto.RepeatedField(proto.STRING, number=3,) + add_time = proto.Field(proto.MESSAGE, number=4, message=timestamp_pb2.Timestamp,) + allow_missing = proto.Field(proto.BOOL, number=5,) + + +class AddFulfillmentPlacesMetadata(proto.Message): + r"""Metadata related to the progress of the AddFulfillmentPlaces + operation. Currently empty because there is no meaningful metadata + populated from the [AddFulfillmentPlaces][] method. + """ + + +class AddFulfillmentPlacesResponse(proto.Message): + r"""Response of the RemoveFulfillmentPlacesRequest. Currently empty + because there is no meaningful response populated from the + [AddFulfillmentPlaces][] method. + """ + + +class RemoveFulfillmentPlacesRequest(proto.Message): + r"""Request message for [RemoveFulfillmentPlaces][] method. + Attributes: + product (str): + Required. Full resource name of + [Product][google.cloud.retail.v2.Product], such as + ``projects/*/locations/global/catalogs/default_catalog/branches/default_branch/products/some_product_id``. + + If the caller does not have permission to access the + [Product][google.cloud.retail.v2.Product], regardless of + whether or not it exists, a PERMISSION_DENIED error is + returned. + type_ (str): + Required. The fulfillment type, including commonly used + types (such as pickup in store and same day delivery), and + custom types. + + Supported values: + + - "pickup-in-store" + - "ship-to-store" + - "same-day-delivery" + - "next-day-delivery" + - "custom-type-1" + - "custom-type-2" + - "custom-type-3" + - "custom-type-4" + - "custom-type-5" + + If this field is set to an invalid value other than these, + an INVALID_ARGUMENT error is returned. + + This field directly corresponds to + [Product.fulfillment_info.type][]. + place_ids (Sequence[str]): + Required. The IDs for this + [type][google.cloud.retail.v2.RemoveFulfillmentPlacesRequest.type], + such as the store IDs for "pickup-in-store" or the region + IDs for "same-day-delivery", to be removed for this + [type][google.cloud.retail.v2.RemoveFulfillmentPlacesRequest.type]. + + At least 1 value is required, and a maximum of 2000 values + are allowed. Each value must be a string with a length limit + of 10 characters, matching the pattern [a-zA-Z0-9\_-]+, such + as "store1" or "REGION-2". Otherwise, an INVALID_ARGUMENT + error is returned. + remove_time (google.protobuf.timestamp_pb2.Timestamp): + The time when the fulfillment updates are + issued, used to prevent out-of-order updates on + fulfillment information. If not provided, the + internal system time will be used. + allow_missing (bool): + If set to true, and the + [Product][google.cloud.retail.v2.Product] is not found, the + fulfillment information will still be processed and retained + for at most 1 day and processed once the + [Product][google.cloud.retail.v2.Product] is created. If set + to false, an INVALID_ARGUMENT error is returned if the + [Product][google.cloud.retail.v2.Product] is not found. + """ + + product = proto.Field(proto.STRING, number=1,) + type_ = proto.Field(proto.STRING, number=2,) + place_ids = proto.RepeatedField(proto.STRING, number=3,) + remove_time = proto.Field(proto.MESSAGE, number=4, message=timestamp_pb2.Timestamp,) + allow_missing = proto.Field(proto.BOOL, number=5,) + + +class RemoveFulfillmentPlacesMetadata(proto.Message): + r"""Metadata related to the progress of the RemoveFulfillmentPlaces + operation. Currently empty because there is no meaningful metadata + populated from the [RemoveFulfillmentPlaces][] method. + """ + + +class RemoveFulfillmentPlacesResponse(proto.Message): + r"""Response of the RemoveFulfillmentPlacesRequest. Currently empty + because there is no meaningful response populated from the + [RemoveFulfillmentPlaces][] method. + """ + + __all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/retail_v2/types/purge_config.py b/google/cloud/retail_v2/types/purge_config.py index c293cc0e..9a69c35f 100644 --- a/google/cloud/retail_v2/types/purge_config.py +++ b/google/cloud/retail_v2/types/purge_config.py @@ -33,10 +33,9 @@ class PurgeUserEventsRequest(proto.Message): r"""Request message for PurgeUserEvents method. Attributes: parent (str): - Required. The resource name of the catalog - under which the events are created. The format - is - "projects/${projectId}/locations/global/catalogs/${catalogId}". + Required. The resource name of the catalog under which the + events are created. The format is + ``projects/${projectId}/locations/global/catalogs/${catalogId}`` filter (str): Required. The filter string to specify the events to be deleted with a length limit of 5,000 characters. Empty diff --git a/google/cloud/retail_v2/types/search_service.py b/google/cloud/retail_v2/types/search_service.py new file mode 100644 index 00000000..8277cada --- /dev/null +++ b/google/cloud/retail_v2/types/search_service.py @@ -0,0 +1,782 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import proto # type: ignore + +from google.cloud.retail_v2.types import common +from google.cloud.retail_v2.types import product as gcr_product +from google.protobuf import field_mask_pb2 # type: ignore +from google.protobuf import struct_pb2 # type: ignore + + +__protobuf__ = proto.module( + package="google.cloud.retail.v2", manifest={"SearchRequest", "SearchResponse",}, +) + + +class SearchRequest(proto.Message): + r"""Request message for + [SearchService.Search][google.cloud.retail.v2.SearchService.Search] + method. + + Attributes: + placement (str): + Required. The resource name of the search engine placement, + such as + ``projects/*/locations/global/catalogs/default_catalog/placements/default_search``. + This field is used to identify the set of models that will + be used to make the search. + + We currently support one placement with the following ID: + + - ``default_search``. + branch (str): + The branch resource name, such as + ``projects/*/locations/global/catalogs/default_catalog/branches/0``. + + Use "default_branch" as the branch ID or leave this field + empty, to search products under the default branch. + query (str): + Raw search query. + visitor_id (str): + Required. A unique identifier for tracking visitors. For + example, this could be implemented with an HTTP cookie, + which should be able to uniquely identify a visitor on a + single device. This unique identifier should not change if + the visitor logs in or out of the website. + + The field must be a UTF-8 encoded string with a length limit + of 128 characters. Otherwise, an INVALID_ARGUMENT error is + returned. + user_info (google.cloud.retail_v2.types.UserInfo): + User information. + page_size (int): + Maximum number of [Product][google.cloud.retail.v2.Product]s + to return. If unspecified, defaults to a reasonable value. + The maximum allowed value is 120. Values above 120 will be + coerced to 120. + + If this field is negative, an INVALID_ARGUMENT is returned. + page_token (str): + A page token + [SearchResponse.next_page_token][google.cloud.retail.v2.SearchResponse.next_page_token], + received from a previous + [SearchService.Search][google.cloud.retail.v2.SearchService.Search] + call. Provide this to retrieve the subsequent page. + + When paginating, all other parameters provided to + [SearchService.Search][google.cloud.retail.v2.SearchService.Search] + must match the call that provided the page token. Otherwise, + an INVALID_ARGUMENT error is returned. + offset (int): + A 0-indexed integer that specifies the current offset (that + is, starting result location, amongst the + [Product][google.cloud.retail.v2.Product]s deemed by the API + as relevant) in search results. This field is only + considered if + [page_token][google.cloud.retail.v2.SearchRequest.page_token] + is unset. + + If this field is negative, an INVALID_ARGUMENT is returned. + filter (str): + The filter syntax consists of an expression language for + constructing a predicate from one or more fields of the + products being filtered. Filter expression is + case-sensitive. See more details at this `user + guide `__. + + If this field is unrecognizable, an INVALID_ARGUMENT is + returned. + canonical_filter (str): + The filter applied to every search request when quality + improvement such as query expansion is needed. For example, + if a query does not have enough results, an expanded query + with + [SearchRequest.canonical_filter][google.cloud.retail.v2.SearchRequest.canonical_filter] + will be returned as a supplement of the original query. This + field is strongly recommended to achieve high search + quality. + + See + [SearchRequest.filter][google.cloud.retail.v2.SearchRequest.filter] + for more details about filter syntax. + order_by (str): + The order in which products are returned. Products can be + ordered by a field in an + [Product][google.cloud.retail.v2.Product] object. Leave it + unset if ordered by relevance. OrderBy expression is + case-sensitive. See more details at this `user + guide `__. + + If this field is unrecognizable, an INVALID_ARGUMENT is + returned. + facet_specs (Sequence[google.cloud.retail_v2.types.SearchRequest.FacetSpec]): + Facet specifications for faceted search. If empty, no facets + are returned. + + A maximum of 100 values are allowed. Otherwise, an + INVALID_ARGUMENT error is returned. + dynamic_facet_spec (google.cloud.retail_v2.types.SearchRequest.DynamicFacetSpec): + The specification for dynamically generated + facets. Notice that only textual facets can be + dynamically generated. + This feature requires additional allowlisting. + Contact Retail Support (retail-search- + support@google.com) if you are interested in + using dynamic facet feature. + boost_spec (google.cloud.retail_v2.types.SearchRequest.BoostSpec): + Boost specification to boost certain products. See more + details at this `user + guide `__. + query_expansion_spec (google.cloud.retail_v2.types.SearchRequest.QueryExpansionSpec): + The query expansion specification that specifies the + conditions under which query expansion will occur. See more + details at this `user + guide `__. + variant_rollup_keys (Sequence[str]): + The keys to fetch and rollup the matching + [variant][google.cloud.retail.v2.Product.Type.VARIANT] + [Product][google.cloud.retail.v2.Product]s attributes. The + attributes from all the matching + [variant][google.cloud.retail.v2.Product.Type.VARIANT] + [Product][google.cloud.retail.v2.Product]s are merged and + de-duplicated. Notice that rollup + [variant][google.cloud.retail.v2.Product.Type.VARIANT] + [Product][google.cloud.retail.v2.Product]s attributes will + lead to extra query latency. Maximum number of keys is 10. + + For + [Product.fulfillment_info][google.cloud.retail.v2.Product.fulfillment_info], + a fulfillment type and a fulfillment ID must be provided in + the format of "fulfillmentType.filfillmentId". E.g., in + "pickupInStore.store123", "pickupInStore" is fulfillment + type and "store123" is the store ID. + + Supported keys are: + + - colorFamilies + - price + - originalPrice + - discount + - attributes.key, where key is any key in the + [Product.attributes][google.cloud.retail.v2.Product.attributes] + map. + - pickupInStore.id, where id is any [FulfillmentInfo.ids][] + for type [FulfillmentInfo.Type.PICKUP_IN_STORE][]. + - shipToStore.id, where id is any [FulfillmentInfo.ids][] + for type [FulfillmentInfo.Type.SHIP_TO_STORE][]. + - sameDayDelivery.id, where id is any + [FulfillmentInfo.ids][] for type + [FulfillmentInfo.Type.SAME_DAY_DELIVERY][]. + - nextDayDelivery.id, where id is any + [FulfillmentInfo.ids][] for type + [FulfillmentInfo.Type.NEXT_DAY_DELIVERY][]. + - customFulfillment1.id, where id is any + [FulfillmentInfo.ids][] for type + [FulfillmentInfo.Type.CUSTOM_TYPE_1][]. + - customFulfillment2.id, where id is any + [FulfillmentInfo.ids][] for type + [FulfillmentInfo.Type.CUSTOM_TYPE_2][]. + - customFulfillment3.id, where id is any + [FulfillmentInfo.ids][] for type + [FulfillmentInfo.Type.CUSTOM_TYPE_3][]. + - customFulfillment4.id, where id is any + [FulfillmentInfo.ids][] for type + [FulfillmentInfo.Type.CUSTOM_TYPE_4][]. + - customFulfillment5.id, where id is any + [FulfillmentInfo.ids][] for type + [FulfillmentInfo.Type.CUSTOM_TYPE_5][]. + + If this field is set to an invalid value other than these, + an INVALID_ARGUMENT error is returned. + page_categories (Sequence[str]): + The categories associated with a category page. Required for + category navigation queries to achieve good search quality. + The format should be the same as + [UserEvent.page_categories][google.cloud.retail.v2.UserEvent.page_categories]; + + To represent full path of category, use '>' sign to separate + different hierarchies. If '>' is part of the category name, + please replace it with other character(s). + + Category pages include special pages such as sales or + promotions. For instance, a special sale page may have the + category hierarchy: "pageCategories" : ["Sales > 2017 Black + Friday Deals"]. + """ + + class FacetSpec(proto.Message): + r"""A facet specification to perform faceted search. + Attributes: + facet_key (google.cloud.retail_v2.types.SearchRequest.FacetSpec.FacetKey): + Required. The facet key specification. + limit (int): + Maximum of facet values that should be returned for this + facet. If unspecified, defaults to 20. The maximum allowed + value is 300. Values above 300 will be coerced to 300. + + If this field is negative, an INVALID_ARGUMENT is returned. + excluded_filter_keys (Sequence[str]): + List of keys to exclude when faceting. + + By default, + [FacetKey.key][google.cloud.retail.v2.SearchRequest.FacetSpec.FacetKey.key] + is not excluded from the filter unless it is listed in this + field. + + For example, suppose there are 100 products with color facet + "Red" and 200 products with color facet "Blue". A query + containing the filter "colorFamilies:ANY("Red")" and have + "colorFamilies" as + [FacetKey.key][google.cloud.retail.v2.SearchRequest.FacetSpec.FacetKey.key] + will by default return the "Red" with count 100. + + If this field contains "colorFamilies", then the query + returns both the "Red" with count 100 and "Blue" with count + 200, because the "colorFamilies" key is now excluded from + the filter. + + A maximum of 100 values are allowed. Otherwise, an + INVALID_ARGUMENT error is returned. + enable_dynamic_position (bool): + Enables dynamic position for this facet. If set to true, the + position of this facet among all facets in the response is + determined by Google Retail Search. It will be ordered + together with dynamic facets if dynamic facets is enabled. + If set to false, the position of this facet in the response + will be the same as in the request, and it will be ranked + before the facets with dynamic position enable and all + dynamic facets. + + For example, you may always want to have rating facet + returned in the response, but it's not necessarily to always + display the rating facet at the top. In that case, you can + set enable_dynamic_position to true so that the position of + rating facet in response will be determined by Google Retail + Search. + + Another example, assuming you have the following facets in + the request: + + - "rating", enable_dynamic_position = true + + - "price", enable_dynamic_position = false + + - "brands", enable_dynamic_position = false + + And also you have a dynamic facets enable, which will + generate a facet 'gender'. Then the final order of the + facets in the response can be ("price", "brands", "rating", + "gender") or ("price", "brands", "gender", "rating") depends + on how Google Retail Search orders "gender" and "rating" + facets. However, notice that "price" and "brands" will + always be ranked at 1st and 2nd position since their + enable_dynamic_position are false. + """ + + class FacetKey(proto.Message): + r"""Specifies how a facet is computed. + Attributes: + key (str): + Required. Supported textual and numerical facet keys in + [Product][google.cloud.retail.v2.Product] object, over which + the facet values are computed. Facet key is case-sensitive. + + Allowed facet keys when + [FacetKey.query][google.cloud.retail.v2.SearchRequest.FacetSpec.FacetKey.query] + is not specified: + + - textual_field = *# The + [Product.brands][google.cloud.retail.v2.Product.brands].* + "brands"; *# The + [Product.categories][google.cloud.retail.v2.Product.categories].* + "categories"; *# The + [Audience.genders][google.cloud.retail.v2.Audience.genders].* + \| "genders"; *# The + [Audience.age_groups][google.cloud.retail.v2.Audience.age_groups].* + \| "ageGroups"; *# The + [Product.availability][google.cloud.retail.v2.Product.availability]. + Value is one of* *# "IN_STOCK", "OUT_OF_STOCK", + PREORDER", "BACKORDER".* \| "availability"; *# The + [ColorInfo.color_families][google.cloud.retail.v2.ColorInfo.color_families].* + \| "colorFamilies"; *# The + [ColorInfo.colors][google.cloud.retail.v2.ColorInfo.colors].* + \| "colors"; *# The + [Product.sizes][google.cloud.retail.v2.Product.sizes].* + \| "sizes"; *# The + [Product.materials][google.cloud.retail.v2.Product.materials].* + \| "materials"; *# The + [Product.patterns][google.cloud.retail.v2.Product.patterns].* + \| "patterns"; *# The + [Product.conditions][google.cloud.retail.v2.Product.conditions].* + \| "conditions"; *# The textual custom attribute in + [Product][google.cloud.retail.v2.Product] object. Key + can* *# be any key in the + [Product.attributes][google.cloud.retail.v2.Product.attributes] + map* *# if the attribute values are textual.* *# map.* \| + "attributes.key"; \*# The [FulfillmentInfo.ids][] for + type *# [FulfillmentInfo.Type.PICKUP_IN_STORE][].* \| + "pickupInStore"; \*# The [FulfillmentInfo.ids][] for type + *# [FulfillmentInfo.Type.SHIP_TO_STORE][].* \| + "shipToStore"; \*# The [FulfillmentInfo.ids][] for type + *# [FulfillmentInfo.Type.SAME_DAY_DELIVERY][].* \| + "sameDayDelivery"; \*# The [FulfillmentInfo.ids][] for + type *# [FulfillmentInfo.Type.NEXT_DAY_DELIVERY][].* \| + "nextDayDelivery"; \*# The [FulfillmentInfo.ids][] for + type *# [FulfillmentInfo.Type.CUSTOM_TYPE_1][].* \| + "customFulfillment1"; \*# The [FulfillmentInfo.ids][] for + type *# [FulfillmentInfo.Type.CUSTOM_TYPE_2][].* \| + "customFulfillment2"; \*# The [FulfillmentInfo.ids][] for + type *# [FulfillmentInfo.Type.CUSTOM_TYPE_3][].* \| + "customFulfillment3"; \*# The [FulfillmentInfo.ids][] for + type *# [FulfillmentInfo.Type.CUSTOM_TYPE_4][].* \| + "customFulfillment4"; \*# The [FulfillmentInfo.ids][] for + type *# [FulfillmentInfo.Type.CUSTOM_TYPE_5][].* \| + "customFulfillment5"; + + - numerical_field = *# The + [PriceInfo.price][google.cloud.retail.v2.PriceInfo.price].* + "price"; *# The discount. Computed by + (original_price-price)/price * "discount"; *# The + [Rating.average_rating][google.cloud.retail.v2.Rating.average_rating].* + "rating"; *# The + [Rating.rating_count][google.cloud.retail.v2.Rating.rating_count].* + "ratingCount"; *# The numerical custom attribute in + [Product][google.cloud.retail.v2.Product] object. Key + can* *# be any key in the + [Product.attributes][google.cloud.retail.v2.Product.attributes] + map* *# if the attribute values are numerical.* \| + "attributes.key"; + intervals (Sequence[google.cloud.retail_v2.types.Interval]): + Set only if values should be bucketized into + intervals. Must be set for facets with numerical + values. Must not be set for facet with text + values. Maximum number of intervals is 30. + restricted_values (Sequence[str]): + Only get facet for the given restricted values. For example, + when using "pickupInStore" as key and set restricted values + to ["store123", "store456"], only facets for "store123" and + "store456" are returned. Only supported on textual fields + and fulfillments. Maximum is 20. + + Must be set for the fulfillment facet keys: + + - pickupInStore + + - shipToStore + + - sameDayDelivery + + - nextDayDelivery + + - customFulfillment1 + + - customFulfillment2 + + - customFulfillment3 + + - customFulfillment4 + + - customFulfillment5 + prefixes (Sequence[str]): + Only get facet values that start with the + given string prefix. For example, suppose + "categories" has three values "Women > Shoe", + "Women > Dress" and "Men > Shoe". If set + "prefixes" to "Women", the "categories" facet + will give only "Women > Shoe" and "Women > + Dress". Only supported on textual fields. + Maximum is 10. + contains (Sequence[str]): + Only get facet values that contains the given + strings. For example, suppose "categories" has + three values "Women > Shoe", "Women > Dress" and + "Men > Shoe". If set "contains" to "Shoe", the + "categories" facet will give only "Women > Shoe" + and "Men > Shoe". Only supported on textual + fields. Maximum is 10. + order_by (str): + The order in which [Facet.values][] are returned. + + Allowed values are: + + - "count desc", which means order by + [Facet.FacetValue.count][] descending. + + - "value desc", which means order by + [Facet.FacetValue.value][] descending. Only applies to + textual facets. + + If not set, textual values are sorted in `natural + order `__; + numerical intervals are sorted in the order given by + [FacetSpec.FacetKey.intervals][google.cloud.retail.v2.SearchRequest.FacetSpec.FacetKey.intervals]; + [FulfillmentInfo.ids][] are sorted in the order given by + [FacetSpec.FacetKey.restricted_values][google.cloud.retail.v2.SearchRequest.FacetSpec.FacetKey.restricted_values]. + query (str): + The query that is used to compute facet for the given facet + key. When provided, it will override the default behavior of + facet computation. The query syntax is the same as a filter + expression. See + [SearchRequest.filter][google.cloud.retail.v2.SearchRequest.filter] + for detail syntax and limitations. Notice that there is no + limitation on + [FacetKey.key][google.cloud.retail.v2.SearchRequest.FacetSpec.FacetKey.key] + when query is specified. + + In the response, [FacetValue.value][] will be always "1" and + [FacetValue.count][] will be the number of results that + matches the query. + + For example, you can set a customized facet for + "shipToStore", where + [FacetKey.key][google.cloud.retail.v2.SearchRequest.FacetSpec.FacetKey.key] + is "customizedShipToStore", and + [FacetKey.query][google.cloud.retail.v2.SearchRequest.FacetSpec.FacetKey.query] + is "availability: ANY("IN_STOCK") AND shipToStore: + ANY("123")". Then the facet will count the products that are + both in stock and ship to store "123". + """ + + key = proto.Field(proto.STRING, number=1,) + intervals = proto.RepeatedField( + proto.MESSAGE, number=2, message=common.Interval, + ) + restricted_values = proto.RepeatedField(proto.STRING, number=3,) + prefixes = proto.RepeatedField(proto.STRING, number=8,) + contains = proto.RepeatedField(proto.STRING, number=9,) + order_by = proto.Field(proto.STRING, number=4,) + query = proto.Field(proto.STRING, number=5,) + + facet_key = proto.Field( + proto.MESSAGE, number=1, message="SearchRequest.FacetSpec.FacetKey", + ) + limit = proto.Field(proto.INT32, number=2,) + excluded_filter_keys = proto.RepeatedField(proto.STRING, number=3,) + enable_dynamic_position = proto.Field(proto.BOOL, number=4,) + + class DynamicFacetSpec(proto.Message): + r"""The specifications of dynamically generated facets. + Attributes: + mode (google.cloud.retail_v2.types.SearchRequest.DynamicFacetSpec.Mode): + Mode of the DynamicFacet feature. Defaults to + [Mode.DISABLED][google.cloud.retail.v2.SearchRequest.DynamicFacetSpec.Mode.DISABLED] + if it's unset. + """ + + class Mode(proto.Enum): + r"""Enum to control DynamicFacet mode""" + MODE_UNSPECIFIED = 0 + DISABLED = 1 + ENABLED = 2 + + mode = proto.Field( + proto.ENUM, number=1, enum="SearchRequest.DynamicFacetSpec.Mode", + ) + + class BoostSpec(proto.Message): + r"""Boost specification to boost certain items. + Attributes: + condition_boost_specs (Sequence[google.cloud.retail_v2.types.SearchRequest.BoostSpec.ConditionBoostSpec]): + Condition boost specifications. If a product + matches multiple conditions in the + specifictions, boost scores from these + specifications are all applied and combined in a + non-linear way. Maximum number of specifications + is 10. + """ + + class ConditionBoostSpec(proto.Message): + r"""Boost applies to products which match a condition. + Attributes: + condition (str): + An expression which specifies a boost condition. The syntax + and supported fields are the same as a filter expression. + See + [SearchRequest.filter][google.cloud.retail.v2.SearchRequest.filter] + for detail syntax and limitations. + + Examples: + + - To boost products with product ID "product_1" or + "product_2", and color "Red" or "Blue": *(id: + ANY("product_1", "product_2"))* *AND* *(colorFamilies: + ANY("Red", "Blue"))* + boost (float): + Strength of the condition boost, which should be in [-1, 1]. + Negative boost means demotion. Default is 0.0. + + Setting to 1.0 gives the item a big promotion. However, it + does not necessarily mean that the boosted item will be the + top result at all times, nor that other items will be + excluded. Results could still be shown even when none of + them matches the condition. And results that are + significantly more relevant to the search query can still + trump your heavily favored but irrelevant items. + + Setting to -1.0 gives the item a big demotion. However, + results that are deeply relevant might still be shown. The + item will have an upstream battle to get a fairly high + ranking, but it is not blocked out completely. + + Setting to 0.0 means no boost applied. The boosting + condition is ignored. + """ + + condition = proto.Field(proto.STRING, number=1,) + boost = proto.Field(proto.FLOAT, number=2,) + + condition_boost_specs = proto.RepeatedField( + proto.MESSAGE, + number=1, + message="SearchRequest.BoostSpec.ConditionBoostSpec", + ) + + class QueryExpansionSpec(proto.Message): + r"""Specification to determine under which conditions query + expansion should occur. + + Attributes: + condition (google.cloud.retail_v2.types.SearchRequest.QueryExpansionSpec.Condition): + The condition under which query expansion should occur. + Default to + [Condition.DISABLED][google.cloud.retail.v2.SearchRequest.QueryExpansionSpec.Condition.DISABLED]. + """ + + class Condition(proto.Enum): + r"""Enum describing under which condition query expansion should + occur. + """ + CONDITION_UNSPECIFIED = 0 + DISABLED = 1 + AUTO = 3 + + condition = proto.Field( + proto.ENUM, number=1, enum="SearchRequest.QueryExpansionSpec.Condition", + ) + + placement = proto.Field(proto.STRING, number=1,) + branch = proto.Field(proto.STRING, number=2,) + query = proto.Field(proto.STRING, number=3,) + visitor_id = proto.Field(proto.STRING, number=4,) + user_info = proto.Field(proto.MESSAGE, number=5, message=common.UserInfo,) + page_size = proto.Field(proto.INT32, number=7,) + page_token = proto.Field(proto.STRING, number=8,) + offset = proto.Field(proto.INT32, number=9,) + filter = proto.Field(proto.STRING, number=10,) + canonical_filter = proto.Field(proto.STRING, number=28,) + order_by = proto.Field(proto.STRING, number=11,) + facet_specs = proto.RepeatedField(proto.MESSAGE, number=12, message=FacetSpec,) + dynamic_facet_spec = proto.Field( + proto.MESSAGE, number=21, message=DynamicFacetSpec, + ) + boost_spec = proto.Field(proto.MESSAGE, number=13, message=BoostSpec,) + query_expansion_spec = proto.Field( + proto.MESSAGE, number=14, message=QueryExpansionSpec, + ) + variant_rollup_keys = proto.RepeatedField(proto.STRING, number=17,) + page_categories = proto.RepeatedField(proto.STRING, number=23,) + + +class SearchResponse(proto.Message): + r"""Response message for + [SearchService.Search][google.cloud.retail.v2.SearchService.Search] + method. + + Attributes: + results (Sequence[google.cloud.retail_v2.types.SearchResponse.SearchResult]): + A list of matched items. The order represents + the ranking. + facets (Sequence[google.cloud.retail_v2.types.SearchResponse.Facet]): + Results of facets requested by user. + total_size (int): + The estimated total count of matched items irrespective of + pagination. The count of + [results][google.cloud.retail.v2.SearchResponse.results] + returned by pagination may be less than the + [total_size][google.cloud.retail.v2.SearchResponse.total_size] + that matches. + corrected_query (str): + If spell correction applies, the corrected + query. Otherwise, empty. + attribution_token (str): + A unique search token. This should be included in the + [UserEvent][google.cloud.retail.v2.UserEvent] logs resulting + from this search, which enables accurate attribution of + search model performance. + next_page_token (str): + A token that can be sent as + [SearchRequest.page_token][google.cloud.retail.v2.SearchRequest.page_token] + to retrieve the next page. If this field is omitted, there + are no subsequent pages. + query_expansion_info (google.cloud.retail_v2.types.SearchResponse.QueryExpansionInfo): + Query expansion information for the returned + results. + redirect_uri (str): + The URI of a customer-defined redirect page. If redirect + action is triggered, no search will be performed, and only + [redirect_uri][google.cloud.retail.v2.SearchResponse.redirect_uri] + and + [attribution_token][google.cloud.retail.v2.SearchResponse.attribution_token] + will be set in the response. + """ + + class SearchResult(proto.Message): + r"""Represents the search results. + Attributes: + id (str): + [Product.id][google.cloud.retail.v2.Product.id] of the + searched [Product][google.cloud.retail.v2.Product]. + product (google.cloud.retail_v2.types.Product): + The product data snippet in the search response. Only + [Product.name][google.cloud.retail.v2.Product.name] is + guaranteed to be populated. + + [Product.variants][google.cloud.retail.v2.Product.variants] + contains the product variants that match the search query. + If there are multiple product variants matching the query, + top 5 most relevant product variants are returned and + ordered by relevancy. + + If relevancy can be deternmined, use + [matching_variant_fields][google.cloud.retail.v2.SearchResponse.SearchResult.matching_variant_fields] + to look up matched product variants fields. If relevancy + cannot be determined, e.g. when searching "shoe" all + products in a shoe product can be a match, 5 product + variants are returned but order is meaningless. + matching_variant_count (int): + The count of matched + [variant][google.cloud.retail.v2.Product.Type.VARIANT] + [Product][google.cloud.retail.v2.Product]s. + matching_variant_fields (Sequence[google.cloud.retail_v2.types.SearchResponse.SearchResult.MatchingVariantFieldsEntry]): + If a [variant][google.cloud.retail.v2.Product.Type.VARIANT] + [Product][google.cloud.retail.v2.Product] matches the search + query, this map indicates which + [Product][google.cloud.retail.v2.Product] fields are + matched. The key is the + [Product.name][google.cloud.retail.v2.Product.name], the + value is a field mask of the matched + [Product][google.cloud.retail.v2.Product] fields. If matched + attributes cannot be determined, this map will be empty. + + For example, a key "sku1" with field mask + "products.color_info" indicates there is a match between + "sku1" [ColorInfo][google.cloud.retail.v2.ColorInfo] and the + query. + variant_rollup_values (Sequence[google.cloud.retail_v2.types.SearchResponse.SearchResult.VariantRollupValuesEntry]): + The rollup matching + [variant][google.cloud.retail.v2.Product.Type.VARIANT] + [Product][google.cloud.retail.v2.Product] attributes. The + key is one of the + [SearchRequest.variant_rollup_keys][google.cloud.retail.v2.SearchRequest.variant_rollup_keys]. + The values are the merged and de-duplicated + [Product][google.cloud.retail.v2.Product] attributes. Notice + that the rollup values are respect filter. For example, when + filtering by "colorFamilies:ANY("red")" and rollup + "colorFamilies", only "red" is returned. + + For textual and numerical attributes, the rollup values is a + list of string or double values with type + [google.protobuf.ListValue][google.protobuf.ListValue]. For + example, if there are two variants with colors "red" and + "blue", the rollup values are { key: "colorFamilies" value { + list_value { values { string_value: "red" } values { + string_value: "blue" } } } } + + For + [Product.fulfillment_info][google.cloud.retail.v2.Product.fulfillment_info], + the rollup values is a double value with type + [google.protobuf.Value][google.protobuf.Value]. For example, + {key: "pickupInStore.store1" value { number_value: 10 }} + means a there are 10 variants in this product are available + in the store "store1". + """ + + id = proto.Field(proto.STRING, number=1,) + product = proto.Field(proto.MESSAGE, number=2, message=gcr_product.Product,) + matching_variant_count = proto.Field(proto.INT32, number=3,) + matching_variant_fields = proto.MapField( + proto.STRING, proto.MESSAGE, number=4, message=field_mask_pb2.FieldMask, + ) + variant_rollup_values = proto.MapField( + proto.STRING, proto.MESSAGE, number=5, message=struct_pb2.Value, + ) + + class Facet(proto.Message): + r"""A facet result. + Attributes: + key (str): + The key for this facet. E.g., "colorFamilies" + or "price" or "attributes.attr1". + values (Sequence[google.cloud.retail_v2.types.SearchResponse.Facet.FacetValue]): + The facet values for this field. + dynamic_facet (bool): + Whether the facet is dynamically generated. + """ + + class FacetValue(proto.Message): + r"""A facet value which contains value names and their count. + Attributes: + value (str): + Text value of a facet, such as "Black" for + facet "colorFamilies". + interval (google.cloud.retail_v2.types.Interval): + Interval value for a facet, such as [10, 20) for facet + "price". + count (int): + Number of items that have this facet value. + """ + + value = proto.Field(proto.STRING, number=1, oneof="facet_value",) + interval = proto.Field( + proto.MESSAGE, number=2, oneof="facet_value", message=common.Interval, + ) + count = proto.Field(proto.INT64, number=3,) + + key = proto.Field(proto.STRING, number=1,) + values = proto.RepeatedField( + proto.MESSAGE, number=2, message="SearchResponse.Facet.FacetValue", + ) + dynamic_facet = proto.Field(proto.BOOL, number=3,) + + class QueryExpansionInfo(proto.Message): + r"""Information describing query expansion including whether + expansion has occurred. + + Attributes: + expanded_query (bool): + Bool describing whether query expansion has + occurred. + """ + + expanded_query = proto.Field(proto.BOOL, number=1,) + + @property + def raw_page(self): + return self + + results = proto.RepeatedField(proto.MESSAGE, number=1, message=SearchResult,) + facets = proto.RepeatedField(proto.MESSAGE, number=2, message=Facet,) + total_size = proto.Field(proto.INT32, number=3,) + corrected_query = proto.Field(proto.STRING, number=4,) + attribution_token = proto.Field(proto.STRING, number=5,) + next_page_token = proto.Field(proto.STRING, number=6,) + query_expansion_info = proto.Field( + proto.MESSAGE, number=7, message=QueryExpansionInfo, + ) + redirect_uri = proto.Field(proto.STRING, number=10,) + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/google/cloud/retail_v2/types/user_event.py b/google/cloud/retail_v2/types/user_event.py index 73ab4ccb..377d99ab 100644 --- a/google/cloud/retail_v2/types/user_event.py +++ b/google/cloud/retail_v2/types/user_event.py @@ -23,7 +23,7 @@ __protobuf__ = proto.module( package="google.cloud.retail.v2", - manifest={"UserEvent", "ProductDetail", "PurchaseTransaction",}, + manifest={"UserEvent", "ProductDetail", "CompletionDetail", "PurchaseTransaction",}, ) @@ -38,8 +38,12 @@ class UserEvent(proto.Message): - ``add-to-cart``: Products being added to cart. - ``category-page-view``: Special pages such as sale or promotion pages viewed. + - ``completion``: Completion query result showed/clicked. - ``detail-page-view``: Products detail page viewed. - ``home-page-view``: Homepage viewed. + - ``promotion-offered``: Promotion is offered to a user. + - ``promotion-not-offered``: Promotion is not offered to a + user. - ``purchase-complete``: User finishing a purchase. - ``search``: Product search. - ``shopping-cart-page-view``: User viewing a shopping @@ -55,6 +59,22 @@ class UserEvent(proto.Message): The field must be a UTF-8 encoded string with a length limit of 128 characters. Otherwise, an INVALID_ARGUMENT error is returned. + + The field should not contain PII or user-data. We recommend + to use Google Analystics `Client + ID `__ + for this field. + session_id (str): + A unique identifier for tracking a visitor session with a + length limit of 128 bytes. A session is an aggregation of an + end user behavior in a time span. + + A general guideline to populate the sesion_id: + + 1. If user has no activity for 30 min, a new session_id + should be assigned. + 2. The session_id should be unique across users, suggest use + uuid or add visitor_id as prefix. event_time (google.protobuf.timestamp_pb2.Timestamp): Only required for [UserEventService.ImportUserEvents][google.cloud.retail.v2.UserEventService.ImportUserEvents] @@ -76,6 +96,10 @@ class UserEvent(proto.Message): [PredictResponse.attribution_token][google.cloud.retail.v2.PredictResponse.attribution_token] for user events that are the result of [PredictionService.Predict][google.cloud.retail.v2.PredictionService.Predict]. + The value must be a valid + [SearchResponse.attribution_token][google.cloud.retail.v2.SearchResponse.attribution_token] + for user events that are the result of + [SearchService.Search][google.cloud.retail.v2.SearchService.Search]. This token enables us to accurately attribute page view or purchase back to the event and the particular predict @@ -104,6 +128,14 @@ class UserEvent(proto.Message): [product_details][google.cloud.retail.v2.UserEvent.product_details] is desired. The end user may have not finished broswing the whole page yet. + completion_detail (google.cloud.retail_v2.types.CompletionDetail): + The main completion details related to the event. + + In a ``completion`` event, this field represents the + completions returned to the end user and the clicked + completion by the end user. In a ``search`` event, it + represents the search event happens after clicking + completion. attributes (Sequence[google.cloud.retail_v2.types.UserEvent.AttributesEntry]): Extra user event features to include in the recommendation model. @@ -133,13 +165,61 @@ class UserEvent(proto.Message): search_query (str): The user's search query. + See + [SearchRequest.query][google.cloud.retail.v2.SearchRequest.query] + for definition. + The value must be a UTF-8 encoded string with a length limit of 5,000 characters. Otherwise, an INVALID_ARGUMENT error is returned. - Required for ``search`` events. Other event types should not - set this field. Otherwise, an INVALID_ARGUMENT error is + At least one of + [search_query][google.cloud.retail.v2.UserEvent.search_query] + or + [page_categories][google.cloud.retail.v2.UserEvent.page_categories] + is required for ``search`` events. Other event types should + not set this field. Otherwise, an INVALID_ARGUMENT error is + returned. + filter (str): + The filter syntax consists of an expression language for + constructing a predicate from one or more fields of the + products being filtered. + + See + [SearchRequest.filter][google.cloud.retail.v2.SearchRequest.filter] + for definition and syntax. + + The value must be a UTF-8 encoded string with a length limit + of 1,000 characters. Otherwise, an INVALID_ARGUMENT error is returned. + order_by (str): + The order in which products are returned. + + See + [SearchRequest.order_by][google.cloud.retail.v2.SearchRequest.order_by] + for definition and syntax. + + The value must be a UTF-8 encoded string with a length limit + of 1,000 characters. Otherwise, an INVALID_ARGUMENT error is + returned. + + This can only be set for ``search`` events. Other event + types should not set this field. Otherwise, an + INVALID_ARGUMENT error is returned. + offset (int): + An integer that specifies the current offset for pagination + (the 0-indexed starting location, amongst the products + deemed by the API as relevant). + + See + [SearchRequest.offset][google.cloud.retail.v2.SearchRequest.offset] + for definition. + + If this field is negative, an INVALID_ARGUMENT is returned. + + This can only be set for ``search`` events. Other event + types should not set this field. Otherwise, an + INVALID_ARGUMENT error is returned. page_categories (Sequence[str]): The categories associated with a category page. @@ -152,9 +232,13 @@ class UserEvent(proto.Message): category hierarchy: "pageCategories" : ["Sales > 2017 Black Friday Deals"]. - Required for ``category-page-view`` events. Other event - types should not set this field. Otherwise, an - INVALID_ARGUMENT error is returned. + Required for ``category-page-view`` events. At least one of + [search_query][google.cloud.retail.v2.UserEvent.search_query] + or + [page_categories][google.cloud.retail.v2.UserEvent.page_categories] + is required for ``search`` events. Other event types should + not set this field. Otherwise, an INVALID_ARGUMENT error is + returned. user_info (google.cloud.retail_v2.types.UserInfo): User information. uri (str): @@ -186,12 +270,16 @@ class UserEvent(proto.Message): event_type = proto.Field(proto.STRING, number=1,) visitor_id = proto.Field(proto.STRING, number=2,) + session_id = proto.Field(proto.STRING, number=21,) event_time = proto.Field(proto.MESSAGE, number=3, message=timestamp_pb2.Timestamp,) experiment_ids = proto.RepeatedField(proto.STRING, number=4,) attribution_token = proto.Field(proto.STRING, number=5,) product_details = proto.RepeatedField( proto.MESSAGE, number=6, message="ProductDetail", ) + completion_detail = proto.Field( + proto.MESSAGE, number=22, message="CompletionDetail", + ) attributes = proto.MapField( proto.STRING, proto.MESSAGE, number=7, message=common.CustomAttribute, ) @@ -200,6 +288,9 @@ class UserEvent(proto.Message): proto.MESSAGE, number=9, message="PurchaseTransaction", ) search_query = proto.Field(proto.STRING, number=10,) + filter = proto.Field(proto.STRING, number=16,) + order_by = proto.Field(proto.STRING, number=17,) + offset = proto.Field(proto.INT32, number=18,) page_categories = proto.RepeatedField(proto.STRING, number=11,) user_info = proto.Field(proto.MESSAGE, number=12, message=common.UserInfo,) uri = proto.Field(proto.STRING, number=13,) @@ -230,6 +321,28 @@ class ProductDetail(proto.Message): quantity = proto.Field(proto.MESSAGE, number=2, message=wrappers_pb2.Int32Value,) +class CompletionDetail(proto.Message): + r"""Detailed completion information including completion + attribution token and clicked completion info. + + Attributes: + completion_attribution_token (str): + Completion attribution token in + [CompleteQueryResponse.attribution_token][google.cloud.retail.v2.CompleteQueryResponse.attribution_token]. + selected_suggestion (str): + End user selected + [CompleteQueryResponse.CompletionResult.suggestion][google.cloud.retail.v2.CompleteQueryResponse.CompletionResult.suggestion]. + selected_position (int): + End user selected + [CompleteQueryResponse.CompletionResult.suggestion][google.cloud.retail.v2.CompleteQueryResponse.CompletionResult.suggestion] + position, starting from 0. + """ + + completion_attribution_token = proto.Field(proto.STRING, number=1,) + selected_suggestion = proto.Field(proto.STRING, number=2,) + selected_position = proto.Field(proto.INT32, number=3,) + + class PurchaseTransaction(proto.Message): r"""A transaction represents the entire purchase transaction. Attributes: diff --git a/owlbot.py b/owlbot.py index 29be2e8b..593baa2b 100644 --- a/owlbot.py +++ b/owlbot.py @@ -25,6 +25,20 @@ default_version = "v2" for library in s.get_staging_dirs(default_version): + # Work around sphinx docs issue + s.replace( + library / f"google/cloud/retail_{library.name}/types/*.py", + "a-zA-Z0-9_", + "a-zA-Z0-9\_" + ) + + # Work around sphinx docs issue + s.replace( + library / f"google/cloud/retail_{library.name}/types/completion_service.py", + "A customized string starts with OTHER_", + "A customized string starts with OTHER\_", + ) + s.move(library, excludes=["setup.py", "README.rst", "docs/index.rst"]) s.remove_staging_dirs() diff --git a/scripts/fixup_retail_v2_keywords.py b/scripts/fixup_retail_v2_keywords.py index c5554e83..e0ddf763 100644 --- a/scripts/fixup_retail_v2_keywords.py +++ b/scripts/fixup_retail_v2_keywords.py @@ -39,18 +39,27 @@ def partition( class retailCallTransformer(cst.CSTTransformer): CTRL_PARAMS: Tuple[str] = ('retry', 'timeout', 'metadata') METHOD_TO_PARAMS: Dict[str, Tuple[str]] = { + 'add_fulfillment_places': ('product', 'type_', 'place_ids', 'add_time', 'allow_missing', ), 'collect_user_event': ('parent', 'user_event', 'uri', 'ets', ), + 'complete_query': ('catalog', 'query', 'visitor_id', 'language_codes', 'device_type', 'dataset', 'max_suggestions', ), 'create_product': ('parent', 'product', 'product_id', ), 'delete_product': ('name', ), + 'get_default_branch': ('catalog', ), 'get_product': ('name', ), - 'import_products': ('parent', 'input_config', 'errors_config', 'update_mask', ), + 'import_completion_data': ('parent', 'input_config', 'notification_pubsub_topic', ), + 'import_products': ('parent', 'input_config', 'request_id', 'errors_config', 'update_mask', 'reconciliation_mode', 'notification_pubsub_topic', ), 'import_user_events': ('parent', 'input_config', 'errors_config', ), 'list_catalogs': ('parent', 'page_size', 'page_token', ), + 'list_products': ('parent', 'page_size', 'page_token', 'filter', 'read_mask', ), 'predict': ('placement', 'user_event', 'page_size', 'page_token', 'filter', 'validate_only', 'params', 'labels', ), 'purge_user_events': ('parent', 'filter', 'force', ), 'rejoin_user_events': ('parent', 'user_event_rejoin_scope', ), + 'remove_fulfillment_places': ('product', 'type_', 'place_ids', 'remove_time', 'allow_missing', ), + 'search': ('placement', 'visitor_id', 'branch', 'query', 'user_info', 'page_size', 'page_token', 'offset', 'filter', 'canonical_filter', 'order_by', 'facet_specs', 'dynamic_facet_spec', 'boost_spec', 'query_expansion_spec', 'variant_rollup_keys', 'page_categories', ), + 'set_default_branch': ('catalog', 'branch_id', 'note', ), + 'set_inventory': ('inventory', 'set_mask', 'set_time', 'allow_missing', ), 'update_catalog': ('catalog', 'update_mask', ), - 'update_product': ('product', 'update_mask', ), + 'update_product': ('product', 'update_mask', 'allow_missing', ), 'write_user_event': ('parent', 'user_event', ), } diff --git a/tests/unit/gapic/retail_v2/test_catalog_service.py b/tests/unit/gapic/retail_v2/test_catalog_service.py index 2df03b36..0118a31e 100644 --- a/tests/unit/gapic/retail_v2/test_catalog_service.py +++ b/tests/unit/gapic/retail_v2/test_catalog_service.py @@ -43,6 +43,7 @@ from google.cloud.retail_v2.types import catalog_service from google.oauth2 import service_account from google.protobuf import field_mask_pb2 # type: ignore +from google.protobuf import timestamp_pb2 # type: ignore import google.auth @@ -1043,6 +1044,438 @@ async def test_update_catalog_flattened_error_async(): ) +def test_set_default_branch( + transport: str = "grpc", request_type=catalog_service.SetDefaultBranchRequest +): + client = CatalogServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.set_default_branch), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = None + response = client.set_default_branch(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == catalog_service.SetDefaultBranchRequest() + + # Establish that the response is the type that we expect. + assert response is None + + +def test_set_default_branch_from_dict(): + test_set_default_branch(request_type=dict) + + +def test_set_default_branch_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CatalogServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.set_default_branch), "__call__" + ) as call: + client.set_default_branch() + call.assert_called() + _, args, _ = call.mock_calls[0] + assert args[0] == catalog_service.SetDefaultBranchRequest() + + +@pytest.mark.asyncio +async def test_set_default_branch_async( + transport: str = "grpc_asyncio", + request_type=catalog_service.SetDefaultBranchRequest, +): + client = CatalogServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.set_default_branch), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall(None) + response = await client.set_default_branch(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == catalog_service.SetDefaultBranchRequest() + + # Establish that the response is the type that we expect. + assert response is None + + +@pytest.mark.asyncio +async def test_set_default_branch_async_from_dict(): + await test_set_default_branch_async(request_type=dict) + + +def test_set_default_branch_field_headers(): + client = CatalogServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = catalog_service.SetDefaultBranchRequest() + + request.catalog = "catalog/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.set_default_branch), "__call__" + ) as call: + call.return_value = None + client.set_default_branch(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "catalog=catalog/value",) in kw["metadata"] + + +@pytest.mark.asyncio +async def test_set_default_branch_field_headers_async(): + client = CatalogServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = catalog_service.SetDefaultBranchRequest() + + request.catalog = "catalog/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.set_default_branch), "__call__" + ) as call: + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall(None) + await client.set_default_branch(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "catalog=catalog/value",) in kw["metadata"] + + +def test_set_default_branch_flattened(): + client = CatalogServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.set_default_branch), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = None + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + client.set_default_branch(catalog="catalog_value",) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0].catalog == "catalog_value" + + +def test_set_default_branch_flattened_error(): + client = CatalogServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + client.set_default_branch( + catalog_service.SetDefaultBranchRequest(), catalog="catalog_value", + ) + + +@pytest.mark.asyncio +async def test_set_default_branch_flattened_async(): + client = CatalogServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.set_default_branch), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = None + + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall(None) + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + response = await client.set_default_branch(catalog="catalog_value",) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0].catalog == "catalog_value" + + +@pytest.mark.asyncio +async def test_set_default_branch_flattened_error_async(): + client = CatalogServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + await client.set_default_branch( + catalog_service.SetDefaultBranchRequest(), catalog="catalog_value", + ) + + +def test_get_default_branch( + transport: str = "grpc", request_type=catalog_service.GetDefaultBranchRequest +): + client = CatalogServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.get_default_branch), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = catalog_service.GetDefaultBranchResponse( + branch="branch_value", note="note_value", + ) + response = client.get_default_branch(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == catalog_service.GetDefaultBranchRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, catalog_service.GetDefaultBranchResponse) + assert response.branch == "branch_value" + assert response.note == "note_value" + + +def test_get_default_branch_from_dict(): + test_get_default_branch(request_type=dict) + + +def test_get_default_branch_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CatalogServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.get_default_branch), "__call__" + ) as call: + client.get_default_branch() + call.assert_called() + _, args, _ = call.mock_calls[0] + assert args[0] == catalog_service.GetDefaultBranchRequest() + + +@pytest.mark.asyncio +async def test_get_default_branch_async( + transport: str = "grpc_asyncio", + request_type=catalog_service.GetDefaultBranchRequest, +): + client = CatalogServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.get_default_branch), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + catalog_service.GetDefaultBranchResponse( + branch="branch_value", note="note_value", + ) + ) + response = await client.get_default_branch(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == catalog_service.GetDefaultBranchRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, catalog_service.GetDefaultBranchResponse) + assert response.branch == "branch_value" + assert response.note == "note_value" + + +@pytest.mark.asyncio +async def test_get_default_branch_async_from_dict(): + await test_get_default_branch_async(request_type=dict) + + +def test_get_default_branch_field_headers(): + client = CatalogServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = catalog_service.GetDefaultBranchRequest() + + request.catalog = "catalog/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.get_default_branch), "__call__" + ) as call: + call.return_value = catalog_service.GetDefaultBranchResponse() + client.get_default_branch(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "catalog=catalog/value",) in kw["metadata"] + + +@pytest.mark.asyncio +async def test_get_default_branch_field_headers_async(): + client = CatalogServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = catalog_service.GetDefaultBranchRequest() + + request.catalog = "catalog/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.get_default_branch), "__call__" + ) as call: + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + catalog_service.GetDefaultBranchResponse() + ) + await client.get_default_branch(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "catalog=catalog/value",) in kw["metadata"] + + +def test_get_default_branch_flattened(): + client = CatalogServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.get_default_branch), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = catalog_service.GetDefaultBranchResponse() + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + client.get_default_branch(catalog="catalog_value",) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0].catalog == "catalog_value" + + +def test_get_default_branch_flattened_error(): + client = CatalogServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + client.get_default_branch( + catalog_service.GetDefaultBranchRequest(), catalog="catalog_value", + ) + + +@pytest.mark.asyncio +async def test_get_default_branch_flattened_async(): + client = CatalogServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.get_default_branch), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = catalog_service.GetDefaultBranchResponse() + + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + catalog_service.GetDefaultBranchResponse() + ) + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + response = await client.get_default_branch(catalog="catalog_value",) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0].catalog == "catalog_value" + + +@pytest.mark.asyncio +async def test_get_default_branch_flattened_error_async(): + client = CatalogServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + await client.get_default_branch( + catalog_service.GetDefaultBranchRequest(), catalog="catalog_value", + ) + + def test_credentials_transport_error(): # It is an error to provide credentials and a transport instance. transport = transports.CatalogServiceGrpcTransport( @@ -1142,6 +1575,8 @@ def test_catalog_service_base_transport(): methods = ( "list_catalogs", "update_catalog", + "set_default_branch", + "get_default_branch", ) for method in methods: with pytest.raises(NotImplementedError): @@ -1490,10 +1925,36 @@ def test_catalog_service_transport_channel_mtls_with_adc(transport_class): assert transport.grpc_channel == mock_grpc_channel -def test_catalog_path(): +def test_branch_path(): project = "squid" location = "clam" catalog = "whelk" + branch = "octopus" + expected = "projects/{project}/locations/{location}/catalogs/{catalog}/branches/{branch}".format( + project=project, location=location, catalog=catalog, branch=branch, + ) + actual = CatalogServiceClient.branch_path(project, location, catalog, branch) + assert expected == actual + + +def test_parse_branch_path(): + expected = { + "project": "oyster", + "location": "nudibranch", + "catalog": "cuttlefish", + "branch": "mussel", + } + path = CatalogServiceClient.branch_path(**expected) + + # Check that the path construction is reversible. + actual = CatalogServiceClient.parse_branch_path(path) + assert expected == actual + + +def test_catalog_path(): + project = "winkle" + location = "nautilus" + catalog = "scallop" expected = "projects/{project}/locations/{location}/catalogs/{catalog}".format( project=project, location=location, catalog=catalog, ) @@ -1503,9 +1964,9 @@ def test_catalog_path(): def test_parse_catalog_path(): expected = { - "project": "octopus", - "location": "oyster", - "catalog": "nudibranch", + "project": "abalone", + "location": "squid", + "catalog": "clam", } path = CatalogServiceClient.catalog_path(**expected) @@ -1515,7 +1976,7 @@ def test_parse_catalog_path(): def test_common_billing_account_path(): - billing_account = "cuttlefish" + billing_account = "whelk" expected = "billingAccounts/{billing_account}".format( billing_account=billing_account, ) @@ -1525,7 +1986,7 @@ def test_common_billing_account_path(): def test_parse_common_billing_account_path(): expected = { - "billing_account": "mussel", + "billing_account": "octopus", } path = CatalogServiceClient.common_billing_account_path(**expected) @@ -1535,7 +1996,7 @@ def test_parse_common_billing_account_path(): def test_common_folder_path(): - folder = "winkle" + folder = "oyster" expected = "folders/{folder}".format(folder=folder,) actual = CatalogServiceClient.common_folder_path(folder) assert expected == actual @@ -1543,7 +2004,7 @@ def test_common_folder_path(): def test_parse_common_folder_path(): expected = { - "folder": "nautilus", + "folder": "nudibranch", } path = CatalogServiceClient.common_folder_path(**expected) @@ -1553,7 +2014,7 @@ def test_parse_common_folder_path(): def test_common_organization_path(): - organization = "scallop" + organization = "cuttlefish" expected = "organizations/{organization}".format(organization=organization,) actual = CatalogServiceClient.common_organization_path(organization) assert expected == actual @@ -1561,7 +2022,7 @@ def test_common_organization_path(): def test_parse_common_organization_path(): expected = { - "organization": "abalone", + "organization": "mussel", } path = CatalogServiceClient.common_organization_path(**expected) @@ -1571,7 +2032,7 @@ def test_parse_common_organization_path(): def test_common_project_path(): - project = "squid" + project = "winkle" expected = "projects/{project}".format(project=project,) actual = CatalogServiceClient.common_project_path(project) assert expected == actual @@ -1579,7 +2040,7 @@ def test_common_project_path(): def test_parse_common_project_path(): expected = { - "project": "clam", + "project": "nautilus", } path = CatalogServiceClient.common_project_path(**expected) @@ -1589,8 +2050,8 @@ def test_parse_common_project_path(): def test_common_location_path(): - project = "whelk" - location = "octopus" + project = "scallop" + location = "abalone" expected = "projects/{project}/locations/{location}".format( project=project, location=location, ) @@ -1600,8 +2061,8 @@ def test_common_location_path(): def test_parse_common_location_path(): expected = { - "project": "oyster", - "location": "nudibranch", + "project": "squid", + "location": "clam", } path = CatalogServiceClient.common_location_path(**expected) diff --git a/tests/unit/gapic/retail_v2/test_completion_service.py b/tests/unit/gapic/retail_v2/test_completion_service.py new file mode 100644 index 00000000..ec0cc266 --- /dev/null +++ b/tests/unit/gapic/retail_v2/test_completion_service.py @@ -0,0 +1,1413 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import os +import mock +import packaging.version + +import grpc +from grpc.experimental import aio +import math +import pytest +from proto.marshal.rules.dates import DurationRule, TimestampRule + + +from google.api_core import client_options +from google.api_core import exceptions as core_exceptions +from google.api_core import future +from google.api_core import gapic_v1 +from google.api_core import grpc_helpers +from google.api_core import grpc_helpers_async +from google.api_core import operation_async # type: ignore +from google.api_core import operations_v1 +from google.auth import credentials as ga_credentials +from google.auth.exceptions import MutualTLSChannelError +from google.cloud.retail_v2.services.completion_service import ( + CompletionServiceAsyncClient, +) +from google.cloud.retail_v2.services.completion_service import CompletionServiceClient +from google.cloud.retail_v2.services.completion_service import transports +from google.cloud.retail_v2.services.completion_service.transports.base import ( + _GOOGLE_AUTH_VERSION, +) +from google.cloud.retail_v2.types import completion_service +from google.cloud.retail_v2.types import import_config +from google.longrunning import operations_pb2 +from google.oauth2 import service_account +from google.type import date_pb2 # type: ignore +import google.auth + + +# TODO(busunkim): Once google-auth >= 1.25.0 is required transitively +# through google-api-core: +# - Delete the auth "less than" test cases +# - Delete these pytest markers (Make the "greater than or equal to" tests the default). +requires_google_auth_lt_1_25_0 = pytest.mark.skipif( + packaging.version.parse(_GOOGLE_AUTH_VERSION) >= packaging.version.parse("1.25.0"), + reason="This test requires google-auth < 1.25.0", +) +requires_google_auth_gte_1_25_0 = pytest.mark.skipif( + packaging.version.parse(_GOOGLE_AUTH_VERSION) < packaging.version.parse("1.25.0"), + reason="This test requires google-auth >= 1.25.0", +) + + +def client_cert_source_callback(): + return b"cert bytes", b"key bytes" + + +# If default endpoint is localhost, then default mtls endpoint will be the same. +# This method modifies the default endpoint so the client can produce a different +# mtls endpoint for endpoint testing purposes. +def modify_default_endpoint(client): + return ( + "foo.googleapis.com" + if ("localhost" in client.DEFAULT_ENDPOINT) + else client.DEFAULT_ENDPOINT + ) + + +def test__get_default_mtls_endpoint(): + api_endpoint = "example.googleapis.com" + api_mtls_endpoint = "example.mtls.googleapis.com" + sandbox_endpoint = "example.sandbox.googleapis.com" + sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" + non_googleapi = "api.example.com" + + assert CompletionServiceClient._get_default_mtls_endpoint(None) is None + assert ( + CompletionServiceClient._get_default_mtls_endpoint(api_endpoint) + == api_mtls_endpoint + ) + assert ( + CompletionServiceClient._get_default_mtls_endpoint(api_mtls_endpoint) + == api_mtls_endpoint + ) + assert ( + CompletionServiceClient._get_default_mtls_endpoint(sandbox_endpoint) + == sandbox_mtls_endpoint + ) + assert ( + CompletionServiceClient._get_default_mtls_endpoint(sandbox_mtls_endpoint) + == sandbox_mtls_endpoint + ) + assert ( + CompletionServiceClient._get_default_mtls_endpoint(non_googleapi) + == non_googleapi + ) + + +@pytest.mark.parametrize( + "client_class", [CompletionServiceClient, CompletionServiceAsyncClient,] +) +def test_completion_service_client_from_service_account_info(client_class): + creds = ga_credentials.AnonymousCredentials() + with mock.patch.object( + service_account.Credentials, "from_service_account_info" + ) as factory: + factory.return_value = creds + info = {"valid": True} + client = client_class.from_service_account_info(info) + assert client.transport._credentials == creds + assert isinstance(client, client_class) + + assert client.transport._host == "retail.googleapis.com:443" + + +@pytest.mark.parametrize( + "transport_class,transport_name", + [ + (transports.CompletionServiceGrpcTransport, "grpc"), + (transports.CompletionServiceGrpcAsyncIOTransport, "grpc_asyncio"), + ], +) +def test_completion_service_client_service_account_always_use_jwt( + transport_class, transport_name +): + with mock.patch.object( + service_account.Credentials, "with_always_use_jwt_access", create=True + ) as use_jwt: + creds = service_account.Credentials(None, None, None) + transport = transport_class(credentials=creds, always_use_jwt_access=True) + use_jwt.assert_called_once_with(True) + + with mock.patch.object( + service_account.Credentials, "with_always_use_jwt_access", create=True + ) as use_jwt: + creds = service_account.Credentials(None, None, None) + transport = transport_class(credentials=creds, always_use_jwt_access=False) + use_jwt.assert_not_called() + + +@pytest.mark.parametrize( + "client_class", [CompletionServiceClient, CompletionServiceAsyncClient,] +) +def test_completion_service_client_from_service_account_file(client_class): + creds = ga_credentials.AnonymousCredentials() + with mock.patch.object( + service_account.Credentials, "from_service_account_file" + ) as factory: + factory.return_value = creds + client = client_class.from_service_account_file("dummy/file/path.json") + assert client.transport._credentials == creds + assert isinstance(client, client_class) + + client = client_class.from_service_account_json("dummy/file/path.json") + assert client.transport._credentials == creds + assert isinstance(client, client_class) + + assert client.transport._host == "retail.googleapis.com:443" + + +def test_completion_service_client_get_transport_class(): + transport = CompletionServiceClient.get_transport_class() + available_transports = [ + transports.CompletionServiceGrpcTransport, + ] + assert transport in available_transports + + transport = CompletionServiceClient.get_transport_class("grpc") + assert transport == transports.CompletionServiceGrpcTransport + + +@pytest.mark.parametrize( + "client_class,transport_class,transport_name", + [ + (CompletionServiceClient, transports.CompletionServiceGrpcTransport, "grpc"), + ( + CompletionServiceAsyncClient, + transports.CompletionServiceGrpcAsyncIOTransport, + "grpc_asyncio", + ), + ], +) +@mock.patch.object( + CompletionServiceClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(CompletionServiceClient), +) +@mock.patch.object( + CompletionServiceAsyncClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(CompletionServiceAsyncClient), +) +def test_completion_service_client_client_options( + client_class, transport_class, transport_name +): + # Check that if channel is provided we won't create a new one. + with mock.patch.object(CompletionServiceClient, "get_transport_class") as gtc: + transport = transport_class(credentials=ga_credentials.AnonymousCredentials()) + client = client_class(transport=transport) + gtc.assert_not_called() + + # Check that if channel is provided via str we will create a new one. + with mock.patch.object(CompletionServiceClient, "get_transport_class") as gtc: + client = client_class(transport=transport_name) + gtc.assert_called() + + # Check the case api_endpoint is provided. + options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host="squid.clam.whelk", + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS_ENDPOINT is + # "never". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS_ENDPOINT is + # "always". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}): + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_MTLS_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS_ENDPOINT has + # unsupported value. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "Unsupported"}): + with pytest.raises(MutualTLSChannelError): + client = client_class() + + # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} + ): + with pytest.raises(ValueError): + client = client_class() + + # Check the case quota_project_id is provided + options = client_options.ClientOptions(quota_project_id="octopus") + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id="octopus", + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + +@pytest.mark.parametrize( + "client_class,transport_class,transport_name,use_client_cert_env", + [ + ( + CompletionServiceClient, + transports.CompletionServiceGrpcTransport, + "grpc", + "true", + ), + ( + CompletionServiceAsyncClient, + transports.CompletionServiceGrpcAsyncIOTransport, + "grpc_asyncio", + "true", + ), + ( + CompletionServiceClient, + transports.CompletionServiceGrpcTransport, + "grpc", + "false", + ), + ( + CompletionServiceAsyncClient, + transports.CompletionServiceGrpcAsyncIOTransport, + "grpc_asyncio", + "false", + ), + ], +) +@mock.patch.object( + CompletionServiceClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(CompletionServiceClient), +) +@mock.patch.object( + CompletionServiceAsyncClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(CompletionServiceAsyncClient), +) +@mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}) +def test_completion_service_client_mtls_env_auto( + client_class, transport_class, transport_name, use_client_cert_env +): + # This tests the endpoint autoswitch behavior. Endpoint is autoswitched to the default + # mtls endpoint, if GOOGLE_API_USE_CLIENT_CERTIFICATE is "true" and client cert exists. + + # Check the case client_cert_source is provided. Whether client cert is used depends on + # GOOGLE_API_USE_CLIENT_CERTIFICATE value. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} + ): + options = client_options.ClientOptions( + client_cert_source=client_cert_source_callback + ) + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + + if use_client_cert_env == "false": + expected_client_cert_source = None + expected_host = client.DEFAULT_ENDPOINT + else: + expected_client_cert_source = client_cert_source_callback + expected_host = client.DEFAULT_MTLS_ENDPOINT + + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=expected_host, + scopes=None, + client_cert_source_for_mtls=expected_client_cert_source, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + # Check the case ADC client cert is provided. Whether client cert is used depends on + # GOOGLE_API_USE_CLIENT_CERTIFICATE value. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} + ): + with mock.patch.object(transport_class, "__init__") as patched: + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=client_cert_source_callback, + ): + if use_client_cert_env == "false": + expected_host = client.DEFAULT_ENDPOINT + expected_client_cert_source = None + else: + expected_host = client.DEFAULT_MTLS_ENDPOINT + expected_client_cert_source = client_cert_source_callback + + patched.return_value = None + client = client_class() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=expected_host, + scopes=None, + client_cert_source_for_mtls=expected_client_cert_source, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + # Check the case client_cert_source and ADC client cert are not provided. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} + ): + with mock.patch.object(transport_class, "__init__") as patched: + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=False, + ): + patched.return_value = None + client = client_class() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + +@pytest.mark.parametrize( + "client_class,transport_class,transport_name", + [ + (CompletionServiceClient, transports.CompletionServiceGrpcTransport, "grpc"), + ( + CompletionServiceAsyncClient, + transports.CompletionServiceGrpcAsyncIOTransport, + "grpc_asyncio", + ), + ], +) +def test_completion_service_client_client_options_scopes( + client_class, transport_class, transport_name +): + # Check the case scopes are provided. + options = client_options.ClientOptions(scopes=["1", "2"],) + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=["1", "2"], + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + +@pytest.mark.parametrize( + "client_class,transport_class,transport_name", + [ + (CompletionServiceClient, transports.CompletionServiceGrpcTransport, "grpc"), + ( + CompletionServiceAsyncClient, + transports.CompletionServiceGrpcAsyncIOTransport, + "grpc_asyncio", + ), + ], +) +def test_completion_service_client_client_options_credentials_file( + client_class, transport_class, transport_name +): + # Check the case credentials file is provided. + options = client_options.ClientOptions(credentials_file="credentials.json") + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=None, + credentials_file="credentials.json", + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + +def test_completion_service_client_client_options_from_dict(): + with mock.patch( + "google.cloud.retail_v2.services.completion_service.transports.CompletionServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = CompletionServiceClient( + client_options={"api_endpoint": "squid.clam.whelk"} + ) + grpc_transport.assert_called_once_with( + credentials=None, + credentials_file=None, + host="squid.clam.whelk", + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + +def test_complete_query( + transport: str = "grpc", request_type=completion_service.CompleteQueryRequest +): + client = CompletionServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.complete_query), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = completion_service.CompleteQueryResponse( + attribution_token="attribution_token_value", + ) + response = client.complete_query(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == completion_service.CompleteQueryRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, completion_service.CompleteQueryResponse) + assert response.attribution_token == "attribution_token_value" + + +def test_complete_query_from_dict(): + test_complete_query(request_type=dict) + + +def test_complete_query_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CompletionServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.complete_query), "__call__") as call: + client.complete_query() + call.assert_called() + _, args, _ = call.mock_calls[0] + assert args[0] == completion_service.CompleteQueryRequest() + + +@pytest.mark.asyncio +async def test_complete_query_async( + transport: str = "grpc_asyncio", + request_type=completion_service.CompleteQueryRequest, +): + client = CompletionServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.complete_query), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + completion_service.CompleteQueryResponse( + attribution_token="attribution_token_value", + ) + ) + response = await client.complete_query(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == completion_service.CompleteQueryRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, completion_service.CompleteQueryResponse) + assert response.attribution_token == "attribution_token_value" + + +@pytest.mark.asyncio +async def test_complete_query_async_from_dict(): + await test_complete_query_async(request_type=dict) + + +def test_complete_query_field_headers(): + client = CompletionServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = completion_service.CompleteQueryRequest() + + request.catalog = "catalog/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.complete_query), "__call__") as call: + call.return_value = completion_service.CompleteQueryResponse() + client.complete_query(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "catalog=catalog/value",) in kw["metadata"] + + +@pytest.mark.asyncio +async def test_complete_query_field_headers_async(): + client = CompletionServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = completion_service.CompleteQueryRequest() + + request.catalog = "catalog/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.complete_query), "__call__") as call: + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + completion_service.CompleteQueryResponse() + ) + await client.complete_query(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "catalog=catalog/value",) in kw["metadata"] + + +def test_import_completion_data( + transport: str = "grpc", request_type=import_config.ImportCompletionDataRequest +): + client = CompletionServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.import_completion_data), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/spam") + response = client.import_completion_data(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == import_config.ImportCompletionDataRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, future.Future) + + +def test_import_completion_data_from_dict(): + test_import_completion_data(request_type=dict) + + +def test_import_completion_data_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = CompletionServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.import_completion_data), "__call__" + ) as call: + client.import_completion_data() + call.assert_called() + _, args, _ = call.mock_calls[0] + assert args[0] == import_config.ImportCompletionDataRequest() + + +@pytest.mark.asyncio +async def test_import_completion_data_async( + transport: str = "grpc_asyncio", + request_type=import_config.ImportCompletionDataRequest, +): + client = CompletionServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.import_completion_data), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/spam") + ) + response = await client.import_completion_data(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == import_config.ImportCompletionDataRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, future.Future) + + +@pytest.mark.asyncio +async def test_import_completion_data_async_from_dict(): + await test_import_completion_data_async(request_type=dict) + + +def test_import_completion_data_field_headers(): + client = CompletionServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = import_config.ImportCompletionDataRequest() + + request.parent = "parent/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.import_completion_data), "__call__" + ) as call: + call.return_value = operations_pb2.Operation(name="operations/op") + client.import_completion_data(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "parent=parent/value",) in kw["metadata"] + + +@pytest.mark.asyncio +async def test_import_completion_data_field_headers_async(): + client = CompletionServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = import_config.ImportCompletionDataRequest() + + request.parent = "parent/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.import_completion_data), "__call__" + ) as call: + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/op") + ) + await client.import_completion_data(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "parent=parent/value",) in kw["metadata"] + + +def test_credentials_transport_error(): + # It is an error to provide credentials and a transport instance. + transport = transports.CompletionServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + with pytest.raises(ValueError): + client = CompletionServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # It is an error to provide a credentials file and a transport instance. + transport = transports.CompletionServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + with pytest.raises(ValueError): + client = CompletionServiceClient( + client_options={"credentials_file": "credentials.json"}, + transport=transport, + ) + + # It is an error to provide scopes and a transport instance. + transport = transports.CompletionServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + with pytest.raises(ValueError): + client = CompletionServiceClient( + client_options={"scopes": ["1", "2"]}, transport=transport, + ) + + +def test_transport_instance(): + # A client may be instantiated with a custom transport instance. + transport = transports.CompletionServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + client = CompletionServiceClient(transport=transport) + assert client.transport is transport + + +def test_transport_get_channel(): + # A client may be instantiated with a custom transport instance. + transport = transports.CompletionServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + channel = transport.grpc_channel + assert channel + + transport = transports.CompletionServiceGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + channel = transport.grpc_channel + assert channel + + +@pytest.mark.parametrize( + "transport_class", + [ + transports.CompletionServiceGrpcTransport, + transports.CompletionServiceGrpcAsyncIOTransport, + ], +) +def test_transport_adc(transport_class): + # Test default credentials are used if not provided. + with mock.patch.object(google.auth, "default") as adc: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport_class() + adc.assert_called_once() + + +def test_transport_grpc_default(): + # A client should use the gRPC transport by default. + client = CompletionServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + assert isinstance(client.transport, transports.CompletionServiceGrpcTransport,) + + +def test_completion_service_base_transport_error(): + # Passing both a credentials object and credentials_file should raise an error + with pytest.raises(core_exceptions.DuplicateCredentialArgs): + transport = transports.CompletionServiceTransport( + credentials=ga_credentials.AnonymousCredentials(), + credentials_file="credentials.json", + ) + + +def test_completion_service_base_transport(): + # Instantiate the base transport. + with mock.patch( + "google.cloud.retail_v2.services.completion_service.transports.CompletionServiceTransport.__init__" + ) as Transport: + Transport.return_value = None + transport = transports.CompletionServiceTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Every method on the transport should just blindly + # raise NotImplementedError. + methods = ( + "complete_query", + "import_completion_data", + ) + for method in methods: + with pytest.raises(NotImplementedError): + getattr(transport, method)(request=object()) + + # Additionally, the LRO client (a property) should + # also raise NotImplementedError + with pytest.raises(NotImplementedError): + transport.operations_client + + +@requires_google_auth_gte_1_25_0 +def test_completion_service_base_transport_with_credentials_file(): + # Instantiate the base transport with a credentials file + with mock.patch.object( + google.auth, "load_credentials_from_file", autospec=True + ) as load_creds, mock.patch( + "google.cloud.retail_v2.services.completion_service.transports.CompletionServiceTransport._prep_wrapped_messages" + ) as Transport: + Transport.return_value = None + load_creds.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.CompletionServiceTransport( + credentials_file="credentials.json", quota_project_id="octopus", + ) + load_creds.assert_called_once_with( + "credentials.json", + scopes=None, + default_scopes=("https://www.googleapis.com/auth/cloud-platform",), + quota_project_id="octopus", + ) + + +@requires_google_auth_lt_1_25_0 +def test_completion_service_base_transport_with_credentials_file_old_google_auth(): + # Instantiate the base transport with a credentials file + with mock.patch.object( + google.auth, "load_credentials_from_file", autospec=True + ) as load_creds, mock.patch( + "google.cloud.retail_v2.services.completion_service.transports.CompletionServiceTransport._prep_wrapped_messages" + ) as Transport: + Transport.return_value = None + load_creds.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.CompletionServiceTransport( + credentials_file="credentials.json", quota_project_id="octopus", + ) + load_creds.assert_called_once_with( + "credentials.json", + scopes=("https://www.googleapis.com/auth/cloud-platform",), + quota_project_id="octopus", + ) + + +def test_completion_service_base_transport_with_adc(): + # Test the default credentials are used if credentials and credentials_file are None. + with mock.patch.object(google.auth, "default", autospec=True) as adc, mock.patch( + "google.cloud.retail_v2.services.completion_service.transports.CompletionServiceTransport._prep_wrapped_messages" + ) as Transport: + Transport.return_value = None + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.CompletionServiceTransport() + adc.assert_called_once() + + +@requires_google_auth_gte_1_25_0 +def test_completion_service_auth_adc(): + # If no credentials are provided, we should use ADC credentials. + with mock.patch.object(google.auth, "default", autospec=True) as adc: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + CompletionServiceClient() + adc.assert_called_once_with( + scopes=None, + default_scopes=("https://www.googleapis.com/auth/cloud-platform",), + quota_project_id=None, + ) + + +@requires_google_auth_lt_1_25_0 +def test_completion_service_auth_adc_old_google_auth(): + # If no credentials are provided, we should use ADC credentials. + with mock.patch.object(google.auth, "default", autospec=True) as adc: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + CompletionServiceClient() + adc.assert_called_once_with( + scopes=("https://www.googleapis.com/auth/cloud-platform",), + quota_project_id=None, + ) + + +@pytest.mark.parametrize( + "transport_class", + [ + transports.CompletionServiceGrpcTransport, + transports.CompletionServiceGrpcAsyncIOTransport, + ], +) +@requires_google_auth_gte_1_25_0 +def test_completion_service_transport_auth_adc(transport_class): + # If credentials and host are not provided, the transport class should use + # ADC credentials. + with mock.patch.object(google.auth, "default", autospec=True) as adc: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport_class(quota_project_id="octopus", scopes=["1", "2"]) + adc.assert_called_once_with( + scopes=["1", "2"], + default_scopes=("https://www.googleapis.com/auth/cloud-platform",), + quota_project_id="octopus", + ) + + +@pytest.mark.parametrize( + "transport_class", + [ + transports.CompletionServiceGrpcTransport, + transports.CompletionServiceGrpcAsyncIOTransport, + ], +) +@requires_google_auth_lt_1_25_0 +def test_completion_service_transport_auth_adc_old_google_auth(transport_class): + # If credentials and host are not provided, the transport class should use + # ADC credentials. + with mock.patch.object(google.auth, "default", autospec=True) as adc: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport_class(quota_project_id="octopus") + adc.assert_called_once_with( + scopes=("https://www.googleapis.com/auth/cloud-platform",), + quota_project_id="octopus", + ) + + +@pytest.mark.parametrize( + "transport_class,grpc_helpers", + [ + (transports.CompletionServiceGrpcTransport, grpc_helpers), + (transports.CompletionServiceGrpcAsyncIOTransport, grpc_helpers_async), + ], +) +def test_completion_service_transport_create_channel(transport_class, grpc_helpers): + # If credentials and host are not provided, the transport class should use + # ADC credentials. + with mock.patch.object( + google.auth, "default", autospec=True + ) as adc, mock.patch.object( + grpc_helpers, "create_channel", autospec=True + ) as create_channel: + creds = ga_credentials.AnonymousCredentials() + adc.return_value = (creds, None) + transport_class(quota_project_id="octopus", scopes=["1", "2"]) + + create_channel.assert_called_with( + "retail.googleapis.com:443", + credentials=creds, + credentials_file=None, + quota_project_id="octopus", + default_scopes=("https://www.googleapis.com/auth/cloud-platform",), + scopes=["1", "2"], + default_host="retail.googleapis.com", + ssl_credentials=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + +@pytest.mark.parametrize( + "transport_class", + [ + transports.CompletionServiceGrpcTransport, + transports.CompletionServiceGrpcAsyncIOTransport, + ], +) +def test_completion_service_grpc_transport_client_cert_source_for_mtls(transport_class): + cred = ga_credentials.AnonymousCredentials() + + # Check ssl_channel_credentials is used if provided. + with mock.patch.object(transport_class, "create_channel") as mock_create_channel: + mock_ssl_channel_creds = mock.Mock() + transport_class( + host="squid.clam.whelk", + credentials=cred, + ssl_channel_credentials=mock_ssl_channel_creds, + ) + mock_create_channel.assert_called_once_with( + "squid.clam.whelk:443", + credentials=cred, + credentials_file=None, + scopes=None, + ssl_credentials=mock_ssl_channel_creds, + quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + # Check if ssl_channel_credentials is not provided, then client_cert_source_for_mtls + # is used. + with mock.patch.object(transport_class, "create_channel", return_value=mock.Mock()): + with mock.patch("grpc.ssl_channel_credentials") as mock_ssl_cred: + transport_class( + credentials=cred, + client_cert_source_for_mtls=client_cert_source_callback, + ) + expected_cert, expected_key = client_cert_source_callback() + mock_ssl_cred.assert_called_once_with( + certificate_chain=expected_cert, private_key=expected_key + ) + + +def test_completion_service_host_no_port(): + client = CompletionServiceClient( + credentials=ga_credentials.AnonymousCredentials(), + client_options=client_options.ClientOptions( + api_endpoint="retail.googleapis.com" + ), + ) + assert client.transport._host == "retail.googleapis.com:443" + + +def test_completion_service_host_with_port(): + client = CompletionServiceClient( + credentials=ga_credentials.AnonymousCredentials(), + client_options=client_options.ClientOptions( + api_endpoint="retail.googleapis.com:8000" + ), + ) + assert client.transport._host == "retail.googleapis.com:8000" + + +def test_completion_service_grpc_transport_channel(): + channel = grpc.secure_channel("http://localhost/", grpc.local_channel_credentials()) + + # Check that channel is used if provided. + transport = transports.CompletionServiceGrpcTransport( + host="squid.clam.whelk", channel=channel, + ) + assert transport.grpc_channel == channel + assert transport._host == "squid.clam.whelk:443" + assert transport._ssl_channel_credentials == None + + +def test_completion_service_grpc_asyncio_transport_channel(): + channel = aio.secure_channel("http://localhost/", grpc.local_channel_credentials()) + + # Check that channel is used if provided. + transport = transports.CompletionServiceGrpcAsyncIOTransport( + host="squid.clam.whelk", channel=channel, + ) + assert transport.grpc_channel == channel + assert transport._host == "squid.clam.whelk:443" + assert transport._ssl_channel_credentials == None + + +# Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are +# removed from grpc/grpc_asyncio transport constructor. +@pytest.mark.parametrize( + "transport_class", + [ + transports.CompletionServiceGrpcTransport, + transports.CompletionServiceGrpcAsyncIOTransport, + ], +) +def test_completion_service_transport_channel_mtls_with_client_cert_source( + transport_class, +): + with mock.patch( + "grpc.ssl_channel_credentials", autospec=True + ) as grpc_ssl_channel_cred: + with mock.patch.object( + transport_class, "create_channel" + ) as grpc_create_channel: + mock_ssl_cred = mock.Mock() + grpc_ssl_channel_cred.return_value = mock_ssl_cred + + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + cred = ga_credentials.AnonymousCredentials() + with pytest.warns(DeprecationWarning): + with mock.patch.object(google.auth, "default") as adc: + adc.return_value = (cred, None) + transport = transport_class( + host="squid.clam.whelk", + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=client_cert_source_callback, + ) + adc.assert_called_once() + + grpc_ssl_channel_cred.assert_called_once_with( + certificate_chain=b"cert bytes", private_key=b"key bytes" + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=cred, + credentials_file=None, + scopes=None, + ssl_credentials=mock_ssl_cred, + quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + assert transport.grpc_channel == mock_grpc_channel + assert transport._ssl_channel_credentials == mock_ssl_cred + + +# Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are +# removed from grpc/grpc_asyncio transport constructor. +@pytest.mark.parametrize( + "transport_class", + [ + transports.CompletionServiceGrpcTransport, + transports.CompletionServiceGrpcAsyncIOTransport, + ], +) +def test_completion_service_transport_channel_mtls_with_adc(transport_class): + mock_ssl_cred = mock.Mock() + with mock.patch.multiple( + "google.auth.transport.grpc.SslCredentials", + __init__=mock.Mock(return_value=None), + ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), + ): + with mock.patch.object( + transport_class, "create_channel" + ) as grpc_create_channel: + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + mock_cred = mock.Mock() + + with pytest.warns(DeprecationWarning): + transport = transport_class( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=None, + ) + + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + credentials_file=None, + scopes=None, + ssl_credentials=mock_ssl_cred, + quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + assert transport.grpc_channel == mock_grpc_channel + + +def test_completion_service_grpc_lro_client(): + client = CompletionServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport="grpc", + ) + transport = client.transport + + # Ensure that we have a api-core operations client. + assert isinstance(transport.operations_client, operations_v1.OperationsClient,) + + # Ensure that subsequent calls to the property send the exact same object. + assert transport.operations_client is transport.operations_client + + +def test_completion_service_grpc_lro_async_client(): + client = CompletionServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), transport="grpc_asyncio", + ) + transport = client.transport + + # Ensure that we have a api-core operations client. + assert isinstance(transport.operations_client, operations_v1.OperationsAsyncClient,) + + # Ensure that subsequent calls to the property send the exact same object. + assert transport.operations_client is transport.operations_client + + +def test_catalog_path(): + project = "squid" + location = "clam" + catalog = "whelk" + expected = "projects/{project}/locations/{location}/catalogs/{catalog}".format( + project=project, location=location, catalog=catalog, + ) + actual = CompletionServiceClient.catalog_path(project, location, catalog) + assert expected == actual + + +def test_parse_catalog_path(): + expected = { + "project": "octopus", + "location": "oyster", + "catalog": "nudibranch", + } + path = CompletionServiceClient.catalog_path(**expected) + + # Check that the path construction is reversible. + actual = CompletionServiceClient.parse_catalog_path(path) + assert expected == actual + + +def test_common_billing_account_path(): + billing_account = "cuttlefish" + expected = "billingAccounts/{billing_account}".format( + billing_account=billing_account, + ) + actual = CompletionServiceClient.common_billing_account_path(billing_account) + assert expected == actual + + +def test_parse_common_billing_account_path(): + expected = { + "billing_account": "mussel", + } + path = CompletionServiceClient.common_billing_account_path(**expected) + + # Check that the path construction is reversible. + actual = CompletionServiceClient.parse_common_billing_account_path(path) + assert expected == actual + + +def test_common_folder_path(): + folder = "winkle" + expected = "folders/{folder}".format(folder=folder,) + actual = CompletionServiceClient.common_folder_path(folder) + assert expected == actual + + +def test_parse_common_folder_path(): + expected = { + "folder": "nautilus", + } + path = CompletionServiceClient.common_folder_path(**expected) + + # Check that the path construction is reversible. + actual = CompletionServiceClient.parse_common_folder_path(path) + assert expected == actual + + +def test_common_organization_path(): + organization = "scallop" + expected = "organizations/{organization}".format(organization=organization,) + actual = CompletionServiceClient.common_organization_path(organization) + assert expected == actual + + +def test_parse_common_organization_path(): + expected = { + "organization": "abalone", + } + path = CompletionServiceClient.common_organization_path(**expected) + + # Check that the path construction is reversible. + actual = CompletionServiceClient.parse_common_organization_path(path) + assert expected == actual + + +def test_common_project_path(): + project = "squid" + expected = "projects/{project}".format(project=project,) + actual = CompletionServiceClient.common_project_path(project) + assert expected == actual + + +def test_parse_common_project_path(): + expected = { + "project": "clam", + } + path = CompletionServiceClient.common_project_path(**expected) + + # Check that the path construction is reversible. + actual = CompletionServiceClient.parse_common_project_path(path) + assert expected == actual + + +def test_common_location_path(): + project = "whelk" + location = "octopus" + expected = "projects/{project}/locations/{location}".format( + project=project, location=location, + ) + actual = CompletionServiceClient.common_location_path(project, location) + assert expected == actual + + +def test_parse_common_location_path(): + expected = { + "project": "oyster", + "location": "nudibranch", + } + path = CompletionServiceClient.common_location_path(**expected) + + # Check that the path construction is reversible. + actual = CompletionServiceClient.parse_common_location_path(path) + assert expected == actual + + +def test_client_withDEFAULT_CLIENT_INFO(): + client_info = gapic_v1.client_info.ClientInfo() + + with mock.patch.object( + transports.CompletionServiceTransport, "_prep_wrapped_messages" + ) as prep: + client = CompletionServiceClient( + credentials=ga_credentials.AnonymousCredentials(), client_info=client_info, + ) + prep.assert_called_once_with(client_info) + + with mock.patch.object( + transports.CompletionServiceTransport, "_prep_wrapped_messages" + ) as prep: + transport_class = CompletionServiceClient.get_transport_class() + transport = transport_class( + credentials=ga_credentials.AnonymousCredentials(), client_info=client_info, + ) + prep.assert_called_once_with(client_info) diff --git a/tests/unit/gapic/retail_v2/test_prediction_service.py b/tests/unit/gapic/retail_v2/test_prediction_service.py index 7bd7e8e1..b5cbf7be 100644 --- a/tests/unit/gapic/retail_v2/test_prediction_service.py +++ b/tests/unit/gapic/retail_v2/test_prediction_service.py @@ -44,6 +44,8 @@ from google.cloud.retail_v2.types import product from google.cloud.retail_v2.types import user_event 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 struct_pb2 # type: ignore from google.protobuf import timestamp_pb2 # type: ignore from google.protobuf import wrappers_pb2 # type: ignore diff --git a/tests/unit/gapic/retail_v2/test_product_service.py b/tests/unit/gapic/retail_v2/test_product_service.py index 70885925..67f22750 100644 --- a/tests/unit/gapic/retail_v2/test_product_service.py +++ b/tests/unit/gapic/retail_v2/test_product_service.py @@ -36,6 +36,7 @@ from google.auth.exceptions import MutualTLSChannelError from google.cloud.retail_v2.services.product_service import ProductServiceAsyncClient from google.cloud.retail_v2.services.product_service import ProductServiceClient +from google.cloud.retail_v2.services.product_service import pagers from google.cloud.retail_v2.services.product_service import transports from google.cloud.retail_v2.services.product_service.transports.base import ( _GOOGLE_AUTH_VERSION, @@ -47,9 +48,11 @@ from google.cloud.retail_v2.types import product_service from google.longrunning import operations_pb2 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 timestamp_pb2 # type: ignore from google.protobuf import wrappers_pb2 # type: ignore +from google.type import date_pb2 # type: ignore import google.auth @@ -523,12 +526,21 @@ def test_create_product( id="id_value", type_=gcr_product.Product.Type.PRIMARY, primary_product_id="primary_product_id_value", + collection_member_ids=["collection_member_ids_value"], + gtin="gtin_value", categories=["categories_value"], title="title_value", + brands=["brands_value"], description="description_value", + language_code="language_code_value", tags=["tags_value"], availability=gcr_product.Product.Availability.IN_STOCK, uri="uri_value", + sizes=["sizes_value"], + materials=["materials_value"], + patterns=["patterns_value"], + conditions=["conditions_value"], + expire_time=timestamp_pb2.Timestamp(seconds=751), ) response = client.create_product(request) @@ -543,12 +555,20 @@ def test_create_product( assert response.id == "id_value" assert response.type_ == gcr_product.Product.Type.PRIMARY assert response.primary_product_id == "primary_product_id_value" + assert response.collection_member_ids == ["collection_member_ids_value"] + assert response.gtin == "gtin_value" assert response.categories == ["categories_value"] assert response.title == "title_value" + assert response.brands == ["brands_value"] assert response.description == "description_value" + assert response.language_code == "language_code_value" assert response.tags == ["tags_value"] assert response.availability == gcr_product.Product.Availability.IN_STOCK assert response.uri == "uri_value" + assert response.sizes == ["sizes_value"] + assert response.materials == ["materials_value"] + assert response.patterns == ["patterns_value"] + assert response.conditions == ["conditions_value"] def test_create_product_from_dict(): @@ -591,12 +611,20 @@ async def test_create_product_async( id="id_value", type_=gcr_product.Product.Type.PRIMARY, primary_product_id="primary_product_id_value", + collection_member_ids=["collection_member_ids_value"], + gtin="gtin_value", categories=["categories_value"], title="title_value", + brands=["brands_value"], description="description_value", + language_code="language_code_value", tags=["tags_value"], availability=gcr_product.Product.Availability.IN_STOCK, uri="uri_value", + sizes=["sizes_value"], + materials=["materials_value"], + patterns=["patterns_value"], + conditions=["conditions_value"], ) ) response = await client.create_product(request) @@ -612,12 +640,20 @@ async def test_create_product_async( assert response.id == "id_value" assert response.type_ == gcr_product.Product.Type.PRIMARY assert response.primary_product_id == "primary_product_id_value" + assert response.collection_member_ids == ["collection_member_ids_value"] + assert response.gtin == "gtin_value" assert response.categories == ["categories_value"] assert response.title == "title_value" + assert response.brands == ["brands_value"] assert response.description == "description_value" + assert response.language_code == "language_code_value" assert response.tags == ["tags_value"] assert response.availability == gcr_product.Product.Availability.IN_STOCK assert response.uri == "uri_value" + assert response.sizes == ["sizes_value"] + assert response.materials == ["materials_value"] + assert response.patterns == ["patterns_value"] + assert response.conditions == ["conditions_value"] @pytest.mark.asyncio @@ -687,7 +723,9 @@ def test_create_product_flattened(): # using the keyword arguments to the method. client.create_product( parent="parent_value", - product=gcr_product.Product(name="name_value"), + product=gcr_product.Product( + expire_time=timestamp_pb2.Timestamp(seconds=751) + ), product_id="product_id_value", ) @@ -696,7 +734,9 @@ def test_create_product_flattened(): assert len(call.mock_calls) == 1 _, args, _ = call.mock_calls[0] assert args[0].parent == "parent_value" - assert args[0].product == gcr_product.Product(name="name_value") + assert args[0].product == gcr_product.Product( + expire_time=timestamp_pb2.Timestamp(seconds=751) + ) assert args[0].product_id == "product_id_value" @@ -709,7 +749,9 @@ def test_create_product_flattened_error(): client.create_product( product_service.CreateProductRequest(), parent="parent_value", - product=gcr_product.Product(name="name_value"), + product=gcr_product.Product( + expire_time=timestamp_pb2.Timestamp(seconds=751) + ), product_id="product_id_value", ) @@ -730,7 +772,9 @@ async def test_create_product_flattened_async(): # using the keyword arguments to the method. response = await client.create_product( parent="parent_value", - product=gcr_product.Product(name="name_value"), + product=gcr_product.Product( + expire_time=timestamp_pb2.Timestamp(seconds=751) + ), product_id="product_id_value", ) @@ -739,7 +783,9 @@ async def test_create_product_flattened_async(): assert len(call.mock_calls) _, args, _ = call.mock_calls[0] assert args[0].parent == "parent_value" - assert args[0].product == gcr_product.Product(name="name_value") + assert args[0].product == gcr_product.Product( + expire_time=timestamp_pb2.Timestamp(seconds=751) + ) assert args[0].product_id == "product_id_value" @@ -755,7 +801,9 @@ async def test_create_product_flattened_error_async(): await client.create_product( product_service.CreateProductRequest(), parent="parent_value", - product=gcr_product.Product(name="name_value"), + product=gcr_product.Product( + expire_time=timestamp_pb2.Timestamp(seconds=751) + ), product_id="product_id_value", ) @@ -779,12 +827,21 @@ def test_get_product( id="id_value", type_=product.Product.Type.PRIMARY, primary_product_id="primary_product_id_value", + collection_member_ids=["collection_member_ids_value"], + gtin="gtin_value", categories=["categories_value"], title="title_value", + brands=["brands_value"], description="description_value", + language_code="language_code_value", tags=["tags_value"], availability=product.Product.Availability.IN_STOCK, uri="uri_value", + sizes=["sizes_value"], + materials=["materials_value"], + patterns=["patterns_value"], + conditions=["conditions_value"], + expire_time=timestamp_pb2.Timestamp(seconds=751), ) response = client.get_product(request) @@ -799,12 +856,20 @@ def test_get_product( assert response.id == "id_value" assert response.type_ == product.Product.Type.PRIMARY assert response.primary_product_id == "primary_product_id_value" + assert response.collection_member_ids == ["collection_member_ids_value"] + assert response.gtin == "gtin_value" assert response.categories == ["categories_value"] assert response.title == "title_value" + assert response.brands == ["brands_value"] assert response.description == "description_value" + assert response.language_code == "language_code_value" assert response.tags == ["tags_value"] assert response.availability == product.Product.Availability.IN_STOCK assert response.uri == "uri_value" + assert response.sizes == ["sizes_value"] + assert response.materials == ["materials_value"] + assert response.patterns == ["patterns_value"] + assert response.conditions == ["conditions_value"] def test_get_product_from_dict(): @@ -847,12 +912,20 @@ async def test_get_product_async( id="id_value", type_=product.Product.Type.PRIMARY, primary_product_id="primary_product_id_value", + collection_member_ids=["collection_member_ids_value"], + gtin="gtin_value", categories=["categories_value"], title="title_value", + brands=["brands_value"], description="description_value", + language_code="language_code_value", tags=["tags_value"], availability=product.Product.Availability.IN_STOCK, uri="uri_value", + sizes=["sizes_value"], + materials=["materials_value"], + patterns=["patterns_value"], + conditions=["conditions_value"], ) ) response = await client.get_product(request) @@ -868,12 +941,20 @@ async def test_get_product_async( assert response.id == "id_value" assert response.type_ == product.Product.Type.PRIMARY assert response.primary_product_id == "primary_product_id_value" + assert response.collection_member_ids == ["collection_member_ids_value"] + assert response.gtin == "gtin_value" assert response.categories == ["categories_value"] assert response.title == "title_value" + assert response.brands == ["brands_value"] assert response.description == "description_value" + assert response.language_code == "language_code_value" assert response.tags == ["tags_value"] assert response.availability == product.Product.Availability.IN_STOCK assert response.uri == "uri_value" + assert response.sizes == ["sizes_value"] + assert response.materials == ["materials_value"] + assert response.patterns == ["patterns_value"] + assert response.conditions == ["conditions_value"] @pytest.mark.asyncio @@ -998,6 +1079,334 @@ async def test_get_product_flattened_error_async(): ) +def test_list_products( + transport: str = "grpc", request_type=product_service.ListProductsRequest +): + client = ProductServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_products), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = product_service.ListProductsResponse( + next_page_token="next_page_token_value", + ) + response = client.list_products(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == product_service.ListProductsRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, pagers.ListProductsPager) + assert response.next_page_token == "next_page_token_value" + + +def test_list_products_from_dict(): + test_list_products(request_type=dict) + + +def test_list_products_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = ProductServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_products), "__call__") as call: + client.list_products() + call.assert_called() + _, args, _ = call.mock_calls[0] + assert args[0] == product_service.ListProductsRequest() + + +@pytest.mark.asyncio +async def test_list_products_async( + transport: str = "grpc_asyncio", request_type=product_service.ListProductsRequest +): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_products), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + product_service.ListProductsResponse( + next_page_token="next_page_token_value", + ) + ) + response = await client.list_products(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == product_service.ListProductsRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, pagers.ListProductsAsyncPager) + assert response.next_page_token == "next_page_token_value" + + +@pytest.mark.asyncio +async def test_list_products_async_from_dict(): + await test_list_products_async(request_type=dict) + + +def test_list_products_field_headers(): + client = ProductServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = product_service.ListProductsRequest() + + request.parent = "parent/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_products), "__call__") as call: + call.return_value = product_service.ListProductsResponse() + client.list_products(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "parent=parent/value",) in kw["metadata"] + + +@pytest.mark.asyncio +async def test_list_products_field_headers_async(): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = product_service.ListProductsRequest() + + request.parent = "parent/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_products), "__call__") as call: + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + product_service.ListProductsResponse() + ) + await client.list_products(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "parent=parent/value",) in kw["metadata"] + + +def test_list_products_flattened(): + client = ProductServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_products), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = product_service.ListProductsResponse() + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + client.list_products(parent="parent_value",) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0].parent == "parent_value" + + +def test_list_products_flattened_error(): + client = ProductServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + client.list_products( + product_service.ListProductsRequest(), parent="parent_value", + ) + + +@pytest.mark.asyncio +async def test_list_products_flattened_async(): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_products), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = product_service.ListProductsResponse() + + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + product_service.ListProductsResponse() + ) + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + response = await client.list_products(parent="parent_value",) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0].parent == "parent_value" + + +@pytest.mark.asyncio +async def test_list_products_flattened_error_async(): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + await client.list_products( + product_service.ListProductsRequest(), parent="parent_value", + ) + + +def test_list_products_pager(): + client = ProductServiceClient(credentials=ga_credentials.AnonymousCredentials,) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_products), "__call__") as call: + # Set the response to a series of pages. + call.side_effect = ( + product_service.ListProductsResponse( + products=[product.Product(), product.Product(), product.Product(),], + next_page_token="abc", + ), + product_service.ListProductsResponse(products=[], next_page_token="def",), + product_service.ListProductsResponse( + products=[product.Product(),], next_page_token="ghi", + ), + product_service.ListProductsResponse( + products=[product.Product(), product.Product(),], + ), + RuntimeError, + ) + + metadata = () + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("parent", ""),)), + ) + pager = client.list_products(request={}) + + assert pager._metadata == metadata + + results = [i for i in pager] + assert len(results) == 6 + assert all(isinstance(i, product.Product) for i in results) + + +def test_list_products_pages(): + client = ProductServiceClient(credentials=ga_credentials.AnonymousCredentials,) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_products), "__call__") as call: + # Set the response to a series of pages. + call.side_effect = ( + product_service.ListProductsResponse( + products=[product.Product(), product.Product(), product.Product(),], + next_page_token="abc", + ), + product_service.ListProductsResponse(products=[], next_page_token="def",), + product_service.ListProductsResponse( + products=[product.Product(),], next_page_token="ghi", + ), + product_service.ListProductsResponse( + products=[product.Product(), product.Product(),], + ), + RuntimeError, + ) + pages = list(client.list_products(request={}).pages) + for page_, token in zip(pages, ["abc", "def", "ghi", ""]): + assert page_.raw_page.next_page_token == token + + +@pytest.mark.asyncio +async def test_list_products_async_pager(): + client = ProductServiceAsyncClient(credentials=ga_credentials.AnonymousCredentials,) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.list_products), "__call__", new_callable=mock.AsyncMock + ) as call: + # Set the response to a series of pages. + call.side_effect = ( + product_service.ListProductsResponse( + products=[product.Product(), product.Product(), product.Product(),], + next_page_token="abc", + ), + product_service.ListProductsResponse(products=[], next_page_token="def",), + product_service.ListProductsResponse( + products=[product.Product(),], next_page_token="ghi", + ), + product_service.ListProductsResponse( + products=[product.Product(), product.Product(),], + ), + RuntimeError, + ) + async_pager = await client.list_products(request={},) + assert async_pager.next_page_token == "abc" + responses = [] + async for response in async_pager: + responses.append(response) + + assert len(responses) == 6 + assert all(isinstance(i, product.Product) for i in responses) + + +@pytest.mark.asyncio +async def test_list_products_async_pages(): + client = ProductServiceAsyncClient(credentials=ga_credentials.AnonymousCredentials,) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.list_products), "__call__", new_callable=mock.AsyncMock + ) as call: + # Set the response to a series of pages. + call.side_effect = ( + product_service.ListProductsResponse( + products=[product.Product(), product.Product(), product.Product(),], + next_page_token="abc", + ), + product_service.ListProductsResponse(products=[], next_page_token="def",), + product_service.ListProductsResponse( + products=[product.Product(),], next_page_token="ghi", + ), + product_service.ListProductsResponse( + products=[product.Product(), product.Product(),], + ), + RuntimeError, + ) + pages = [] + async for page_ in (await client.list_products(request={})).pages: + pages.append(page_) + for page_, token in zip(pages, ["abc", "def", "ghi", ""]): + assert page_.raw_page.next_page_token == token + + def test_update_product( transport: str = "grpc", request_type=product_service.UpdateProductRequest ): @@ -1017,12 +1426,21 @@ def test_update_product( id="id_value", type_=gcr_product.Product.Type.PRIMARY, primary_product_id="primary_product_id_value", + collection_member_ids=["collection_member_ids_value"], + gtin="gtin_value", categories=["categories_value"], title="title_value", + brands=["brands_value"], description="description_value", + language_code="language_code_value", tags=["tags_value"], availability=gcr_product.Product.Availability.IN_STOCK, uri="uri_value", + sizes=["sizes_value"], + materials=["materials_value"], + patterns=["patterns_value"], + conditions=["conditions_value"], + expire_time=timestamp_pb2.Timestamp(seconds=751), ) response = client.update_product(request) @@ -1037,12 +1455,20 @@ def test_update_product( assert response.id == "id_value" assert response.type_ == gcr_product.Product.Type.PRIMARY assert response.primary_product_id == "primary_product_id_value" + assert response.collection_member_ids == ["collection_member_ids_value"] + assert response.gtin == "gtin_value" assert response.categories == ["categories_value"] assert response.title == "title_value" + assert response.brands == ["brands_value"] assert response.description == "description_value" + assert response.language_code == "language_code_value" assert response.tags == ["tags_value"] assert response.availability == gcr_product.Product.Availability.IN_STOCK assert response.uri == "uri_value" + assert response.sizes == ["sizes_value"] + assert response.materials == ["materials_value"] + assert response.patterns == ["patterns_value"] + assert response.conditions == ["conditions_value"] def test_update_product_from_dict(): @@ -1085,12 +1511,20 @@ async def test_update_product_async( id="id_value", type_=gcr_product.Product.Type.PRIMARY, primary_product_id="primary_product_id_value", + collection_member_ids=["collection_member_ids_value"], + gtin="gtin_value", categories=["categories_value"], title="title_value", + brands=["brands_value"], description="description_value", + language_code="language_code_value", tags=["tags_value"], availability=gcr_product.Product.Availability.IN_STOCK, uri="uri_value", + sizes=["sizes_value"], + materials=["materials_value"], + patterns=["patterns_value"], + conditions=["conditions_value"], ) ) response = await client.update_product(request) @@ -1106,12 +1540,20 @@ async def test_update_product_async( assert response.id == "id_value" assert response.type_ == gcr_product.Product.Type.PRIMARY assert response.primary_product_id == "primary_product_id_value" + assert response.collection_member_ids == ["collection_member_ids_value"] + assert response.gtin == "gtin_value" assert response.categories == ["categories_value"] assert response.title == "title_value" + assert response.brands == ["brands_value"] assert response.description == "description_value" + assert response.language_code == "language_code_value" assert response.tags == ["tags_value"] assert response.availability == gcr_product.Product.Availability.IN_STOCK assert response.uri == "uri_value" + assert response.sizes == ["sizes_value"] + assert response.materials == ["materials_value"] + assert response.patterns == ["patterns_value"] + assert response.conditions == ["conditions_value"] @pytest.mark.asyncio @@ -1184,7 +1626,9 @@ def test_update_product_flattened(): # Call the method with a truthy value for each flattened field, # using the keyword arguments to the method. client.update_product( - product=gcr_product.Product(name="name_value"), + product=gcr_product.Product( + expire_time=timestamp_pb2.Timestamp(seconds=751) + ), update_mask=field_mask_pb2.FieldMask(paths=["paths_value"]), ) @@ -1192,7 +1636,9 @@ def test_update_product_flattened(): # request object values. assert len(call.mock_calls) == 1 _, args, _ = call.mock_calls[0] - assert args[0].product == gcr_product.Product(name="name_value") + assert args[0].product == gcr_product.Product( + expire_time=timestamp_pb2.Timestamp(seconds=751) + ) assert args[0].update_mask == field_mask_pb2.FieldMask(paths=["paths_value"]) @@ -1204,7 +1650,9 @@ def test_update_product_flattened_error(): with pytest.raises(ValueError): client.update_product( product_service.UpdateProductRequest(), - product=gcr_product.Product(name="name_value"), + product=gcr_product.Product( + expire_time=timestamp_pb2.Timestamp(seconds=751) + ), update_mask=field_mask_pb2.FieldMask(paths=["paths_value"]), ) @@ -1224,7 +1672,9 @@ async def test_update_product_flattened_async(): # Call the method with a truthy value for each flattened field, # using the keyword arguments to the method. response = await client.update_product( - product=gcr_product.Product(name="name_value"), + product=gcr_product.Product( + expire_time=timestamp_pb2.Timestamp(seconds=751) + ), update_mask=field_mask_pb2.FieldMask(paths=["paths_value"]), ) @@ -1232,7 +1682,9 @@ async def test_update_product_flattened_async(): # request object values. assert len(call.mock_calls) _, args, _ = call.mock_calls[0] - assert args[0].product == gcr_product.Product(name="name_value") + assert args[0].product == gcr_product.Product( + expire_time=timestamp_pb2.Timestamp(seconds=751) + ) assert args[0].update_mask == field_mask_pb2.FieldMask(paths=["paths_value"]) @@ -1247,7 +1699,9 @@ async def test_update_product_flattened_error_async(): with pytest.raises(ValueError): await client.update_product( product_service.UpdateProductRequest(), - product=gcr_product.Product(name="name_value"), + product=gcr_product.Product( + expire_time=timestamp_pb2.Timestamp(seconds=751) + ), update_mask=field_mask_pb2.FieldMask(paths=["paths_value"]), ) @@ -1578,6 +2032,656 @@ async def test_import_products_field_headers_async(): assert ("x-goog-request-params", "parent=parent/value",) in kw["metadata"] +def test_set_inventory( + transport: str = "grpc", request_type=product_service.SetInventoryRequest +): + client = ProductServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.set_inventory), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/spam") + response = client.set_inventory(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == product_service.SetInventoryRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, future.Future) + + +def test_set_inventory_from_dict(): + test_set_inventory(request_type=dict) + + +def test_set_inventory_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = ProductServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.set_inventory), "__call__") as call: + client.set_inventory() + call.assert_called() + _, args, _ = call.mock_calls[0] + assert args[0] == product_service.SetInventoryRequest() + + +@pytest.mark.asyncio +async def test_set_inventory_async( + transport: str = "grpc_asyncio", request_type=product_service.SetInventoryRequest +): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.set_inventory), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/spam") + ) + response = await client.set_inventory(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == product_service.SetInventoryRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, future.Future) + + +@pytest.mark.asyncio +async def test_set_inventory_async_from_dict(): + await test_set_inventory_async(request_type=dict) + + +def test_set_inventory_field_headers(): + client = ProductServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = product_service.SetInventoryRequest() + + request.inventory.name = "inventory.name/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.set_inventory), "__call__") as call: + call.return_value = operations_pb2.Operation(name="operations/op") + client.set_inventory(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "inventory.name=inventory.name/value",) in kw[ + "metadata" + ] + + +@pytest.mark.asyncio +async def test_set_inventory_field_headers_async(): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = product_service.SetInventoryRequest() + + request.inventory.name = "inventory.name/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.set_inventory), "__call__") as call: + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/op") + ) + await client.set_inventory(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "inventory.name=inventory.name/value",) in kw[ + "metadata" + ] + + +def test_set_inventory_flattened(): + client = ProductServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.set_inventory), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/op") + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + client.set_inventory( + inventory=product.Product(expire_time=timestamp_pb2.Timestamp(seconds=751)), + set_mask=field_mask_pb2.FieldMask(paths=["paths_value"]), + ) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0].inventory == product.Product( + expire_time=timestamp_pb2.Timestamp(seconds=751) + ) + assert args[0].set_mask == field_mask_pb2.FieldMask(paths=["paths_value"]) + + +def test_set_inventory_flattened_error(): + client = ProductServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + client.set_inventory( + product_service.SetInventoryRequest(), + inventory=product.Product(expire_time=timestamp_pb2.Timestamp(seconds=751)), + set_mask=field_mask_pb2.FieldMask(paths=["paths_value"]), + ) + + +@pytest.mark.asyncio +async def test_set_inventory_flattened_async(): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.set_inventory), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/op") + + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/spam") + ) + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + response = await client.set_inventory( + inventory=product.Product(expire_time=timestamp_pb2.Timestamp(seconds=751)), + set_mask=field_mask_pb2.FieldMask(paths=["paths_value"]), + ) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0].inventory == product.Product( + expire_time=timestamp_pb2.Timestamp(seconds=751) + ) + assert args[0].set_mask == field_mask_pb2.FieldMask(paths=["paths_value"]) + + +@pytest.mark.asyncio +async def test_set_inventory_flattened_error_async(): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + await client.set_inventory( + product_service.SetInventoryRequest(), + inventory=product.Product(expire_time=timestamp_pb2.Timestamp(seconds=751)), + set_mask=field_mask_pb2.FieldMask(paths=["paths_value"]), + ) + + +def test_add_fulfillment_places( + transport: str = "grpc", request_type=product_service.AddFulfillmentPlacesRequest +): + client = ProductServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.add_fulfillment_places), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/spam") + response = client.add_fulfillment_places(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == product_service.AddFulfillmentPlacesRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, future.Future) + + +def test_add_fulfillment_places_from_dict(): + test_add_fulfillment_places(request_type=dict) + + +def test_add_fulfillment_places_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = ProductServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.add_fulfillment_places), "__call__" + ) as call: + client.add_fulfillment_places() + call.assert_called() + _, args, _ = call.mock_calls[0] + assert args[0] == product_service.AddFulfillmentPlacesRequest() + + +@pytest.mark.asyncio +async def test_add_fulfillment_places_async( + transport: str = "grpc_asyncio", + request_type=product_service.AddFulfillmentPlacesRequest, +): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.add_fulfillment_places), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/spam") + ) + response = await client.add_fulfillment_places(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == product_service.AddFulfillmentPlacesRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, future.Future) + + +@pytest.mark.asyncio +async def test_add_fulfillment_places_async_from_dict(): + await test_add_fulfillment_places_async(request_type=dict) + + +def test_add_fulfillment_places_field_headers(): + client = ProductServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = product_service.AddFulfillmentPlacesRequest() + + request.product = "product/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.add_fulfillment_places), "__call__" + ) as call: + call.return_value = operations_pb2.Operation(name="operations/op") + client.add_fulfillment_places(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "product=product/value",) in kw["metadata"] + + +@pytest.mark.asyncio +async def test_add_fulfillment_places_field_headers_async(): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = product_service.AddFulfillmentPlacesRequest() + + request.product = "product/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.add_fulfillment_places), "__call__" + ) as call: + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/op") + ) + await client.add_fulfillment_places(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "product=product/value",) in kw["metadata"] + + +def test_add_fulfillment_places_flattened(): + client = ProductServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.add_fulfillment_places), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/op") + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + client.add_fulfillment_places(product="product_value",) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0].product == "product_value" + + +def test_add_fulfillment_places_flattened_error(): + client = ProductServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + client.add_fulfillment_places( + product_service.AddFulfillmentPlacesRequest(), product="product_value", + ) + + +@pytest.mark.asyncio +async def test_add_fulfillment_places_flattened_async(): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.add_fulfillment_places), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/op") + + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/spam") + ) + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + response = await client.add_fulfillment_places(product="product_value",) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0].product == "product_value" + + +@pytest.mark.asyncio +async def test_add_fulfillment_places_flattened_error_async(): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + await client.add_fulfillment_places( + product_service.AddFulfillmentPlacesRequest(), product="product_value", + ) + + +def test_remove_fulfillment_places( + transport: str = "grpc", request_type=product_service.RemoveFulfillmentPlacesRequest +): + client = ProductServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.remove_fulfillment_places), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/spam") + response = client.remove_fulfillment_places(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == product_service.RemoveFulfillmentPlacesRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, future.Future) + + +def test_remove_fulfillment_places_from_dict(): + test_remove_fulfillment_places(request_type=dict) + + +def test_remove_fulfillment_places_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = ProductServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.remove_fulfillment_places), "__call__" + ) as call: + client.remove_fulfillment_places() + call.assert_called() + _, args, _ = call.mock_calls[0] + assert args[0] == product_service.RemoveFulfillmentPlacesRequest() + + +@pytest.mark.asyncio +async def test_remove_fulfillment_places_async( + transport: str = "grpc_asyncio", + request_type=product_service.RemoveFulfillmentPlacesRequest, +): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.remove_fulfillment_places), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/spam") + ) + response = await client.remove_fulfillment_places(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == product_service.RemoveFulfillmentPlacesRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, future.Future) + + +@pytest.mark.asyncio +async def test_remove_fulfillment_places_async_from_dict(): + await test_remove_fulfillment_places_async(request_type=dict) + + +def test_remove_fulfillment_places_field_headers(): + client = ProductServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = product_service.RemoveFulfillmentPlacesRequest() + + request.product = "product/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.remove_fulfillment_places), "__call__" + ) as call: + call.return_value = operations_pb2.Operation(name="operations/op") + client.remove_fulfillment_places(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "product=product/value",) in kw["metadata"] + + +@pytest.mark.asyncio +async def test_remove_fulfillment_places_field_headers_async(): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = product_service.RemoveFulfillmentPlacesRequest() + + request.product = "product/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.remove_fulfillment_places), "__call__" + ) as call: + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/op") + ) + await client.remove_fulfillment_places(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "product=product/value",) in kw["metadata"] + + +def test_remove_fulfillment_places_flattened(): + client = ProductServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.remove_fulfillment_places), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/op") + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + client.remove_fulfillment_places(product="product_value",) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0].product == "product_value" + + +def test_remove_fulfillment_places_flattened_error(): + client = ProductServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + client.remove_fulfillment_places( + product_service.RemoveFulfillmentPlacesRequest(), product="product_value", + ) + + +@pytest.mark.asyncio +async def test_remove_fulfillment_places_flattened_async(): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.remove_fulfillment_places), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation(name="operations/op") + + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation(name="operations/spam") + ) + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + response = await client.remove_fulfillment_places(product="product_value",) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0].product == "product_value" + + +@pytest.mark.asyncio +async def test_remove_fulfillment_places_flattened_error_async(): + client = ProductServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + await client.remove_fulfillment_places( + product_service.RemoveFulfillmentPlacesRequest(), product="product_value", + ) + + def test_credentials_transport_error(): # It is an error to provide credentials and a transport instance. transport = transports.ProductServiceGrpcTransport( @@ -1677,9 +2781,13 @@ def test_product_service_base_transport(): methods = ( "create_product", "get_product", + "list_products", "update_product", "delete_product", "import_products", + "set_inventory", + "add_fulfillment_places", + "remove_fulfillment_places", ) for method in methods: with pytest.raises(NotImplementedError): diff --git a/tests/unit/gapic/retail_v2/test_search_service.py b/tests/unit/gapic/retail_v2/test_search_service.py new file mode 100644 index 00000000..932bce09 --- /dev/null +++ b/tests/unit/gapic/retail_v2/test_search_service.py @@ -0,0 +1,1424 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import os +import mock +import packaging.version + +import grpc +from grpc.experimental import aio +import math +import pytest +from proto.marshal.rules.dates import DurationRule, TimestampRule + + +from google.api_core import client_options +from google.api_core import exceptions as core_exceptions +from google.api_core import gapic_v1 +from google.api_core import grpc_helpers +from google.api_core import grpc_helpers_async +from google.auth import credentials as ga_credentials +from google.auth.exceptions import MutualTLSChannelError +from google.cloud.retail_v2.services.search_service import SearchServiceAsyncClient +from google.cloud.retail_v2.services.search_service import SearchServiceClient +from google.cloud.retail_v2.services.search_service import pagers +from google.cloud.retail_v2.services.search_service import transports +from google.cloud.retail_v2.services.search_service.transports.base import ( + _GOOGLE_AUTH_VERSION, +) +from google.cloud.retail_v2.types import common +from google.cloud.retail_v2.types import search_service +from google.oauth2 import service_account +import google.auth + + +# TODO(busunkim): Once google-auth >= 1.25.0 is required transitively +# through google-api-core: +# - Delete the auth "less than" test cases +# - Delete these pytest markers (Make the "greater than or equal to" tests the default). +requires_google_auth_lt_1_25_0 = pytest.mark.skipif( + packaging.version.parse(_GOOGLE_AUTH_VERSION) >= packaging.version.parse("1.25.0"), + reason="This test requires google-auth < 1.25.0", +) +requires_google_auth_gte_1_25_0 = pytest.mark.skipif( + packaging.version.parse(_GOOGLE_AUTH_VERSION) < packaging.version.parse("1.25.0"), + reason="This test requires google-auth >= 1.25.0", +) + + +def client_cert_source_callback(): + return b"cert bytes", b"key bytes" + + +# If default endpoint is localhost, then default mtls endpoint will be the same. +# This method modifies the default endpoint so the client can produce a different +# mtls endpoint for endpoint testing purposes. +def modify_default_endpoint(client): + return ( + "foo.googleapis.com" + if ("localhost" in client.DEFAULT_ENDPOINT) + else client.DEFAULT_ENDPOINT + ) + + +def test__get_default_mtls_endpoint(): + api_endpoint = "example.googleapis.com" + api_mtls_endpoint = "example.mtls.googleapis.com" + sandbox_endpoint = "example.sandbox.googleapis.com" + sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" + non_googleapi = "api.example.com" + + assert SearchServiceClient._get_default_mtls_endpoint(None) is None + assert ( + SearchServiceClient._get_default_mtls_endpoint(api_endpoint) + == api_mtls_endpoint + ) + assert ( + SearchServiceClient._get_default_mtls_endpoint(api_mtls_endpoint) + == api_mtls_endpoint + ) + assert ( + SearchServiceClient._get_default_mtls_endpoint(sandbox_endpoint) + == sandbox_mtls_endpoint + ) + assert ( + SearchServiceClient._get_default_mtls_endpoint(sandbox_mtls_endpoint) + == sandbox_mtls_endpoint + ) + assert ( + SearchServiceClient._get_default_mtls_endpoint(non_googleapi) == non_googleapi + ) + + +@pytest.mark.parametrize( + "client_class", [SearchServiceClient, SearchServiceAsyncClient,] +) +def test_search_service_client_from_service_account_info(client_class): + creds = ga_credentials.AnonymousCredentials() + with mock.patch.object( + service_account.Credentials, "from_service_account_info" + ) as factory: + factory.return_value = creds + info = {"valid": True} + client = client_class.from_service_account_info(info) + assert client.transport._credentials == creds + assert isinstance(client, client_class) + + assert client.transport._host == "retail.googleapis.com:443" + + +@pytest.mark.parametrize( + "transport_class,transport_name", + [ + (transports.SearchServiceGrpcTransport, "grpc"), + (transports.SearchServiceGrpcAsyncIOTransport, "grpc_asyncio"), + ], +) +def test_search_service_client_service_account_always_use_jwt( + transport_class, transport_name +): + with mock.patch.object( + service_account.Credentials, "with_always_use_jwt_access", create=True + ) as use_jwt: + creds = service_account.Credentials(None, None, None) + transport = transport_class(credentials=creds, always_use_jwt_access=True) + use_jwt.assert_called_once_with(True) + + with mock.patch.object( + service_account.Credentials, "with_always_use_jwt_access", create=True + ) as use_jwt: + creds = service_account.Credentials(None, None, None) + transport = transport_class(credentials=creds, always_use_jwt_access=False) + use_jwt.assert_not_called() + + +@pytest.mark.parametrize( + "client_class", [SearchServiceClient, SearchServiceAsyncClient,] +) +def test_search_service_client_from_service_account_file(client_class): + creds = ga_credentials.AnonymousCredentials() + with mock.patch.object( + service_account.Credentials, "from_service_account_file" + ) as factory: + factory.return_value = creds + client = client_class.from_service_account_file("dummy/file/path.json") + assert client.transport._credentials == creds + assert isinstance(client, client_class) + + client = client_class.from_service_account_json("dummy/file/path.json") + assert client.transport._credentials == creds + assert isinstance(client, client_class) + + assert client.transport._host == "retail.googleapis.com:443" + + +def test_search_service_client_get_transport_class(): + transport = SearchServiceClient.get_transport_class() + available_transports = [ + transports.SearchServiceGrpcTransport, + ] + assert transport in available_transports + + transport = SearchServiceClient.get_transport_class("grpc") + assert transport == transports.SearchServiceGrpcTransport + + +@pytest.mark.parametrize( + "client_class,transport_class,transport_name", + [ + (SearchServiceClient, transports.SearchServiceGrpcTransport, "grpc"), + ( + SearchServiceAsyncClient, + transports.SearchServiceGrpcAsyncIOTransport, + "grpc_asyncio", + ), + ], +) +@mock.patch.object( + SearchServiceClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(SearchServiceClient), +) +@mock.patch.object( + SearchServiceAsyncClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(SearchServiceAsyncClient), +) +def test_search_service_client_client_options( + client_class, transport_class, transport_name +): + # Check that if channel is provided we won't create a new one. + with mock.patch.object(SearchServiceClient, "get_transport_class") as gtc: + transport = transport_class(credentials=ga_credentials.AnonymousCredentials()) + client = client_class(transport=transport) + gtc.assert_not_called() + + # Check that if channel is provided via str we will create a new one. + with mock.patch.object(SearchServiceClient, "get_transport_class") as gtc: + client = client_class(transport=transport_name) + gtc.assert_called() + + # Check the case api_endpoint is provided. + options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host="squid.clam.whelk", + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS_ENDPOINT is + # "never". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS_ENDPOINT is + # "always". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}): + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_MTLS_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + # Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS_ENDPOINT has + # unsupported value. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "Unsupported"}): + with pytest.raises(MutualTLSChannelError): + client = client_class() + + # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} + ): + with pytest.raises(ValueError): + client = client_class() + + # Check the case quota_project_id is provided + options = client_options.ClientOptions(quota_project_id="octopus") + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id="octopus", + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + +@pytest.mark.parametrize( + "client_class,transport_class,transport_name,use_client_cert_env", + [ + (SearchServiceClient, transports.SearchServiceGrpcTransport, "grpc", "true"), + ( + SearchServiceAsyncClient, + transports.SearchServiceGrpcAsyncIOTransport, + "grpc_asyncio", + "true", + ), + (SearchServiceClient, transports.SearchServiceGrpcTransport, "grpc", "false"), + ( + SearchServiceAsyncClient, + transports.SearchServiceGrpcAsyncIOTransport, + "grpc_asyncio", + "false", + ), + ], +) +@mock.patch.object( + SearchServiceClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(SearchServiceClient), +) +@mock.patch.object( + SearchServiceAsyncClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(SearchServiceAsyncClient), +) +@mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}) +def test_search_service_client_mtls_env_auto( + client_class, transport_class, transport_name, use_client_cert_env +): + # This tests the endpoint autoswitch behavior. Endpoint is autoswitched to the default + # mtls endpoint, if GOOGLE_API_USE_CLIENT_CERTIFICATE is "true" and client cert exists. + + # Check the case client_cert_source is provided. Whether client cert is used depends on + # GOOGLE_API_USE_CLIENT_CERTIFICATE value. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} + ): + options = client_options.ClientOptions( + client_cert_source=client_cert_source_callback + ) + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + + if use_client_cert_env == "false": + expected_client_cert_source = None + expected_host = client.DEFAULT_ENDPOINT + else: + expected_client_cert_source = client_cert_source_callback + expected_host = client.DEFAULT_MTLS_ENDPOINT + + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=expected_host, + scopes=None, + client_cert_source_for_mtls=expected_client_cert_source, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + # Check the case ADC client cert is provided. Whether client cert is used depends on + # GOOGLE_API_USE_CLIENT_CERTIFICATE value. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} + ): + with mock.patch.object(transport_class, "__init__") as patched: + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=client_cert_source_callback, + ): + if use_client_cert_env == "false": + expected_host = client.DEFAULT_ENDPOINT + expected_client_cert_source = None + else: + expected_host = client.DEFAULT_MTLS_ENDPOINT + expected_client_cert_source = client_cert_source_callback + + patched.return_value = None + client = client_class() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=expected_host, + scopes=None, + client_cert_source_for_mtls=expected_client_cert_source, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + # Check the case client_cert_source and ADC client cert are not provided. + with mock.patch.dict( + os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert_env} + ): + with mock.patch.object(transport_class, "__init__") as patched: + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=False, + ): + patched.return_value = None + client = client_class() + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + +@pytest.mark.parametrize( + "client_class,transport_class,transport_name", + [ + (SearchServiceClient, transports.SearchServiceGrpcTransport, "grpc"), + ( + SearchServiceAsyncClient, + transports.SearchServiceGrpcAsyncIOTransport, + "grpc_asyncio", + ), + ], +) +def test_search_service_client_client_options_scopes( + client_class, transport_class, transport_name +): + # Check the case scopes are provided. + options = client_options.ClientOptions(scopes=["1", "2"],) + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=["1", "2"], + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + +@pytest.mark.parametrize( + "client_class,transport_class,transport_name", + [ + (SearchServiceClient, transports.SearchServiceGrpcTransport, "grpc"), + ( + SearchServiceAsyncClient, + transports.SearchServiceGrpcAsyncIOTransport, + "grpc_asyncio", + ), + ], +) +def test_search_service_client_client_options_credentials_file( + client_class, transport_class, transport_name +): + # Check the case credentials file is provided. + options = client_options.ClientOptions(credentials_file="credentials.json") + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=None, + credentials_file="credentials.json", + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + +def test_search_service_client_client_options_from_dict(): + with mock.patch( + "google.cloud.retail_v2.services.search_service.transports.SearchServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = SearchServiceClient( + client_options={"api_endpoint": "squid.clam.whelk"} + ) + grpc_transport.assert_called_once_with( + credentials=None, + credentials_file=None, + host="squid.clam.whelk", + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + + +def test_search(transport: str = "grpc", request_type=search_service.SearchRequest): + client = SearchServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.search), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = search_service.SearchResponse( + total_size=1086, + corrected_query="corrected_query_value", + attribution_token="attribution_token_value", + next_page_token="next_page_token_value", + redirect_uri="redirect_uri_value", + ) + response = client.search(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == search_service.SearchRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, pagers.SearchPager) + assert response.total_size == 1086 + assert response.corrected_query == "corrected_query_value" + assert response.attribution_token == "attribution_token_value" + assert response.next_page_token == "next_page_token_value" + assert response.redirect_uri == "redirect_uri_value" + + +def test_search_from_dict(): + test_search(request_type=dict) + + +def test_search_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = SearchServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.search), "__call__") as call: + client.search() + call.assert_called() + _, args, _ = call.mock_calls[0] + assert args[0] == search_service.SearchRequest() + + +@pytest.mark.asyncio +async def test_search_async( + transport: str = "grpc_asyncio", request_type=search_service.SearchRequest +): + client = SearchServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.search), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + search_service.SearchResponse( + total_size=1086, + corrected_query="corrected_query_value", + attribution_token="attribution_token_value", + next_page_token="next_page_token_value", + redirect_uri="redirect_uri_value", + ) + ) + response = await client.search(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == search_service.SearchRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, pagers.SearchAsyncPager) + assert response.total_size == 1086 + assert response.corrected_query == "corrected_query_value" + assert response.attribution_token == "attribution_token_value" + assert response.next_page_token == "next_page_token_value" + assert response.redirect_uri == "redirect_uri_value" + + +@pytest.mark.asyncio +async def test_search_async_from_dict(): + await test_search_async(request_type=dict) + + +def test_search_field_headers(): + client = SearchServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = search_service.SearchRequest() + + request.placement = "placement/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.search), "__call__") as call: + call.return_value = search_service.SearchResponse() + client.search(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "placement=placement/value",) in kw["metadata"] + + +@pytest.mark.asyncio +async def test_search_field_headers_async(): + client = SearchServiceAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = search_service.SearchRequest() + + request.placement = "placement/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.search), "__call__") as call: + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + search_service.SearchResponse() + ) + await client.search(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "placement=placement/value",) in kw["metadata"] + + +def test_search_pager(): + client = SearchServiceClient(credentials=ga_credentials.AnonymousCredentials,) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.search), "__call__") as call: + # Set the response to a series of pages. + call.side_effect = ( + search_service.SearchResponse( + results=[ + search_service.SearchResponse.SearchResult(), + search_service.SearchResponse.SearchResult(), + search_service.SearchResponse.SearchResult(), + ], + next_page_token="abc", + ), + search_service.SearchResponse(results=[], next_page_token="def",), + search_service.SearchResponse( + results=[search_service.SearchResponse.SearchResult(),], + next_page_token="ghi", + ), + search_service.SearchResponse( + results=[ + search_service.SearchResponse.SearchResult(), + search_service.SearchResponse.SearchResult(), + ], + ), + RuntimeError, + ) + + metadata = () + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata((("placement", ""),)), + ) + pager = client.search(request={}) + + assert pager._metadata == metadata + + results = [i for i in pager] + assert len(results) == 6 + assert all( + isinstance(i, search_service.SearchResponse.SearchResult) for i in results + ) + + +def test_search_pages(): + client = SearchServiceClient(credentials=ga_credentials.AnonymousCredentials,) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.search), "__call__") as call: + # Set the response to a series of pages. + call.side_effect = ( + search_service.SearchResponse( + results=[ + search_service.SearchResponse.SearchResult(), + search_service.SearchResponse.SearchResult(), + search_service.SearchResponse.SearchResult(), + ], + next_page_token="abc", + ), + search_service.SearchResponse(results=[], next_page_token="def",), + search_service.SearchResponse( + results=[search_service.SearchResponse.SearchResult(),], + next_page_token="ghi", + ), + search_service.SearchResponse( + results=[ + search_service.SearchResponse.SearchResult(), + search_service.SearchResponse.SearchResult(), + ], + ), + RuntimeError, + ) + pages = list(client.search(request={}).pages) + for page_, token in zip(pages, ["abc", "def", "ghi", ""]): + assert page_.raw_page.next_page_token == token + + +@pytest.mark.asyncio +async def test_search_async_pager(): + client = SearchServiceAsyncClient(credentials=ga_credentials.AnonymousCredentials,) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.search), "__call__", new_callable=mock.AsyncMock + ) as call: + # Set the response to a series of pages. + call.side_effect = ( + search_service.SearchResponse( + results=[ + search_service.SearchResponse.SearchResult(), + search_service.SearchResponse.SearchResult(), + search_service.SearchResponse.SearchResult(), + ], + next_page_token="abc", + ), + search_service.SearchResponse(results=[], next_page_token="def",), + search_service.SearchResponse( + results=[search_service.SearchResponse.SearchResult(),], + next_page_token="ghi", + ), + search_service.SearchResponse( + results=[ + search_service.SearchResponse.SearchResult(), + search_service.SearchResponse.SearchResult(), + ], + ), + RuntimeError, + ) + async_pager = await client.search(request={},) + assert async_pager.next_page_token == "abc" + responses = [] + async for response in async_pager: + responses.append(response) + + assert len(responses) == 6 + assert all( + isinstance(i, search_service.SearchResponse.SearchResult) for i in responses + ) + + +@pytest.mark.asyncio +async def test_search_async_pages(): + client = SearchServiceAsyncClient(credentials=ga_credentials.AnonymousCredentials,) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.search), "__call__", new_callable=mock.AsyncMock + ) as call: + # Set the response to a series of pages. + call.side_effect = ( + search_service.SearchResponse( + results=[ + search_service.SearchResponse.SearchResult(), + search_service.SearchResponse.SearchResult(), + search_service.SearchResponse.SearchResult(), + ], + next_page_token="abc", + ), + search_service.SearchResponse(results=[], next_page_token="def",), + search_service.SearchResponse( + results=[search_service.SearchResponse.SearchResult(),], + next_page_token="ghi", + ), + search_service.SearchResponse( + results=[ + search_service.SearchResponse.SearchResult(), + search_service.SearchResponse.SearchResult(), + ], + ), + RuntimeError, + ) + pages = [] + async for page_ in (await client.search(request={})).pages: + pages.append(page_) + 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.SearchServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + with pytest.raises(ValueError): + client = SearchServiceClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # It is an error to provide a credentials file and a transport instance. + transport = transports.SearchServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + with pytest.raises(ValueError): + client = SearchServiceClient( + client_options={"credentials_file": "credentials.json"}, + transport=transport, + ) + + # It is an error to provide scopes and a transport instance. + transport = transports.SearchServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + with pytest.raises(ValueError): + client = SearchServiceClient( + client_options={"scopes": ["1", "2"]}, transport=transport, + ) + + +def test_transport_instance(): + # A client may be instantiated with a custom transport instance. + transport = transports.SearchServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + client = SearchServiceClient(transport=transport) + assert client.transport is transport + + +def test_transport_get_channel(): + # A client may be instantiated with a custom transport instance. + transport = transports.SearchServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + channel = transport.grpc_channel + assert channel + + transport = transports.SearchServiceGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + channel = transport.grpc_channel + assert channel + + +@pytest.mark.parametrize( + "transport_class", + [ + transports.SearchServiceGrpcTransport, + transports.SearchServiceGrpcAsyncIOTransport, + ], +) +def test_transport_adc(transport_class): + # Test default credentials are used if not provided. + with mock.patch.object(google.auth, "default") as adc: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport_class() + adc.assert_called_once() + + +def test_transport_grpc_default(): + # A client should use the gRPC transport by default. + client = SearchServiceClient(credentials=ga_credentials.AnonymousCredentials(),) + assert isinstance(client.transport, transports.SearchServiceGrpcTransport,) + + +def test_search_service_base_transport_error(): + # Passing both a credentials object and credentials_file should raise an error + with pytest.raises(core_exceptions.DuplicateCredentialArgs): + transport = transports.SearchServiceTransport( + credentials=ga_credentials.AnonymousCredentials(), + credentials_file="credentials.json", + ) + + +def test_search_service_base_transport(): + # Instantiate the base transport. + with mock.patch( + "google.cloud.retail_v2.services.search_service.transports.SearchServiceTransport.__init__" + ) as Transport: + Transport.return_value = None + transport = transports.SearchServiceTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Every method on the transport should just blindly + # raise NotImplementedError. + methods = ("search",) + for method in methods: + with pytest.raises(NotImplementedError): + getattr(transport, method)(request=object()) + + +@requires_google_auth_gte_1_25_0 +def test_search_service_base_transport_with_credentials_file(): + # Instantiate the base transport with a credentials file + with mock.patch.object( + google.auth, "load_credentials_from_file", autospec=True + ) as load_creds, mock.patch( + "google.cloud.retail_v2.services.search_service.transports.SearchServiceTransport._prep_wrapped_messages" + ) as Transport: + Transport.return_value = None + load_creds.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.SearchServiceTransport( + credentials_file="credentials.json", quota_project_id="octopus", + ) + load_creds.assert_called_once_with( + "credentials.json", + scopes=None, + default_scopes=("https://www.googleapis.com/auth/cloud-platform",), + quota_project_id="octopus", + ) + + +@requires_google_auth_lt_1_25_0 +def test_search_service_base_transport_with_credentials_file_old_google_auth(): + # Instantiate the base transport with a credentials file + with mock.patch.object( + google.auth, "load_credentials_from_file", autospec=True + ) as load_creds, mock.patch( + "google.cloud.retail_v2.services.search_service.transports.SearchServiceTransport._prep_wrapped_messages" + ) as Transport: + Transport.return_value = None + load_creds.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.SearchServiceTransport( + credentials_file="credentials.json", quota_project_id="octopus", + ) + load_creds.assert_called_once_with( + "credentials.json", + scopes=("https://www.googleapis.com/auth/cloud-platform",), + quota_project_id="octopus", + ) + + +def test_search_service_base_transport_with_adc(): + # Test the default credentials are used if credentials and credentials_file are None. + with mock.patch.object(google.auth, "default", autospec=True) as adc, mock.patch( + "google.cloud.retail_v2.services.search_service.transports.SearchServiceTransport._prep_wrapped_messages" + ) as Transport: + Transport.return_value = None + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.SearchServiceTransport() + adc.assert_called_once() + + +@requires_google_auth_gte_1_25_0 +def test_search_service_auth_adc(): + # If no credentials are provided, we should use ADC credentials. + with mock.patch.object(google.auth, "default", autospec=True) as adc: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + SearchServiceClient() + adc.assert_called_once_with( + scopes=None, + default_scopes=("https://www.googleapis.com/auth/cloud-platform",), + quota_project_id=None, + ) + + +@requires_google_auth_lt_1_25_0 +def test_search_service_auth_adc_old_google_auth(): + # If no credentials are provided, we should use ADC credentials. + with mock.patch.object(google.auth, "default", autospec=True) as adc: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + SearchServiceClient() + adc.assert_called_once_with( + scopes=("https://www.googleapis.com/auth/cloud-platform",), + quota_project_id=None, + ) + + +@pytest.mark.parametrize( + "transport_class", + [ + transports.SearchServiceGrpcTransport, + transports.SearchServiceGrpcAsyncIOTransport, + ], +) +@requires_google_auth_gte_1_25_0 +def test_search_service_transport_auth_adc(transport_class): + # If credentials and host are not provided, the transport class should use + # ADC credentials. + with mock.patch.object(google.auth, "default", autospec=True) as adc: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport_class(quota_project_id="octopus", scopes=["1", "2"]) + adc.assert_called_once_with( + scopes=["1", "2"], + default_scopes=("https://www.googleapis.com/auth/cloud-platform",), + quota_project_id="octopus", + ) + + +@pytest.mark.parametrize( + "transport_class", + [ + transports.SearchServiceGrpcTransport, + transports.SearchServiceGrpcAsyncIOTransport, + ], +) +@requires_google_auth_lt_1_25_0 +def test_search_service_transport_auth_adc_old_google_auth(transport_class): + # If credentials and host are not provided, the transport class should use + # ADC credentials. + with mock.patch.object(google.auth, "default", autospec=True) as adc: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport_class(quota_project_id="octopus") + adc.assert_called_once_with( + scopes=("https://www.googleapis.com/auth/cloud-platform",), + quota_project_id="octopus", + ) + + +@pytest.mark.parametrize( + "transport_class,grpc_helpers", + [ + (transports.SearchServiceGrpcTransport, grpc_helpers), + (transports.SearchServiceGrpcAsyncIOTransport, grpc_helpers_async), + ], +) +def test_search_service_transport_create_channel(transport_class, grpc_helpers): + # If credentials and host are not provided, the transport class should use + # ADC credentials. + with mock.patch.object( + google.auth, "default", autospec=True + ) as adc, mock.patch.object( + grpc_helpers, "create_channel", autospec=True + ) as create_channel: + creds = ga_credentials.AnonymousCredentials() + adc.return_value = (creds, None) + transport_class(quota_project_id="octopus", scopes=["1", "2"]) + + create_channel.assert_called_with( + "retail.googleapis.com:443", + credentials=creds, + credentials_file=None, + quota_project_id="octopus", + default_scopes=("https://www.googleapis.com/auth/cloud-platform",), + scopes=["1", "2"], + default_host="retail.googleapis.com", + ssl_credentials=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + +@pytest.mark.parametrize( + "transport_class", + [ + transports.SearchServiceGrpcTransport, + transports.SearchServiceGrpcAsyncIOTransport, + ], +) +def test_search_service_grpc_transport_client_cert_source_for_mtls(transport_class): + cred = ga_credentials.AnonymousCredentials() + + # Check ssl_channel_credentials is used if provided. + with mock.patch.object(transport_class, "create_channel") as mock_create_channel: + mock_ssl_channel_creds = mock.Mock() + transport_class( + host="squid.clam.whelk", + credentials=cred, + ssl_channel_credentials=mock_ssl_channel_creds, + ) + mock_create_channel.assert_called_once_with( + "squid.clam.whelk:443", + credentials=cred, + credentials_file=None, + scopes=None, + ssl_credentials=mock_ssl_channel_creds, + quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + + # Check if ssl_channel_credentials is not provided, then client_cert_source_for_mtls + # is used. + with mock.patch.object(transport_class, "create_channel", return_value=mock.Mock()): + with mock.patch("grpc.ssl_channel_credentials") as mock_ssl_cred: + transport_class( + credentials=cred, + client_cert_source_for_mtls=client_cert_source_callback, + ) + expected_cert, expected_key = client_cert_source_callback() + mock_ssl_cred.assert_called_once_with( + certificate_chain=expected_cert, private_key=expected_key + ) + + +def test_search_service_host_no_port(): + client = SearchServiceClient( + credentials=ga_credentials.AnonymousCredentials(), + client_options=client_options.ClientOptions( + api_endpoint="retail.googleapis.com" + ), + ) + assert client.transport._host == "retail.googleapis.com:443" + + +def test_search_service_host_with_port(): + client = SearchServiceClient( + credentials=ga_credentials.AnonymousCredentials(), + client_options=client_options.ClientOptions( + api_endpoint="retail.googleapis.com:8000" + ), + ) + assert client.transport._host == "retail.googleapis.com:8000" + + +def test_search_service_grpc_transport_channel(): + channel = grpc.secure_channel("http://localhost/", grpc.local_channel_credentials()) + + # Check that channel is used if provided. + transport = transports.SearchServiceGrpcTransport( + host="squid.clam.whelk", channel=channel, + ) + assert transport.grpc_channel == channel + assert transport._host == "squid.clam.whelk:443" + assert transport._ssl_channel_credentials == None + + +def test_search_service_grpc_asyncio_transport_channel(): + channel = aio.secure_channel("http://localhost/", grpc.local_channel_credentials()) + + # Check that channel is used if provided. + transport = transports.SearchServiceGrpcAsyncIOTransport( + host="squid.clam.whelk", channel=channel, + ) + assert transport.grpc_channel == channel + assert transport._host == "squid.clam.whelk:443" + assert transport._ssl_channel_credentials == None + + +# Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are +# removed from grpc/grpc_asyncio transport constructor. +@pytest.mark.parametrize( + "transport_class", + [ + transports.SearchServiceGrpcTransport, + transports.SearchServiceGrpcAsyncIOTransport, + ], +) +def test_search_service_transport_channel_mtls_with_client_cert_source(transport_class): + with mock.patch( + "grpc.ssl_channel_credentials", autospec=True + ) as grpc_ssl_channel_cred: + with mock.patch.object( + transport_class, "create_channel" + ) as grpc_create_channel: + mock_ssl_cred = mock.Mock() + grpc_ssl_channel_cred.return_value = mock_ssl_cred + + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + cred = ga_credentials.AnonymousCredentials() + with pytest.warns(DeprecationWarning): + with mock.patch.object(google.auth, "default") as adc: + adc.return_value = (cred, None) + transport = transport_class( + host="squid.clam.whelk", + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=client_cert_source_callback, + ) + adc.assert_called_once() + + grpc_ssl_channel_cred.assert_called_once_with( + certificate_chain=b"cert bytes", private_key=b"key bytes" + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=cred, + credentials_file=None, + scopes=None, + ssl_credentials=mock_ssl_cred, + quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + assert transport.grpc_channel == mock_grpc_channel + assert transport._ssl_channel_credentials == mock_ssl_cred + + +# Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are +# removed from grpc/grpc_asyncio transport constructor. +@pytest.mark.parametrize( + "transport_class", + [ + transports.SearchServiceGrpcTransport, + transports.SearchServiceGrpcAsyncIOTransport, + ], +) +def test_search_service_transport_channel_mtls_with_adc(transport_class): + mock_ssl_cred = mock.Mock() + with mock.patch.multiple( + "google.auth.transport.grpc.SslCredentials", + __init__=mock.Mock(return_value=None), + ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), + ): + with mock.patch.object( + transport_class, "create_channel" + ) as grpc_create_channel: + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + mock_cred = mock.Mock() + + with pytest.warns(DeprecationWarning): + transport = transport_class( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=None, + ) + + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + credentials_file=None, + scopes=None, + ssl_credentials=mock_ssl_cred, + quota_project_id=None, + options=[ + ("grpc.max_send_message_length", -1), + ("grpc.max_receive_message_length", -1), + ], + ) + assert transport.grpc_channel == mock_grpc_channel + + +def test_branch_path(): + project = "squid" + location = "clam" + catalog = "whelk" + branch = "octopus" + expected = "projects/{project}/locations/{location}/catalogs/{catalog}/branches/{branch}".format( + project=project, location=location, catalog=catalog, branch=branch, + ) + actual = SearchServiceClient.branch_path(project, location, catalog, branch) + assert expected == actual + + +def test_parse_branch_path(): + expected = { + "project": "oyster", + "location": "nudibranch", + "catalog": "cuttlefish", + "branch": "mussel", + } + path = SearchServiceClient.branch_path(**expected) + + # Check that the path construction is reversible. + actual = SearchServiceClient.parse_branch_path(path) + assert expected == actual + + +def test_product_path(): + project = "winkle" + location = "nautilus" + catalog = "scallop" + branch = "abalone" + product = "squid" + expected = "projects/{project}/locations/{location}/catalogs/{catalog}/branches/{branch}/products/{product}".format( + project=project, + location=location, + catalog=catalog, + branch=branch, + product=product, + ) + actual = SearchServiceClient.product_path( + project, location, catalog, branch, product + ) + assert expected == actual + + +def test_parse_product_path(): + expected = { + "project": "clam", + "location": "whelk", + "catalog": "octopus", + "branch": "oyster", + "product": "nudibranch", + } + path = SearchServiceClient.product_path(**expected) + + # Check that the path construction is reversible. + actual = SearchServiceClient.parse_product_path(path) + assert expected == actual + + +def test_common_billing_account_path(): + billing_account = "cuttlefish" + expected = "billingAccounts/{billing_account}".format( + billing_account=billing_account, + ) + actual = SearchServiceClient.common_billing_account_path(billing_account) + assert expected == actual + + +def test_parse_common_billing_account_path(): + expected = { + "billing_account": "mussel", + } + path = SearchServiceClient.common_billing_account_path(**expected) + + # Check that the path construction is reversible. + actual = SearchServiceClient.parse_common_billing_account_path(path) + assert expected == actual + + +def test_common_folder_path(): + folder = "winkle" + expected = "folders/{folder}".format(folder=folder,) + actual = SearchServiceClient.common_folder_path(folder) + assert expected == actual + + +def test_parse_common_folder_path(): + expected = { + "folder": "nautilus", + } + path = SearchServiceClient.common_folder_path(**expected) + + # Check that the path construction is reversible. + actual = SearchServiceClient.parse_common_folder_path(path) + assert expected == actual + + +def test_common_organization_path(): + organization = "scallop" + expected = "organizations/{organization}".format(organization=organization,) + actual = SearchServiceClient.common_organization_path(organization) + assert expected == actual + + +def test_parse_common_organization_path(): + expected = { + "organization": "abalone", + } + path = SearchServiceClient.common_organization_path(**expected) + + # Check that the path construction is reversible. + actual = SearchServiceClient.parse_common_organization_path(path) + assert expected == actual + + +def test_common_project_path(): + project = "squid" + expected = "projects/{project}".format(project=project,) + actual = SearchServiceClient.common_project_path(project) + assert expected == actual + + +def test_parse_common_project_path(): + expected = { + "project": "clam", + } + path = SearchServiceClient.common_project_path(**expected) + + # Check that the path construction is reversible. + actual = SearchServiceClient.parse_common_project_path(path) + assert expected == actual + + +def test_common_location_path(): + project = "whelk" + location = "octopus" + expected = "projects/{project}/locations/{location}".format( + project=project, location=location, + ) + actual = SearchServiceClient.common_location_path(project, location) + assert expected == actual + + +def test_parse_common_location_path(): + expected = { + "project": "oyster", + "location": "nudibranch", + } + path = SearchServiceClient.common_location_path(**expected) + + # Check that the path construction is reversible. + actual = SearchServiceClient.parse_common_location_path(path) + assert expected == actual + + +def test_client_withDEFAULT_CLIENT_INFO(): + client_info = gapic_v1.client_info.ClientInfo() + + with mock.patch.object( + transports.SearchServiceTransport, "_prep_wrapped_messages" + ) as prep: + client = SearchServiceClient( + credentials=ga_credentials.AnonymousCredentials(), client_info=client_info, + ) + prep.assert_called_once_with(client_info) + + with mock.patch.object( + transports.SearchServiceTransport, "_prep_wrapped_messages" + ) as prep: + transport_class = SearchServiceClient.get_transport_class() + transport = transport_class( + credentials=ga_credentials.AnonymousCredentials(), client_info=client_info, + ) + prep.assert_called_once_with(client_info) diff --git a/tests/unit/gapic/retail_v2/test_user_event_service.py b/tests/unit/gapic/retail_v2/test_user_event_service.py index 4dfd2e29..8216cf15 100644 --- a/tests/unit/gapic/retail_v2/test_user_event_service.py +++ b/tests/unit/gapic/retail_v2/test_user_event_service.py @@ -52,8 +52,11 @@ from google.longrunning import operations_pb2 from google.oauth2 import service_account from google.protobuf import any_pb2 # type: ignore +from google.protobuf import duration_pb2 # type: ignore +from google.protobuf import field_mask_pb2 # type: ignore from google.protobuf import timestamp_pb2 # type: ignore from google.protobuf import wrappers_pb2 # type: ignore +from google.type import date_pb2 # type: ignore import google.auth @@ -536,10 +539,14 @@ def test_write_user_event( call.return_value = user_event.UserEvent( event_type="event_type_value", visitor_id="visitor_id_value", + session_id="session_id_value", experiment_ids=["experiment_ids_value"], attribution_token="attribution_token_value", cart_id="cart_id_value", search_query="search_query_value", + filter="filter_value", + order_by="order_by_value", + offset=647, page_categories=["page_categories_value"], uri="uri_value", referrer_uri="referrer_uri_value", @@ -556,10 +563,14 @@ def test_write_user_event( assert isinstance(response, user_event.UserEvent) assert response.event_type == "event_type_value" assert response.visitor_id == "visitor_id_value" + assert response.session_id == "session_id_value" assert response.experiment_ids == ["experiment_ids_value"] assert response.attribution_token == "attribution_token_value" assert response.cart_id == "cart_id_value" assert response.search_query == "search_query_value" + assert response.filter == "filter_value" + assert response.order_by == "order_by_value" + assert response.offset == 647 assert response.page_categories == ["page_categories_value"] assert response.uri == "uri_value" assert response.referrer_uri == "referrer_uri_value" @@ -605,10 +616,14 @@ async def test_write_user_event_async( user_event.UserEvent( event_type="event_type_value", visitor_id="visitor_id_value", + session_id="session_id_value", experiment_ids=["experiment_ids_value"], attribution_token="attribution_token_value", cart_id="cart_id_value", search_query="search_query_value", + filter="filter_value", + order_by="order_by_value", + offset=647, page_categories=["page_categories_value"], uri="uri_value", referrer_uri="referrer_uri_value", @@ -626,10 +641,14 @@ async def test_write_user_event_async( assert isinstance(response, user_event.UserEvent) assert response.event_type == "event_type_value" assert response.visitor_id == "visitor_id_value" + assert response.session_id == "session_id_value" assert response.experiment_ids == ["experiment_ids_value"] assert response.attribution_token == "attribution_token_value" assert response.cart_id == "cart_id_value" assert response.search_query == "search_query_value" + assert response.filter == "filter_value" + assert response.order_by == "order_by_value" + assert response.offset == 647 assert response.page_categories == ["page_categories_value"] assert response.uri == "uri_value" assert response.referrer_uri == "referrer_uri_value" @@ -1751,12 +1770,36 @@ def test_user_event_service_grpc_lro_async_client(): assert transport.operations_client is transport.operations_client -def test_product_path(): +def test_catalog_path(): project = "squid" location = "clam" catalog = "whelk" - branch = "octopus" - product = "oyster" + expected = "projects/{project}/locations/{location}/catalogs/{catalog}".format( + project=project, location=location, catalog=catalog, + ) + actual = UserEventServiceClient.catalog_path(project, location, catalog) + assert expected == actual + + +def test_parse_catalog_path(): + expected = { + "project": "octopus", + "location": "oyster", + "catalog": "nudibranch", + } + path = UserEventServiceClient.catalog_path(**expected) + + # Check that the path construction is reversible. + actual = UserEventServiceClient.parse_catalog_path(path) + assert expected == actual + + +def test_product_path(): + project = "cuttlefish" + location = "mussel" + catalog = "winkle" + branch = "nautilus" + product = "scallop" expected = "projects/{project}/locations/{location}/catalogs/{catalog}/branches/{branch}/products/{product}".format( project=project, location=location, @@ -1772,11 +1815,11 @@ def test_product_path(): def test_parse_product_path(): expected = { - "project": "nudibranch", - "location": "cuttlefish", - "catalog": "mussel", - "branch": "winkle", - "product": "nautilus", + "project": "abalone", + "location": "squid", + "catalog": "clam", + "branch": "whelk", + "product": "octopus", } path = UserEventServiceClient.product_path(**expected) @@ -1786,7 +1829,7 @@ def test_parse_product_path(): def test_common_billing_account_path(): - billing_account = "scallop" + billing_account = "oyster" expected = "billingAccounts/{billing_account}".format( billing_account=billing_account, ) @@ -1796,7 +1839,7 @@ def test_common_billing_account_path(): def test_parse_common_billing_account_path(): expected = { - "billing_account": "abalone", + "billing_account": "nudibranch", } path = UserEventServiceClient.common_billing_account_path(**expected) @@ -1806,7 +1849,7 @@ def test_parse_common_billing_account_path(): def test_common_folder_path(): - folder = "squid" + folder = "cuttlefish" expected = "folders/{folder}".format(folder=folder,) actual = UserEventServiceClient.common_folder_path(folder) assert expected == actual @@ -1814,7 +1857,7 @@ def test_common_folder_path(): def test_parse_common_folder_path(): expected = { - "folder": "clam", + "folder": "mussel", } path = UserEventServiceClient.common_folder_path(**expected) @@ -1824,7 +1867,7 @@ def test_parse_common_folder_path(): def test_common_organization_path(): - organization = "whelk" + organization = "winkle" expected = "organizations/{organization}".format(organization=organization,) actual = UserEventServiceClient.common_organization_path(organization) assert expected == actual @@ -1832,7 +1875,7 @@ def test_common_organization_path(): def test_parse_common_organization_path(): expected = { - "organization": "octopus", + "organization": "nautilus", } path = UserEventServiceClient.common_organization_path(**expected) @@ -1842,7 +1885,7 @@ def test_parse_common_organization_path(): def test_common_project_path(): - project = "oyster" + project = "scallop" expected = "projects/{project}".format(project=project,) actual = UserEventServiceClient.common_project_path(project) assert expected == actual @@ -1850,7 +1893,7 @@ def test_common_project_path(): def test_parse_common_project_path(): expected = { - "project": "nudibranch", + "project": "abalone", } path = UserEventServiceClient.common_project_path(**expected) @@ -1860,8 +1903,8 @@ def test_parse_common_project_path(): def test_common_location_path(): - project = "cuttlefish" - location = "mussel" + project = "squid" + location = "clam" expected = "projects/{project}/locations/{location}".format( project=project, location=location, ) @@ -1871,8 +1914,8 @@ def test_common_location_path(): def test_parse_common_location_path(): expected = { - "project": "winkle", - "location": "nautilus", + "project": "whelk", + "location": "octopus", } path = UserEventServiceClient.common_location_path(**expected)