Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
path_template.py347 linesDownload Raw Back to api_core
1# Copyright 2017 Google LLC2#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 at6#7#     http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# 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 and13# limitations under the License.14 15"""Expand and validate URL path templates.16 17This module provides the :func:`expand` and :func:`validate` functions for18interacting with Google-style URL `path templates`_ which are commonly used19in Google APIs for `resource names`_.20 21.. _path templates: https://github.com/googleapis/googleapis/blob22    /57e2d376ac7ef48681554204a3ba78a414f2c533/google/api/http.proto#L21223.. _resource names: https://cloud.google.com/apis/design/resource_names24"""25 26from __future__ import unicode_literals27 28from collections import deque29import copy30import functools31import re32 33# Regular expression for extracting variable parts from a path template.34# The variables can be expressed as:35#36# - "*": a single-segment positional variable, for example: "books/*"37# - "**": a multi-segment positional variable, for example: "shelf/**/book/*"38# - "{name}": a single-segment wildcard named variable, for example39#   "books/{name}"40# - "{name=*}: same as above.41# - "{name=**}": a multi-segment wildcard named variable, for example42#   "shelf/{name=**}"43# - "{name=/path/*/**}": a multi-segment named variable with a sub-template.44_VARIABLE_RE = re.compile(45    r"""46    (  # Capture the entire variable expression47        (?P<positional>\*\*?)  # Match & capture * and ** positional variables.48        |49        # Match & capture named variables {name}50        {51            (?P<name>[^/]+?)52            # Optionally match and capture the named variable's template.53            (?:=(?P<template>.+?))?54        }55    )56    """,57    re.VERBOSE,58)59 60# Segment expressions used for validating paths against a template.61_SINGLE_SEGMENT_PATTERN = r"([^/]+)"62_MULTI_SEGMENT_PATTERN = r"(.+)"63 64 65def _expand_variable_match(positional_vars, named_vars, match):66    """Expand a matched variable with its value.67 68    Args:69        positional_vars (list): A list of positional variables. This list will70            be modified.71        named_vars (dict): A dictionary of named variables.72        match (re.Match): A regular expression match.73 74    Returns:75        str: The expanded variable to replace the match.76 77    Raises:78        ValueError: If a positional or named variable is required by the79            template but not specified or if an unexpected template expression80            is encountered.81    """82    positional = match.group("positional")83    name = match.group("name")84    if name is not None:85        try:86            return str(named_vars[name])87        except KeyError:88            raise ValueError(89                "Named variable '{}' not specified and needed by template "90                "`{}` at position {}".format(name, match.string, match.start())91            )92    elif positional is not None:93        try:94            return str(positional_vars.pop(0))95        except IndexError:96            raise ValueError(97                "Positional variable not specified and needed by template "98                "`{}` at position {}".format(match.string, match.start())99            )100    else:101        raise ValueError("Unknown template expression {}".format(match.group(0)))102 103 104def expand(tmpl, *args, **kwargs):105    """Expand a path template with the given variables.106 107    .. code-block:: python108 109        >>> expand('users/*/messages/*', 'me', '123')110        users/me/messages/123111        >>> expand('/v1/{name=shelves/*/books/*}', name='shelves/1/books/3')112        /v1/shelves/1/books/3113 114    Args:115        tmpl (str): The path template.116        args: The positional variables for the path.117        kwargs: The named variables for the path.118 119    Returns:120        str: The expanded path121 122    Raises:123        ValueError: If a positional or named variable is required by the124            template but not specified or if an unexpected template expression125            is encountered.126    """127    replacer = functools.partial(_expand_variable_match, list(args), kwargs)128    return _VARIABLE_RE.sub(replacer, tmpl)129 130 131def _replace_variable_with_pattern(match):132    """Replace a variable match with a pattern that can be used to validate it.133 134    Args:135        match (re.Match): A regular expression match136 137    Returns:138        str: A regular expression pattern that can be used to validate the139            variable in an expanded path.140 141    Raises:142        ValueError: If an unexpected template expression is encountered.143    """144    positional = match.group("positional")145    name = match.group("name")146    template = match.group("template")147    if name is not None:148        if not template:149            return _SINGLE_SEGMENT_PATTERN.format(name)150        elif template == "**":151            return _MULTI_SEGMENT_PATTERN.format(name)152        else:153            return _generate_pattern_for_template(template)154    elif positional == "*":155        return _SINGLE_SEGMENT_PATTERN156    elif positional == "**":157        return _MULTI_SEGMENT_PATTERN158    else:159        raise ValueError("Unknown template expression {}".format(match.group(0)))160 161 162def _generate_pattern_for_template(tmpl):163    """Generate a pattern that can validate a path template.164 165    Args:166        tmpl (str): The path template167 168    Returns:169        str: A regular expression pattern that can be used to validate an170            expanded path template.171    """172    return _VARIABLE_RE.sub(_replace_variable_with_pattern, tmpl)173 174 175def get_field(request, field):176    """Get the value of a field from a given dictionary.177 178    Args:179        request (dict | Message): A dictionary or a Message object.180        field (str): The key to the request in dot notation.181 182    Returns:183        The value of the field.184    """185    parts = field.split(".")186    value = request187 188    for part in parts:189        if not isinstance(value, dict):190            value = getattr(value, part, None)191        else:192            value = value.get(part)193    if isinstance(value, dict):194        return195    return value196 197 198def delete_field(request, field):199    """Delete the value of a field from a given dictionary.200 201    Args:202        request (dict | Message): A dictionary object or a Message.203        field (str): The key to the request in dot notation.204    """205    parts = deque(field.split("."))206    while len(parts) > 1:207        part = parts.popleft()208        if not isinstance(request, dict):209            if hasattr(request, part):210                request = getattr(request, part, None)211            else:212                return213        else:214            request = request.get(part)215    part = parts.popleft()216    if not isinstance(request, dict):217        if hasattr(request, part):218            request.ClearField(part)219        else:220            return221    else:222        request.pop(part, None)223 224 225def validate(tmpl, path):226    """Validate a path against the path template.227 228    .. code-block:: python229 230        >>> validate('users/*/messages/*', 'users/me/messages/123')231        True232        >>> validate('users/*/messages/*', 'users/me/drafts/123')233        False234        >>> validate('/v1/{name=shelves/*/books/*}', /v1/shelves/1/books/3)235        True236        >>> validate('/v1/{name=shelves/*/books/*}', /v1/shelves/1/tapes/3)237        False238 239    Args:240        tmpl (str): The path template.241        path (str): The expanded path.242 243    Returns:244        bool: True if the path matches.245    """246    pattern = _generate_pattern_for_template(tmpl) + "$"247    return True if re.match(pattern, path) is not None else False248 249 250def transcode(http_options, message=None, **request_kwargs):251    """Transcodes a grpc request pattern into a proper HTTP request following the rules outlined here,252    https://github.com/googleapis/googleapis/blob/master/google/api/http.proto#L44-L312253 254     Args:255         http_options (list(dict)): A list of dicts which consist of these keys,256             'method'    (str): The http method257             'uri'       (str): The path template258             'body'      (str): The body field name (optional)259             (This is a simplified representation of the proto option `google.api.http`)260 261         message (Message) : A request object (optional)262         request_kwargs (dict) : A dict representing the request object263 264     Returns:265         dict: The transcoded request with these keys,266             'method'        (str)   : The http method267             'uri'           (str)   : The expanded uri268             'body'          (dict | Message)  : A dict or a Message representing the body (optional)269             'query_params'  (dict | Message)  : A dict or Message mapping query parameter variables and values270 271     Raises:272         ValueError: If the request does not match the given template.273    """274    transcoded_value = message or request_kwargs275    bindings = []276    for http_option in http_options:277        request = {}278 279        # Assign path280        uri_template = http_option["uri"]281        fields = [282            (m.group("name"), m.group("template"))283            for m in _VARIABLE_RE.finditer(uri_template)284        ]285        bindings.append((uri_template, fields))286 287        path_args = {field: get_field(transcoded_value, field) for field, _ in fields}288        request["uri"] = expand(uri_template, **path_args)289 290        if not validate(uri_template, request["uri"]) or not all(path_args.values()):291            continue292 293        # Remove fields used in uri path from request294        leftovers = copy.deepcopy(transcoded_value)295        for path_field, _ in fields:296            delete_field(leftovers, path_field)297 298        # Assign body and query params299        body = http_option.get("body")300 301        if body:302            if body == "*":303                request["body"] = leftovers304                if message:305                    request["query_params"] = message.__class__()306                else:307                    request["query_params"] = {}308            else:309                try:310                    if message:311                        request["body"] = getattr(leftovers, body)312                        delete_field(leftovers, body)313                    else:314                        request["body"] = leftovers.pop(body)315                except (KeyError, AttributeError):316                    continue317                request["query_params"] = leftovers318        else:319            request["query_params"] = leftovers320        request["method"] = http_option["method"]321        return request322 323    bindings_description = [324        '\n\tURI: "{}"'325        "\n\tRequired request fields:\n\t\t{}".format(326            uri,327            "\n\t\t".join(328                [329                    'field: "{}", pattern: "{}"'.format(n, p if p else "*")330                    for n, p in fields331                ]332            ),333        )334        for uri, fields in bindings335    ]336 337    raise ValueError(338        "Invalid request."339        "\nSome of the fields of the request message are either not initialized or "340        "initialized with an invalid value."341        "\nPlease make sure your request matches at least one accepted HTTP binding."342        "\nTo match a binding the request message must have all the required fields "343        "initialized with values matching their patterns as listed below:{}".format(344            "\n".join(bindings_description)345        )346    )347 
codekingpro/portable-devtools · Team Ai