<gh_stars>0
# Copyright 2013 Google Inc. All Rights Reserved.

"""Manage parsing resource arguments for the cloud platform.

The Parse() function and Registry.Parse() method are to be used whenever a
Google Cloud Platform API resource is indicated in a command-line argument.
URLs, bare names with hints, and any other acceptable spelling for a resource
will be accepted, and a consistent python object will be returned for use in
code.
"""

import re
import types
import urllib

from googlecloudapis.apitools.base import py as apitools_base
from googlecloudapis.apitools.base.py import util
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import properties


_COLLECTION_SUB_RE = r'[a-zA-Z]+(?:\.[a-zA-Z0-9]+)+'

_COLLECTIONPATH_RE = re.compile(
    r'(?:(?P<collection>{collection})::)?(?P<path>.+)'.format(
        collection=_COLLECTION_SUB_RE))
# The first two wildcards in this are the API and the API's version. The rest
# are parameters into a specific collection in that API/version.
_URL_RE = re.compile(r'(https?://[^/]+/[^/]+/[^/]+/)(.+)')
_METHOD_ID_RE = re.compile(r'(?P<collection>{collection})\.get'.format(
    collection=_COLLECTION_SUB_RE))


class Error(Exception):
  """Exceptions for this module."""


class _ResourceWithoutGetException(Error):
  """Exception for resources with no Get method."""


class BadResolverException(Error):
  """Exception to signal that a resource has no Get method."""

  def __init__(self, param):
    super(BadResolverException, self).__init__(
        'bad resolver for [{param}]'.format(param=param))


class AmbiguousAPIException(Error):
  """Exception for when two APIs try to define a resource."""

  def __init__(self, collection, base_urls):
    super(AmbiguousAPIException, self).__init__(
        'collection [{collection}] defined in multiple APIs: {apis}'.format(
            collection=collection,
            apis=repr(base_urls)))


class UserError(exceptions.Error, Error):
  """Exceptions that are caused by user input."""


class InvalidResourceException(UserError):
  """A collection-path that was given could not be parsed."""

  def __init__(self, line):
    super(InvalidResourceException, self).__init__(
        'could not parse resource: [{line}]'.format(line=line))


class WrongResourceCollectionException(UserError):
  """A command line that was given had the wrong collection."""

  def __init__(self, expected, got):
    super(WrongResourceCollectionException, self).__init__(
        'wrong collection: expected [{expected}], got [{got}]'.format(
            expected=expected, got=got))


class WrongFieldNumberException(UserError):
  """A command line that was given had too many fields."""

  def __init__(self, path, ordered_params):
    possibilities = [
        '/'.join([p.upper() for p in ordered_params[1:]]),
        '/'.join([''] + [p.upper() for p in ordered_params]),
    ]

    if len(ordered_params) > 2:
      possibilities.insert(0, ordered_params[-1].upper())

    bits = ', '.join(possibilities)

    msg = ('wrong number of fields: [{got}] does not match any of'
           ' {bits}').format(got=path, bits=bits)
    super(WrongFieldNumberException, self).__init__(msg)


class UnknownFieldException(UserError):
  """A command line that was given did not specify a field."""

  def __init__(self, collection_path, expected):
    super(UnknownFieldException, self).__init__(
        'unknown field [{expected}] in [{path}]'.format(
            expected=expected, path=collection_path))


class UnknownCollectionException(UserError):
  """A command line that was given did not specify a collection."""

  def __init__(self, line):
    super(UnknownCollectionException, self).__init__(
        'unknown collection for [{line}]'.format(line=line))


class InvalidCollectionException(UserError):
  """A command line that was given did not specify a collection."""

  def __init__(self, collection):
    super(InvalidCollectionException, self).__init__(
        'unknown collection [{collection}]'.format(collection=collection))


# TODO(user): Ensure that all the user-facing error messages help the
# user figure out what to do.


