Source code for google.api_core.path_template

# Copyright 2017 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.

"""Expand and validate URL path templates.

This module provides the :func:`expand` and :func:`validate` functions for
interacting with Google-style URL `path templates`_ which are commonly used
in Google APIs for `resource names`_.

.. _path templates: https://github.com/googleapis/googleapis/blob
    /57e2d376ac7ef48681554204a3ba78a414f2c533/google/api/http.proto#L212
.. _resource names: https://cloud.google.com/apis/design/resource_names
"""

from __future__ import unicode_literals

import functools
import re

import six

# Regular expression for extracting variable parts from a path template.
# The variables can be expressed as:
#
# - "*": a single-segment positional variable, for example: "books/*"
# - "**": a multi-segment positional variable, for example: "shelf/**/book/*"
# - "{name}": a single-segment wildcard named variable, for example
#   "books/{name}"
# - "{name=*}: same as above.
# - "{name=**}": a multi-segment wildcard named variable, for example
#   "shelf/{name=**}"
# - "{name=/path/*/**}": a multi-segment named variable with a sub-template.
_VARIABLE_RE = re.compile(
    r"""
    (  # Capture the entire variable expression
        (?P<positional>\*\*?)  # Match & capture * and ** positional variables.
        |
        # Match & capture named variables {name}
        {
            (?P<name>[^/]+?)
            # Optionally match and capture the named variable's template.
            (?:=(?P<template>.+?))?
        }
    )
    """,
    re.VERBOSE,
)

# Segment expressions used for validating paths against a template.
_SINGLE_SEGMENT_PATTERN = r"([^/]+)"
_MULTI_SEGMENT_PATTERN = r"(.+)"


def _expand_variable_match(positional_vars, named_vars, match):
    """Expand a matched variable with its value.

    Args:
        positional_vars (list): A list of positonal variables. This list will
            be modified.
        named_vars (dict): A dictionary of named variables.
        match (re.Match): A regular expression match.

    Returns:
        str: The expanded variable to replace the match.

    Raises:
        ValueError: If a positional or named variable is required by the
            template but not specified or if an unexpected template expression
            is encountered.
    """
    positional = match.group("positional")
    name = match.group("name")
    if name is not None:
        try:
            return six.text_type(named_vars[name])
        except KeyError:
            raise ValueError(
                "Named variable '{}' not specified and needed by template "
                "`{}` at position {}".format(name, match.string, match.start())
            )
    elif positional is not None:
        try:
            return six.text_type(positional_vars.pop(0))
        except IndexError:
            raise ValueError(
                "Positional variable not specified and needed by template "
                "`{}` at position {}".format(match.string, match.start())
            )
    else:
        raise ValueError("Unknown template expression {}".format(match.group(0)))


[docs]def expand(tmpl, *args, **kwargs): """Expand a path template with the given variables. ..code-block:: python >>> expand('users/*/messages/*', 'me', '123') users/me/messages/123 >>> expand('/v1/{name=shelves/*/books/*}', name='shelves/1/books/3') /v1/shelves/1/books/3 Args: tmpl (str): The path template. args: The positional variables for the path. kwargs: The named variables for the path. Returns: str: The expanded path Raises: ValueError: If a positional or named variable is required by the template but not specified or if an unexpected template expression is encountered. """ replacer = functools.partial(_expand_variable_match, list(args), kwargs) return _VARIABLE_RE.sub(replacer, tmpl)
def _replace_variable_with_pattern(match): """Replace a variable match with a pattern that can be used to validate it. Args: match (re.Match): A regular expression match Returns: str: A regular expression pattern that can be used to validate the variable in an expanded path. Raises: ValueError: If an unexpected template expression is encountered. """ positional = match.group("positional") name = match.group("name") template = match.group("template") if name is not None: if not template: return _SINGLE_SEGMENT_PATTERN.format(name) elif template == "**": return _MULTI_SEGMENT_PATTERN.format(name) else: return _generate_pattern_for_template(template) elif positional == "*": return _SINGLE_SEGMENT_PATTERN elif positional == "**": return _MULTI_SEGMENT_PATTERN else: raise ValueError("Unknown template expression {}".format(match.group(0))) def _generate_pattern_for_template(tmpl): """Generate a pattern that can validate a path template. Args: tmpl (str): The path template Returns: str: A regular expression pattern that can be used to validate an expanded path template. """ return _VARIABLE_RE.sub(_replace_variable_with_pattern, tmpl)
[docs]def validate(tmpl, path): """Validate a path against the path template. .. code-block:: python >>> validate('users/*/messages/*', 'users/me/messages/123') True >>> validate('users/*/messages/*', 'users/me/drafts/123') False >>> validate('/v1/{name=shelves/*/books/*}', /v1/shelves/1/books/3) True >>> validate('/v1/{name=shelves/*/books/*}', /v1/shelves/1/tapes/3) False Args: tmpl (str): The path template. path (str): The expanded path. Returns: bool: True if the path matches. """ pattern = _generate_pattern_for_template(tmpl) + "$" return True if re.match(pattern, path) is not None else False