1# Copyright 2016 Google LLC 
    2# 
    3# Licensed under the Apache License, Version 2.0 (the "License"); 
    4# you may not use this file except in compliance with the License. 
    5# You may obtain a copy of the License at 
    6# 
    7#     http://www.apache.org/licenses/LICENSE-2.0 
    8# 
    9# Unless required by applicable law or agreed to in writing, software 
    10# distributed under the License is distributed on an "AS IS" BASIS, 
    11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
    12# See the License for the specific language governing permissions and 
    13# limitations under the License. 
    14 
    15"""Wrapper for adapting the autogenerated gapic client to the hand-written 
    16client.""" 
    17 
    18from google.cloud.logging_v2.services.config_service_v2 import ConfigServiceV2Client 
    19from google.cloud.logging_v2.services.logging_service_v2 import LoggingServiceV2Client 
    20from google.cloud.logging_v2.services.metrics_service_v2 import MetricsServiceV2Client 
    21from google.cloud.logging_v2.types import CreateSinkRequest 
    22from google.cloud.logging_v2.types import UpdateSinkRequest 
    23from google.cloud.logging_v2.types import ListSinksRequest 
    24from google.cloud.logging_v2.types import ListLogMetricsRequest 
    25from google.cloud.logging_v2.types import ListLogEntriesRequest 
    26from google.cloud.logging_v2.types import WriteLogEntriesRequest 
    27from google.cloud.logging_v2.types import LogSink 
    28from google.cloud.logging_v2.types import LogMetric 
    29from google.cloud.logging_v2.types import LogEntry as LogEntryPB 
    30 
    31from google.protobuf.json_format import MessageToDict 
    32from google.protobuf.json_format import ParseDict 
    33from google.protobuf.json_format import ParseError 
    34 
    35from google.cloud.logging_v2._helpers import entry_from_resource 
    36from google.cloud.logging_v2.sink import Sink 
    37from google.cloud.logging_v2.metric import Metric 
    38 
    39from google.api_core import client_info 
    40from google.api_core import gapic_v1 
    41 
    42 
    43class _LoggingAPI(object): 
    44    """Helper mapping logging-related APIs.""" 
    45 
    46    def __init__(self, gapic_api, client): 
    47        self._gapic_api = gapic_api 
    48        self._client = client 
    49 
    50    def list_entries( 
    51        self, 
    52        resource_names, 
    53        *, 
    54        filter_=None, 
    55        order_by=None, 
    56        max_results=None, 
    57        page_size=None, 
    58        page_token=None, 
    59    ): 
    60        """Return a generator of log entry resources. 
    61 
    62        Args: 
    63            resource_names (Sequence[str]): Names of one or more parent resources 
    64                from which to retrieve log entries: 
    65 
    66                :: 
    67 
    68                    "projects/[PROJECT_ID]" 
    69                    "organizations/[ORGANIZATION_ID]" 
    70                    "billingAccounts/[BILLING_ACCOUNT_ID]" 
    71                    "folders/[FOLDER_ID]" 
    72 
    73            filter_ (str): a filter expression. See 
    74                https://cloud.google.com/logging/docs/view/advanced_filters 
    75            order_by (str) One of :data:`~logging_v2.ASCENDING` 
    76                or :data:`~logging_v2.DESCENDING`. 
    77            max_results (Optional[int]): 
    78                Optional. The maximum number of entries to return. 
    79                Non-positive values are treated as 0. If None, uses API defaults. 
    80            page_size (int): number of entries to fetch in each API call. Although 
    81                requests are paged internally, logs are returned by the generator 
    82                one at a time. If not passed, defaults to a value set by the API. 
    83            page_token (str): opaque marker for the starting "page" of entries. If not 
    84                passed, the API will return the first page of entries. 
    85        Returns: 
    86            Generator[~logging_v2.LogEntry] 
    87        """ 
    88        # full resource names are expected by the API 
    89        resource_names = resource_names 
    90        request = ListLogEntriesRequest( 
    91            resource_names=resource_names, 
    92            filter=filter_, 
    93            order_by=order_by, 
    94            page_size=page_size, 
    95            page_token=page_token, 
    96        ) 
    97 
    98        response = self._gapic_api.list_log_entries(request=request) 
    99        log_iter = iter(response) 
    100 
    101        # We attach a mutable loggers dictionary so that as Logger 
    102        # objects are created by entry_from_resource, they can be 
    103        # re-used by other log entries from the same logger. 
    104        loggers = {} 
    105 
    106        if max_results is not None and max_results < 0: 
    107            raise ValueError("max_results must be positive") 
    108 
    109        # create generator 
    110        def log_entries_pager(log_iter): 
    111            i = 0 
    112            for entry in log_iter: 
    113                if max_results is not None and i >= max_results: 
    114                    break 
    115                log_entry_dict = _parse_log_entry(LogEntryPB.pb(entry)) 
    116                yield entry_from_resource(log_entry_dict, self._client, loggers=loggers) 
    117                i += 1 
    118 
    119        return log_entries_pager(log_iter) 
    120 
    121    def write_entries( 
    122        self, 
    123        entries, 
    124        *, 
    125        logger_name=None, 
    126        resource=None, 
    127        labels=None, 
    128        partial_success=True, 
    129        dry_run=False, 
    130    ): 
    131        """Log an entry resource via a POST request 
    132 
    133        Args: 
    134            entries (Sequence[Mapping[str, ...]]): sequence of mappings representing 
    135                the log entry resources to log. 
    136            logger_name (Optional[str]): name of default logger to which to log the entries; 
    137                individual entries may override. 
    138            resource(Optional[Mapping[str, ...]]): default resource to associate with entries; 
    139                individual entries may override. 
    140            labels (Optional[Mapping[str, ...]]): default labels to associate with entries; 
    141                individual entries may override. 
    142            partial_success (Optional[bool]): Whether valid entries should be written even if 
    143                some other entries fail due to INVALID_ARGUMENT or 
    144                PERMISSION_DENIED errors. If any entry is not written, then 
    145                the response status is the error associated with one of the 
    146                failed entries and the response includes error details keyed 
    147                by the entries' zero-based index in the ``entries.write`` 
    148                method. 
    149            dry_run (Optional[bool]): 
    150                If true, the request should expect normal response, 
    151                but the entries won't be persisted nor exported. 
    152                Useful for checking whether the logging API endpoints are working 
    153                properly before sending valuable data. 
    154        """ 
    155        try: 
    156            log_entry_pbs = [_log_entry_mapping_to_pb(entry) for entry in entries] 
    157        except ParseError as e: 
    158            raise ValueError(f"Invalid log entry: {str(e)}") from e 
    159 
    160        request = WriteLogEntriesRequest( 
    161            log_name=logger_name, 
    162            resource=resource, 
    163            labels=labels, 
    164            entries=log_entry_pbs, 
    165            partial_success=partial_success, 
    166        ) 
    167        self._gapic_api.write_log_entries(request=request) 
    168 
    169    def logger_delete(self, logger_name): 
    170        """Delete all entries in a logger. 
    171 
    172        Args: 
    173            logger_name (str):  The resource name of the log to delete: 
    174 
    175                :: 
    176 
    177                    "projects/[PROJECT_ID]/logs/[LOG_ID]" 
    178                    "organizations/[ORGANIZATION_ID]/logs/[LOG_ID]" 
    179                    "billingAccounts/[BILLING_ACCOUNT_ID]/logs/[LOG_ID]" 
    180                    "folders/[FOLDER_ID]/logs/[LOG_ID]" 
    181 
    182                ``[LOG_ID]`` must be URL-encoded. For example, 
    183                ``"projects/my-project-id/logs/syslog"``, 
    184                ``"organizations/1234567890/logs/cloudresourcemanager.googleapis.com%2Factivity"``. 
    185        """ 
    186        self._gapic_api.delete_log(log_name=logger_name) 
    187 
    188 
    189class _SinksAPI(object): 
    190    """Helper mapping sink-related APIs.""" 
    191 
    192    def __init__(self, gapic_api, client): 
    193        self._gapic_api = gapic_api 
    194        self._client = client 
    195 
    196    def list_sinks(self, parent, *, max_results=None, page_size=None, page_token=None): 
    197        """List sinks for the parent resource. 
    198 
    199        Args: 
    200            parent (str): The parent resource whose sinks are to be listed: 
    201 
    202                :: 
    203 
    204                    "projects/[PROJECT_ID]" 
    205                    "organizations/[ORGANIZATION_ID]" 
    206                    "billingAccounts/[BILLING_ACCOUNT_ID]" 
    207                    "folders/[FOLDER_ID]". 
    208            max_results (Optional[int]): 
    209                Optional. The maximum number of entries to return. 
    210                Non-positive values are treated as 0. If None, uses API defaults. 
    211            page_size (int): number of entries to fetch in each API call. Although 
    212                requests are paged internally, logs are returned by the generator 
    213                one at a time. If not passed, defaults to a value set by the API. 
    214            page_token (str): opaque marker for the starting "page" of entries. If not 
    215                passed, the API will return the first page of entries. 
    216 
    217        Returns: 
    218            Generator[~logging_v2.Sink] 
    219        """ 
    220        request = ListSinksRequest( 
    221            parent=parent, page_size=page_size, page_token=page_token 
    222        ) 
    223        response = self._gapic_api.list_sinks(request) 
    224        sink_iter = iter(response) 
    225 
    226        if max_results is not None and max_results < 0: 
    227            raise ValueError("max_results must be positive") 
    228 
    229        def sinks_pager(sink_iter): 
    230            i = 0 
    231            for entry in sink_iter: 
    232                if max_results is not None and i >= max_results: 
    233                    break 
    234                # Convert the GAPIC sink type into the handwritten `Sink` type 
    235                yield Sink.from_api_repr(LogSink.to_dict(entry), client=self._client) 
    236                i += 1 
    237 
    238        return sinks_pager(sink_iter) 
    239 
    240    def sink_create( 
    241        self, parent, sink_name, filter_, destination, *, unique_writer_identity=False 
    242    ): 
    243        """Create a sink resource. 
    244 
    245        See 
    246        https://cloud.google.com/logging/docs/reference/v2/rest/v2/projects.sinks/create 
    247 
    248        Args: 
    249            parent(str): The resource in which to create the sink, 
    250                including the parent resource and the sink identifier: 
    251 
    252            :: 
    253 
    254                "projects/[PROJECT_ID]" 
    255                "organizations/[ORGANIZATION_ID]" 
    256                "billingAccounts/[BILLING_ACCOUNT_ID]" 
    257                "folders/[FOLDER_ID]". 
    258            sink_name (str): The name of the sink. 
    259            filter_ (str): The advanced logs filter expression defining the 
    260                entries exported by the sink. 
    261            destination (str): Destination URI for the entries exported by 
    262                the sink. 
    263            unique_writer_identity (Optional[bool]):  determines the kind of 
    264                IAM identity returned as writer_identity in the new sink. 
    265 
    266        Returns: 
    267            dict: The sink resource returned from the API (converted from a 
    268                protobuf to a dictionary). 
    269        """ 
    270        sink_pb = LogSink(name=sink_name, filter=filter_, destination=destination) 
    271        request = CreateSinkRequest( 
    272            parent=parent, sink=sink_pb, unique_writer_identity=unique_writer_identity 
    273        ) 
    274        created_pb = self._gapic_api.create_sink(request=request) 
    275        return MessageToDict( 
    276            LogSink.pb(created_pb), 
    277            preserving_proto_field_name=False, 
    278        ) 
    279 
    280    def sink_get(self, sink_name): 
    281        """Retrieve a sink resource. 
    282 
    283        Args: 
    284            sink_name (str): The resource name of the sink, 
    285                including the parent resource and the sink identifier: 
    286 
    287            :: 
    288 
    289                "projects/[PROJECT_ID]/sinks/[SINK_ID]" 
    290                "organizations/[ORGANIZATION_ID]/sinks/[SINK_ID]" 
    291                "billingAccounts/[BILLING_ACCOUNT_ID]/sinks/[SINK_ID]" 
    292                "folders/[FOLDER_ID]/sinks/[SINK_ID]" 
    293 
    294        Returns: 
    295            dict: The sink object returned from the API (converted from a 
    296                protobuf to a dictionary). 
    297        """ 
    298        sink_pb = self._gapic_api.get_sink(sink_name=sink_name) 
    299        # NOTE: LogSink message type does not have an ``Any`` field 
    300        #       so `MessageToDict`` can safely be used. 
    301        return MessageToDict( 
    302            LogSink.pb(sink_pb), 
    303            preserving_proto_field_name=False, 
    304        ) 
    305 
    306    def sink_update( 
    307        self, 
    308        sink_name, 
    309        filter_, 
    310        destination, 
    311        *, 
    312        unique_writer_identity=False, 
    313    ): 
    314        """Update a sink resource. 
    315 
    316        Args: 
    317            sink_name (str): Required. The resource name of the sink, 
    318                including the parent resource and the sink identifier: 
    319 
    320            :: 
    321 
    322                "projects/[PROJECT_ID]/sinks/[SINK_ID]" 
    323                "organizations/[ORGANIZATION_ID]/sinks/[SINK_ID]" 
    324                "billingAccounts/[BILLING_ACCOUNT_ID]/sinks/[SINK_ID]" 
    325                "folders/[FOLDER_ID]/sinks/[SINK_ID]" 
    326            filter_ (str): The advanced logs filter expression defining the 
    327                entries exported by the sink. 
    328            destination (str): destination URI for the entries exported by 
    329                the sink. 
    330            unique_writer_identity (Optional[bool]): determines the kind of 
    331                IAM identity returned as writer_identity in the new sink. 
    332 
    333 
    334        Returns: 
    335            dict: The sink resource returned from the API (converted from a 
    336                  protobuf to a dictionary). 
    337        """ 
    338        name = sink_name.split("/")[-1]  # parse name out of full resource name 
    339        sink_pb = LogSink( 
    340            name=name, 
    341            filter=filter_, 
    342            destination=destination, 
    343        ) 
    344 
    345        request = UpdateSinkRequest( 
    346            sink_name=sink_name, 
    347            sink=sink_pb, 
    348            unique_writer_identity=unique_writer_identity, 
    349        ) 
    350        sink_pb = self._gapic_api.update_sink(request=request) 
    351        # NOTE: LogSink message type does not have an ``Any`` field 
    352        #       so `MessageToDict`` can safely be used. 
    353        return MessageToDict( 
    354            LogSink.pb(sink_pb), 
    355            preserving_proto_field_name=False, 
    356        ) 
    357 
    358    def sink_delete(self, sink_name): 
    359        """Delete a sink resource. 
    360 
    361        Args: 
    362            sink_name (str): Required. The full resource name of the sink to delete, 
    363            including the parent resource and the sink identifier: 
    364 
    365            :: 
    366 
    367                "projects/[PROJECT_ID]/sinks/[SINK_ID]" 
    368                "organizations/[ORGANIZATION_ID]/sinks/[SINK_ID]" 
    369                "billingAccounts/[BILLING_ACCOUNT_ID]/sinks/[SINK_ID]" 
    370                "folders/[FOLDER_ID]/sinks/[SINK_ID]" 
    371 
    372            Example: ``"projects/my-project-id/sinks/my-sink-id"``. 
    373        """ 
    374        self._gapic_api.delete_sink(sink_name=sink_name) 
    375 
    376 
    377class _MetricsAPI(object): 
    378    """Helper mapping sink-related APIs.""" 
    379 
    380    def __init__(self, gapic_api, client): 
    381        self._gapic_api = gapic_api 
    382        self._client = client 
    383 
    384    def list_metrics( 
    385        self, project, *, max_results=None, page_size=None, page_token=None 
    386    ): 
    387        """List metrics for the project associated with this client. 
    388 
    389        Args: 
    390            project (str): ID of the project whose metrics are to be listed. 
    391            max_results (Optional[int]): 
    392                Optional. The maximum number of entries to return. 
    393                Non-positive values are treated as 0. If None, uses API defaults. 
    394            page_size (int): number of entries to fetch in each API call. Although 
    395                requests are paged internally, logs are returned by the generator 
    396                one at a time. If not passed, defaults to a value set by the API. 
    397            page_token (str): opaque marker for the starting "page" of entries. If not 
    398                passed, the API will return the first page of entries. 
    399 
    400        Returns: 
    401            Generator[logging_v2.Metric] 
    402        """ 
    403        path = f"projects/{project}" 
    404        request = ListLogMetricsRequest( 
    405            parent=path, 
    406            page_size=page_size, 
    407            page_token=page_token, 
    408        ) 
    409        response = self._gapic_api.list_log_metrics(request=request) 
    410        metric_iter = iter(response) 
    411 
    412        if max_results is not None and max_results < 0: 
    413            raise ValueError("max_results must be positive") 
    414 
    415        def metrics_pager(metric_iter): 
    416            i = 0 
    417            for entry in metric_iter: 
    418                if max_results is not None and i >= max_results: 
    419                    break 
    420                # Convert GAPIC metrics type into handwritten `Metric` type 
    421                yield Metric.from_api_repr( 
    422                    LogMetric.to_dict(entry), client=self._client 
    423                ) 
    424                i += 1 
    425 
    426        return metrics_pager(metric_iter) 
    427 
    428    def metric_create(self, project, metric_name, filter_, description): 
    429        """Create a metric resource. 
    430 
    431        See 
    432        https://cloud.google.com/logging/docs/reference/v2/rest/v2/projects.metrics/create 
    433 
    434        Args: 
    435            project (str): ID of the project in which to create the metric. 
    436            metric_name (str): The name of the metric 
    437            filter_ (str): The advanced logs filter expression defining the 
    438                entries exported by the metric. 
    439            description (str): description of the metric. 
    440        """ 
    441        parent = f"projects/{project}" 
    442        metric_pb = LogMetric(name=metric_name, filter=filter_, description=description) 
    443        self._gapic_api.create_log_metric(parent=parent, metric=metric_pb) 
    444 
    445    def metric_get(self, project, metric_name): 
    446        """Retrieve a metric resource. 
    447 
    448        Args: 
    449            project (str): ID of the project containing the metric. 
    450            metric_name (str): The name of the metric 
    451 
    452        Returns: 
    453            dict: The metric object returned from the API (converted from a 
    454                  protobuf to a dictionary). 
    455        """ 
    456        path = f"projects/{project}/metrics/{metric_name}" 
    457        metric_pb = self._gapic_api.get_log_metric(metric_name=path) 
    458        # NOTE: LogMetric message type does not have an ``Any`` field 
    459        #       so `MessageToDict`` can safely be used. 
    460        return MessageToDict( 
    461            LogMetric.pb(metric_pb), 
    462            preserving_proto_field_name=False, 
    463        ) 
    464 
    465    def metric_update( 
    466        self, 
    467        project, 
    468        metric_name, 
    469        filter_, 
    470        description, 
    471    ): 
    472        """Update a metric resource. 
    473 
    474        Args: 
    475            project (str): ID of the project containing the metric. 
    476            metric_name (str): the name of the metric 
    477            filter_ (str): the advanced logs filter expression defining the 
    478                entries exported by the metric. 
    479            description (str): description of the metric. 
    480 
    481        Returns: 
    482            The metric object returned from the API (converted from a 
    483                  protobuf to a dictionary). 
    484        """ 
    485        path = f"projects/{project}/metrics/{metric_name}" 
    486        metric_pb = LogMetric( 
    487            name=path, 
    488            filter=filter_, 
    489            description=description, 
    490        ) 
    491        metric_pb = self._gapic_api.update_log_metric( 
    492            metric_name=path, metric=metric_pb 
    493        ) 
    494        # NOTE: LogMetric message type does not have an ``Any`` field 
    495        #       so `MessageToDict`` can safely be used. 
    496        return MessageToDict( 
    497            LogMetric.pb(metric_pb), 
    498            preserving_proto_field_name=False, 
    499        ) 
    500 
    501    def metric_delete(self, project, metric_name): 
    502        """Delete a metric resource. 
    503 
    504        Args: 
    505            project (str): ID of the project containing the metric. 
    506            metric_name (str): The name of the metric 
    507        """ 
    508        path = f"projects/{project}/metrics/{metric_name}" 
    509        self._gapic_api.delete_log_metric(metric_name=path) 
    510 
    511 
    512def _parse_log_entry(entry_pb): 
    513    """Special helper to parse ``LogEntry`` protobuf into a dictionary. 
    514 
    515    The ``proto_payload`` field in ``LogEntry`` is of type ``Any``. This 
    516    can be problematic if the type URL in the payload isn't in the 
    517    ``google.protobuf`` registry. To help with parsing unregistered types, 
    518    this function will remove ``proto_payload`` before parsing. 
    519 
    520    Args: 
    521        entry_pb (LogEntry): Log entry protobuf. 
    522 
    523    Returns: 
    524        dict: The parsed log entry. The ``protoPayload`` key may contain 
    525              the raw ``Any`` protobuf from ``entry_pb.proto_payload`` if 
    526              it could not be parsed. 
    527    """ 
    528    try: 
    529        return MessageToDict( 
    530            entry_pb, 
    531            preserving_proto_field_name=False, 
    532        ) 
    533    except TypeError: 
    534        if entry_pb.HasField("proto_payload"): 
    535            proto_payload = entry_pb.proto_payload 
    536            entry_pb.ClearField("proto_payload") 
    537            entry_mapping = MessageToDict( 
    538                entry_pb, 
    539                preserving_proto_field_name=False, 
    540            ) 
    541            entry_mapping["protoPayload"] = proto_payload 
    542            return entry_mapping 
    543        else: 
    544            raise 
    545 
    546 
    547def _log_entry_mapping_to_pb(mapping): 
    548    """Helper for :meth:`write_entries`, et aliae 
    549 
    550    Performs "impedance matching" between the protobuf attrs and 
    551    the keys expected in the JSON API. 
    552    """ 
    553    entry_pb = LogEntryPB.pb(LogEntryPB()) 
    554    # NOTE: We assume ``mapping`` was created in ``Batch.commit`` 
    555    #       or ``Logger._make_entry_resource``. In either case, if 
    556    #       the ``protoPayload`` key is present, we assume that the 
    557    #       type URL is registered with ``google.protobuf`` and will 
    558    #       not cause any issues in the JSON->protobuf conversion 
    559    #       of the corresponding ``proto_payload`` in the log entry 
    560    #       (it is an ``Any`` field). 
    561    ParseDict(mapping, entry_pb) 
    562    return LogEntryPB(entry_pb) 
    563 
    564 
    565def _client_info_to_gapic(input_info): 
    566    """ 
    567    Helper function to convert api_core.client_info to 
    568    api_core.gapic_v1.client_info subclass 
    569    """ 
    570    return gapic_v1.client_info.ClientInfo( 
    571        python_version=input_info.python_version, 
    572        grpc_version=input_info.grpc_version, 
    573        api_core_version=input_info.api_core_version, 
    574        gapic_version=input_info.gapic_version, 
    575        client_library_version=input_info.client_library_version, 
    576        user_agent=input_info.user_agent, 
    577        rest_version=input_info.rest_version, 
    578    ) 
    579 
    580 
    581def make_logging_api(client): 
    582    """Create an instance of the Logging API adapter. 
    583 
    584    Args: 
    585        client (~logging_v2.client.Client): The client 
    586            that holds configuration details. 
    587 
    588    Returns: 
    589        _LoggingAPI: A metrics API instance with the proper credentials. 
    590    """ 
    591    info = client._client_info 
    592    if isinstance(info, client_info.ClientInfo): 
    593        # convert into gapic-compatible subclass 
    594        info = _client_info_to_gapic(info) 
    595 
    596    generated = LoggingServiceV2Client( 
    597        credentials=client._credentials, 
    598        client_info=info, 
    599        client_options=client._client_options, 
    600    ) 
    601    return _LoggingAPI(generated, client) 
    602 
    603 
    604def make_metrics_api(client): 
    605    """Create an instance of the Metrics API adapter. 
    606 
    607    Args: 
    608        client (~logging_v2.client.Client): The client 
    609            that holds configuration details. 
    610 
    611    Returns: 
    612        _MetricsAPI: A metrics API instance with the proper credentials. 
    613    """ 
    614    info = client._client_info 
    615    if isinstance(info, client_info.ClientInfo): 
    616        # convert into gapic-compatible subclass 
    617        info = _client_info_to_gapic(info) 
    618 
    619    generated = MetricsServiceV2Client( 
    620        credentials=client._credentials, 
    621        client_info=info, 
    622        client_options=client._client_options, 
    623    ) 
    624    return _MetricsAPI(generated, client) 
    625 
    626 
    627def make_sinks_api(client): 
    628    """Create an instance of the Sinks API adapter. 
    629 
    630    Args: 
    631        client (~logging_v2.client.Client): The client 
    632            that holds configuration details. 
    633 
    634    Returns: 
    635        _SinksAPI: A metrics API instance with the proper credentials. 
    636    """ 
    637    info = client._client_info 
    638    if isinstance(info, client_info.ClientInfo): 
    639        # convert into gapic-compatible subclass 
    640        info = _client_info_to_gapic(info) 
    641 
    642    generated = ConfigServiceV2Client( 
    643        credentials=client._credentials, 
    644        client_info=info, 
    645        client_options=client._client_options, 
    646    ) 
    647    return _SinksAPI(generated, client)