class _ResourceParser(object):
  """Class that turns command-line arguments into a cloud resource message."""

  def __init__(self, client, service, registry):
    """Create a _ResourceParser for a given API and service, and register it.

    Args:
      client: apitools_base.BaseApiClient subclass, The client that handles
          requests to the API.
      service: apitools_base.BaseApiService subclass, The service that manages
          the resource type
      registry: Registry, The registry that this parser should be added to.
    """
    try:
      self.registry = registry
      self.method_config = service.GetMethodConfig('Get')
      self.request_type = service.GetRequestType('Get')
      match = _METHOD_ID_RE.match(self.method_config.method_id)
      if not match:
        raise _ResourceWithoutGetException()
      self.collection = match.group('collection')
      self.client = client
      self.service = service
    except KeyError:
      raise _ResourceWithoutGetException()

  def ParseCollectionPath(self, collection_path, kwargs, resolve):
    """Given a command line and some keyword args, get the resource.

    Args:
      collection_path: str, The human-typed collection-path from the command
          line. Can be None to indicate all params should be taken from kwargs.
      kwargs: {str:str}, The flags available from context that can help
          parse this resource. If the fields in collection-path do not provide
          all the necessary information, kwargs will be searched for what
          remains.
      resolve: bool, If True, call the resource's .Resolve() method before
          returning, ensuring that all of the resource parameters are defined.
          If False, don't call them, under the assumption that it will be called
          later.

    Returns:
      protorpc.messages.Message, The object containing info about this resource.

    Raises:
      InvalidResourceException: If the provided collection-path is malformed.
      WrongResourceCollectionException: If the collection-path specified the
          wrong collection.
      WrongFieldNumberException: If the collection-path's path provided too many
          fields.
      UnknownFieldException: If the collection-path's path did not provide
          enough fields.
    """
    if collection_path is not None:
      fields = self._GetFieldsForKnownCollection(collection_path)
    else:
      fields = [None] * len(self.method_config.ordered_params)

    # Build up the resource params from kwargs or the fields in the
    # collection-path.
    params = {}
    request = self.request_type()
    for param, field in zip(self.method_config.ordered_params, fields):
      params[param] = field
      setattr(request, param, field)

    resource = Resource(
        self.collection, request, self.method_config.ordered_params, kwargs,
        collection_path, self)

    if resolve:
      resource.Resolve()

    return resource

  def _GetFieldsForKnownCollection(self, collection_path):
    """Get the ordered fields for the provided collection-path.

    Args:
      collection_path: str, The not-None string provided on the command line.

    Returns:
      [str], The ordered list of URL params corresponding to this parser's
      resource type.

    Raises:
      InvalidResourceException: If the provided collection-path is malformed.
      WrongResourceCollectionException: If the collection-path specified the
          wrong collection.
      WrongFieldNumberException: If the collection-path's path provided too many
          fields.
      UnknownFieldException: If the collection-path's path did not provide
          enough fields.
    """

    match = _COLLECTIONPATH_RE.match(collection_path)
    if not match:
      # Right now it is impossible for this exception to be raised: the
      # regular expression matches all strings. But we will leave it in
      # in case that ever changes.
      raise InvalidResourceException(collection_path)
    collection, path = match.groups()

    if collection and collection != self.collection:
      raise WrongResourceCollectionException(
          expected=self.collection, got=collection)

    # collection-paths that begin with a slash must have an entry for all
    # ordered params, especially including the project.
    has_project = path.startswith('/')

    # Pending b/17727265, path might contain multiple items after being split
    # on a '/'.
    fields = path.split('/')

    if has_project:
      # first token must be empty, but we already recorded the fact that the
      # next token must be the project
      fields = fields[1:]

    total_param_count = len(self.method_config.ordered_params)

    if has_project and total_param_count != len(fields):
      raise WrongFieldNumberException(
          path=path, ordered_params=self.method_config.ordered_params)

    # Check if there were too many fields provided.
    if len(fields) > total_param_count:
      raise WrongFieldNumberException(
          path=path, ordered_params=self.method_config.ordered_params)

    # If the project is not included, we can either have only one or all-but-one
    # token. So, just simple names or everything that isn't the project.
    if not has_project and len(fields) not in [1, total_param_count-1]:
      raise WrongFieldNumberException(
          path=path, ordered_params=self.method_config.ordered_params)

    num_missing = total_param_count - len(fields)
    # pad the beginning with Nones so we don't have to count backwards.
    fields = [None] * num_missing + fields

    # Did the user enter a literal empty argument at any stage?
    if '' in fields:
      raise WrongFieldNumberException(
          path=path, ordered_params=self.method_config.ordered_params)

    return fields

  def __str__(self):
    path_str = ''
    for param in self.method_config.ordered_params:
      path_str = '[{path}]/{param}'.format(path=path_str, param=param)
    return '[{collection}::]{path}'.format(
        collection=self.collection, path=path_str)


