docs: def validate(url, schemes=None, tlds=None, private=None, local=None,
               credentials=None):
      '''
      Validate and normalize an URL
  
      :param str url: The URL to validate and normalize
      :return str: The normalized URL
      :raises ValidationError: when URL does not validate
      '''
      url = url.strip()
  
      private = config_for(private, 'URLS_ALLOW_PRIVATE')
      local = config_for(local, 'URLS_ALLOW_LOCAL')
      credentials = config_for(credentials, 'URLS_ALLOW_CREDENTIALS')
      schemes = config_for(schemes, 'URLS_ALLOWED_SCHEMES')
      tlds = config_for(tlds, 'URLS_ALLOWED_TLDS')
  
      match = URL_REGEX.match(url)
      if not match:
          error(url)
  
      scheme = (match.group('scheme') or '').lower()
      if scheme and scheme not in schemes:
          error(url, 'Invalid scheme {0}'.format(scheme))
  
      if not credentials and match.group('credentials'):
          error(url, 'Credentials in URL are not allowed')
  
      tld = match.group('tld')
      if tld and tld not in tlds and tld.encode('idna') not in tlds:
          error(url, 'Invalid TLD {0}'.format(tld))
  
      ip = match.group('ipv6') or match.group('ipv4')
      if ip:
          try:
              ip = IPAddress(ip)
          except AddrFormatError:
              error(url)
          if ip.is_multicast():
              error(url, '{0} is a multicast IP'.format(ip))
          elif not ip.is_loopback() and ip.is_hostmask() or ip.is_netmask():
              error(url, '{0} is a mask IP'.format(ip))
  
      if not local:
          if ip and ip.is_loopback() or match.group('localhost'):
              error(url, 'is a local URL')
  
      if not private and ip and ip.is_private():
          error(url, 'is a private URL')
  
      return url