class Resource(object):
  """Information about a Cloud resource."""

  def __init__(self, collection, request, ordered_params, resolvers,
               collection_path, parser):
    """Create a Resource object that may be partially resolved.

    To allow resolving of unknown params to happen after parse-time, the
    param resolution code is in this class rather than the _ResourceParser
    class.

    Args:
      collection: str, The collection name for this resource.
      request: protorpc.messages.Message (not imported) subclass, An instance
          of a request that can be used to fetch the actual entity in the
          collection.
      ordered_params: [str], The list of parameters that define this resource.
      resolvers: {str:(str or func()->str)}, The resolution functions that can
          be used to fill in values that were not specified in the command line.
      collection_path: str, The original command-line argument used to create
          this Resource.
      parser: _ResourceParser, The parser used to create this Resource.
    """
    self.__collection = collection
    self.__request = request
    self.__name = None
    self.__self_link = None
    self.__ordered_params = ordered_params
    self.__resolvers = resolvers
    self.__collection_path = collection_path
    self.__parser = parser
    for param in ordered_params:
      setattr(self, param, getattr(request, param))

  def Collection(self):
    return self.__collection

  def Name(self):
    self.Resolve()
    return self.__name

  def SelfLink(self):
    self.Resolve()
    return self.__self_link

  def WeakSelfLink(self):
    """Returns a self link containing '*'s for unset parameters."""
    self.WeakResolve()
    return self.__self_link

  def Request(self):
    return self.__request

  def Resolve(self):
    """Resolve unknown parameters for this resource.

    Raises:
      UnknownFieldException: If, after resolving, one of the fields is still
          unknown.
    """
    self.WeakResolve()
    for param in self.__ordered_params:
      if not getattr(self, param, None):
        raise UnknownFieldException(self.__collection_path, param)

  def WeakResolve(self):
    """Attempts to resolve unknown parameters for this resource.

       Unknown parameters are left as None.
    """
    for param in self.__ordered_params:
      if getattr(self, param, None):
        continue

      def ResolveParam(value):
        # This is intended to close over param
        # pylint:disable=cell-var-from-loop
        setattr(self, param, value)
        setattr(self.__request, param, value)
        # pylint:enable=cell-var-from-loop

      # First try the resolvers given to this resource explicitly.
      resolver = self.__resolvers.get(param)
      if resolver:
        if callable(resolver):
          ResolveParam(resolver())
        else:
          ResolveParam(resolver)
        continue

      # Then try the registered defaults.
      api, collection = self.__collection.split('.', 1)
      try:
        value = self.__parser.registry.GetParamDefault(api, collection, param)
        ResolveParam(value)
      except properties.RequiredPropertyError:
        # If property lookup fails, that's ok.  Just don't resolve anything.
        pass

    effective_params = dict(
        [(k, getattr(self, k) or '*') for k in self.__ordered_params])

    self.__self_link = '%s%s' % (
        self.__parser.client.url,
        util.ExpandRelativePath(self.__parser.method_config, effective_params))

    if (self.Collection().startswith('compute.') or
        self.Collection().startswith('resourceviews.')):
      # TODO(user): Unquote URLs for compute and resourceviews,
      # pending b/15425944.
      self.__self_link = urllib.unquote(self.__self_link)

    if self.__ordered_params:
      # The last param is defined to be the resource's "name", and is the only
      # part of the resource that cannot be inferred by a resolver or other
      # context, and MUST be provided in the argument.
      self.__name = getattr(self, self.__ordered_params[-1])

  def __str__(self):
    return self.SelfLink()
    # TODO(user): Possibly change what is returned, here.
    # path = '/'.join([getattr(self, param) for param in self.__ordered_params])
    # return '{collection}::{path}'.format(
    #     collection=self.__collection, path=path)


def _CopyNestedDictSpine(maybe_dictionary):
  if type(maybe_dictionary) is types.DictType:
    result = {}
    for key, val in maybe_dictionary.iteritems():
      result[key] = _CopyNestedDictSpine(val)
    return result
  else:
    return maybe_dictionary


class Registry(object):
  """Keep a list of all the resource collections and their parsing functions.

  Attributes:
    parsers_by_collection: {str:_ResourceParser}, All the resource parsers
        indexed by their collection.
    parsers_by_url: Deeply-nested dict. The first key is the API's URL root,
        and each key after that is one of the remaining tokens which can be
        either a constant or a parameter name. At the end, a key of None
        indicates the value is a _ResourceParser.
    default_param_funcs: Triply-nested dict. The first key is the param name,
        the second is the api name, and the third is the collection name. The
        value is a function that can be called to find values for params that
        aren't specified already. If the collection key is None, it matches
        all collections.
  """

  def __init__(self, parsers_by_collection=None,
               parsers_by_url=None, default_param_funcs=None):
    self.parsers_by_collection = parsers_by_collection or {}
    self.parsers_by_url = parsers_by_url or {}
    self.default_param_funcs = default_param_funcs or {}

  def _Clone(self):
    return Registry(
        parsers_by_collection=_CopyNestedDictSpine(self.parsers_by_collection),
        parsers_by_url=_CopyNestedDictSpine(self.parsers_by_url),
        default_param_funcs=_CopyNestedDictSpine(self.default_param_funcs))

  def RegisterAPI(self, api, urls_only=False):
    """Register a generated API with this registry.

    Args:
      api: apitools_base.BaseApiClient, The client for a Google Cloud API.
      urls_only: bool, True if this API should only be used to interpret URLs,
          and not to interpret collection-paths.
    """

    for potential_service in api.__dict__.itervalues():
      if not isinstance(potential_service, apitools_base.BaseApiService):
        continue
      try:
        self._RegisterService(api, potential_service, urls_only)
      except _ResourceWithoutGetException:
        pass

  def _RegisterService(self, api, service, urls_only):
    """Register one service for an API with this registry.

    Args:
      api: apitools_base.BaseApiClient, The client for a Google Cloud API.
      service: apitools_base.BaseApiService, the service to be registered.
      urls_only: bool, True if this API should only be used to interpret URLs,
          and not to interpret collection-paths.

    Raises:
      AmbiguousAPIException: If the API defines a collection that has already
          been added.
    """
    parser = _ResourceParser(api, service, self)

    if not urls_only:
      if parser.collection in self.parsers_by_collection:
        urls = [api.url,
                self.parsers_by_collection[parser.collection].client.url]
        raise AmbiguousAPIException(parser.collection, urls)
      self.parsers_by_collection[parser.collection] = parser
    method_config = service.GetMethodConfig('Get')

    # so we don't confuse the splitting
    endpoint = api.url
    endpoint = endpoint.lstrip('https://')
    endpoint = endpoint.lstrip('http://')
    endpoint = endpoint.rstrip('/')

    tokens = endpoint.split('/') + method_config.relative_path.split('/')
    # Build up a search tree to match URLs against URL templates.
    # The tree will branch at each URL segment, where the first segment
    # is the API's base url, and each subsequent segment is a token in
    # the instance's get method's relative path. At the leaf, a key of
    # None indicates that the URL can finish here, and provides the parser
    # for this resource.
    cur_level = self.parsers_by_url
    while tokens:
      token = tokens.pop(0)
      if token not in cur_level:
        cur_level[token] = {}
      cur_level = cur_level[token]
    cur_level[None] = parser

  def _SwitchAPI(self, api):
    """Replace the registration of one version of an API with another.

    This method will remove references to the previous version of the provided
    API from self.parsers_by_collection, but leave self.parsers_by_url intact.

    Args:
      api: apitools_base.BaseApiClient, The client for a Google Cloud API.
    """
    # Clear out the old collections.
    for collection, parser in self.parsers_by_collection.items():
      if parser.client._PACKAGE == api._PACKAGE:  # pylint:disable=protected-access
        del self.parsers_by_collection[collection]
    # TODO(user): Maybe remove the url parsers as well?

    self.RegisterAPI(api)

  def CloneAndSwitchAPIs(self, *apis):
    reg = self._Clone()
    for api in apis:
      reg._SwitchAPI(api)  # pylint:disable=protected-access
    return reg

  def SetParamDefault(self, api, collection, param, resolver):
    """Provide a function that will be used to fill in missing values.

    Args:
      api: str, The name of the API that func will apply to.
      collection: str, The name of the collection that func will apploy to. Can
          be None to indicate all collections within the API.
      param: str, The param that can be satisfied with func, if no value is
          provided by the path.
      resolver: str or func()->str, A function that returns a string or raises
          an exception that tells the user how to fix the problem, or the value
          itself.

    Raises:
      ValueError: If api or param is None.
    """
    if not api:
      raise ValueError('provided api cannot be None')
    if not param:
      raise ValueError('provided param cannot be None')
    if param not in self.default_param_funcs:
      self.default_param_funcs[param] = {}
    api_collection_funcs = self.default_param_funcs[param]
    if api not in api_collection_funcs:
      api_collection_funcs[api] = {}
    collection_funcs = api_collection_funcs[api]
    collection_funcs[collection] = resolver

  def GetParamDefault(self, api, collection, param):
    """Return the default value for the specified parameter.

    Args:
      api: str, The name of the API that param is part of.
      collection: str, The name of the collection to query. Can be None to
          indicate all collections within the API.
      param: str, The param to return a default for.

    Raises:
      ValueError: If api or param is None.

    Returns:
      The default value for a parameter or None if there is no default.
    """
    if not api:
      raise ValueError('provided api cannot be None')
    if not param:
      raise ValueError('provided param cannot be None')
    api_collection_funcs = self.default_param_funcs.get(param)
    if not api_collection_funcs:
      return None
    collection_funcs = api_collection_funcs.get(api)
    if not collection_funcs:
      return None
    if collection in collection_funcs:
      resolver = collection_funcs[collection]
    elif None in collection_funcs:
      resolver = collection_funcs[None]
    else:
      return None
    return resolver() if callable(resolver) else resolver

  def ParseCollectionPath(self, collection, collection_path, kwargs,
                          resolve=True):
    if collection not in self.parsers_by_collection:
      raise InvalidCollectionException(collection)
    return self.parsers_by_collection[collection].ParseCollectionPath(
        collection_path, kwargs, resolve)

  def ParseURL(self, url, collection):
    """Parse a URL into a Resource.

    This method does not yet handle "api.google.com" in place of
    "www.googleapis.com/api/version".

    Searches self.parsers_by_url to find a _ResourceParser. The parsers_by_url
    attribute is a deeply nested dictionary, where each key corresponds to
    a URL segment. The first segment is an API's base URL (eg.
    "https://www.googleapis.com/compute/v1/"), and after that it's each
    remaining token in the URL, split on '/'. Then a path down the tree is
    followed, keyed by the extracted pieces of the provided URL. If the key in
    the tree is a literal string, like "project" in .../project/{project}/...,
    the token from the URL must match exactly. If it's a parameter, like
    "{project}", then any token can match it, and that token is stored in a
    dict of params to with the associated key ("project" in this case). If there
    are no URL tokens left, and one of the keys at the current level is None,
    the None points to a _ResourceParser that can turn the collected
    params into a Resource.

    Args:
      url: str, The URL of the resource.
      collection: str, The intended collection for the parsed resource, or None
          if unconstrained.

    Returns:
      Resource, The resource indicated by the provided URL.

    Raises:
      InvalidResourceException: If the provided URL could not be turned into
          a cloud resource.
      WrongResourceCollectionException: If the provided URL points into a
          collection other than the one specified.
    """
    match = _URL_RE.match(url)
    if not match:
      raise InvalidResourceException('unknown API host: [{0}]'.format(url))
    endpoint, path = match.groups()
    endpoint = endpoint.lstrip('https://')
    endpoint = endpoint.lstrip('http://')
    endpoint = endpoint.rstrip('/')
    tokens = endpoint.split('/') + path.split('/')
    params = {}

    cur_level = self.parsers_by_url
    for token in tokens:
      if token in cur_level:
        # If the literal token is already here, follow it down.
        cur_level = cur_level[token]
      elif len(cur_level) == 1:
        # If the literal token is not here, and there is only one key, it must
        # be a parameter that will be added to the params dict.
        param = cur_level.keys()[0]
        if not param.startswith('{') or not param.endswith('}'):
          raise InvalidResourceException(url)
        # Clean up the provided value
        params[param[1:-1]] = urllib.unquote(token)
        # Keep digging down.
        cur_level = cur_level[param]
      else:
        # If the token we want isn't here, and there isn't a single parameter,
        # the URL we've been given doesn't match anything we know about.
        raise InvalidResourceException(url)
      # Note: This will break if there are multiple parameters that could be
      # specified at a given level. As far as I can tell, this never happens and
      # never should happen. But in theory it's possible so we'll keep an eye
      # out for this issue.

    # No more tokens, so look for a parser.
    if None not in cur_level:
      raise InvalidResourceException(url)
    parser = cur_level[None]
    resource = parser.ParseCollectionPath(None, params, resolve=True)

    if collection and resource.Collection() != collection:
      raise WrongResourceCollectionException(
          expected=collection, got=resource.Collection())
    return resource

  def Parse(self, line, params=None, collection=None, resolve=True):
    """Parse a Cloud resource from a command line.

    Args:
      line: str, The argument provided on the command line.
      params: {str:str}, The keyword argument context.
      collection: str, The resource's collection, or None if it should be
        inferred from the line.
      resolve: bool, If True, call the resource's .Resolve() method before
          returning, ensuring that all of the resource parameters are defined.
          If False, don't call them, under the assumption that it will be called
          later.

    Returns:
      A resource object.

    Raises:
      InvalidResourceException: If the line is invalid.
      UnknownCollectionException: If no collection is provided or can be
          inferred.
    """
    if line and (line.startswith('https://') or line.startswith('http://')):
      return self.ParseURL(line, collection)
    else:
      if not collection:
        match = _COLLECTIONPATH_RE.match(line)
        if not match:
          raise InvalidResourceException(line)
        collection, unused_path = match.groups()
      if not collection:
        raise UnknownCollectionException(line)

      return self.ParseCollectionPath(collection, line, params or {}, resolve)

  def Create(self, collection, **params):
    """Create a Resource from known collection and params.

    Args:
      collection: str, The name of the collection the resource belongs to.
      **params: {str:str}, The values for each of the resource params.

    Returns:
      Resource, The constructed resource.
    """
    return self.Parse(None, collection=collection, params=params)


# TODO(user): Deglobalize this object, force gcloud to manage it on its own.
REGISTRY = Registry()


def RegisterAPI(api, urls_only=False):
  """Register a generated API for parsing.

  Args:
    api: apitools_base.BaseApiClient, The client for a Google Cloud API.
    urls_only: bool, True if this API should only be used to interpret URLs,
        and not to interpret collection-paths.
  """
  REGISTRY.RegisterAPI(api, urls_only)


def SetParamDefault(api, collection, param, resolver):
  """Provide a function that will be used to fill in missing values.

  Args:
    api: str, The name of the API that func will apply to.
    collection: str, The name of the collection that func will apply to. Can
        be None to indicate all collections within the API.
    param: str, The param that can be satisfied with func, if no value is
        provided by the path.
    resolver: str or func()->str, A function that returns a string or raises an
        exception that tells the user how to fix the problem, or the value
        itself.
  """
  REGISTRY.SetParamDefault(api, collection, param, resolver)


def GetParamDefault(api, collection, param):
  """Return the default value for the specified parameter.

  Args:
    api: str, The name of the API that param is part of.
    collection: str, The name of the collection to query. Can be None to
        indicate all collections within the API.
    param: str, The param to return a default for.

  Raises:
    ValueError: If api or param is None.

  Returns:
    The default value for a parameter or None if there is no default.
  """
  return REGISTRY.GetParamDefault(api, collection, param)


def _ClearAPIs():
  """For testing, clear out any APIs to start with a clean slate.

  """
  global REGISTRY
  REGISTRY = Registry()


def Parse(line, params=None, collection=None, resolve=True):
  """Parse a Cloud resource from a command line.

  Args:
    line: str, The argument provided on the command line.
    params: {str:str}, The keyword argument context.
    collection: str, The resource's collection, or None if it should be
      inferred from the line.
    resolve: bool, If True, call the resource's .Resolve() method before
        returning, ensuring that all of the resource parameters are defined.
        If False, don't call them, under the assumption that it will be called
        later.

  Returns:
    A resource object.

  Raises:
    InvalidResourceException: If the line is invalid.
    UnknownCollectionException: If no collection is provided or can be inferred.
    WrongProtocolException: If the input was http:// instead of https://
  """
  return REGISTRY.Parse(
      line=line, params=params, collection=collection, resolve=resolve)


def Create(collection, **params):
  """Create a Resource from known collection and params.

  Args:
    collection: str, The name of the collection the resource belongs to.
    **params: {str:str}, The values for each of the resource params.

  Returns:
    Resource, The constructed resource.
  """
  return REGISTRY.Create(collection, **params)
