google.py

#

Copyright (C) 2010 Google Inc.

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. Main function for the Google command line tool, GoogleCL.

#

This program provides some functionality for a number of Google services from the command line.

Example usage (omitting the initial "./google"): # Create a photo album with tags "Vermont" and name "Summer Vacation 2009" picasa create -n "Summer Vacation 2009" -t Vermont ~/photos/vacation2009/*

# Post photos to an existing album picasa post -n "Summer Vacation 2008" ~/old_photos/*.jpg

# Download another user's albums whose titles match a regular expression picasa get --user my.friend.joe --name ".computer." ~/photos/joes_computer

# Delete some posts you accidentally put up blogger delete -n "Silly post, number [0-9]*"

# Post your latest film endeavor to YouTube youtube post --category Film --tag "Jane Austen, zombies" ~/final_project.mp4

Some terminology in use: service: The Google service being accessed (e.g. Picasa, Blogger, YouTube). task: What the client wants done by the service (e.g. post, get, delete).

from __future__ import with_statement

__author__ = 'tom.h.miller@gmail.com (Tom Miller)'
import glob
import logging
import optparse
import os
import sys
import traceback
import webbrowser
import googlecl
import googlecl.authentication
import googlecl.config
try: # Fails if Discovery stuff is unavailable
 from googlecl.discovery import DiscoveryManager
 apis = True
except:
 apis = False
#

Renamed here to reduce verbosity in other sections

safe_encode = googlecl.safe_encode
safe_decode = googlecl.safe_decode

VERSION = '0.9.13'

AVAILABLE_SERVICES = ['help', 'picasa', 'blogger', 'youtube', 'docs',
                      'contacts', 'calendar', 'finance']
LOG = logging.getLogger(googlecl.LOGGER_NAME)

discovery = None
AVAILABLE_APIS = None
#
class NonFatalOptionParser(optparse.OptionParser):
#
  def error(self, message):
    self.error_message = message
#
  def bailout(self, message):
    print self.usage, "\n\n", "FATAL ERROR:\n", message, "\n"
    exit(1)
#
  def bailout_if_necessary(self):
    if hasattr(self, 'error_message'):
      print self.usage, "\n\n", "FATAL ERROR:\n", self.error_message, "\n"
      exit(1)
#

Attempts to sanely parse the command line, considering both the legacy gdata services and the new discovery services which aren't known at runtime.

def parse_command_line(parser, original_args):
  (options, args) = parser.parse_args(original_args)
#

If the discovery API is available and we're using it, then it needs to do its own argument parsing.

#

If there's no discovery API, then any argument errors are fatal.

  if not apis:
    if len(args) > 0 and args[0] not in AVAILABLE_SERVICES:
      parser.bailout("Discovery API unavailable.  Services limited to:" +
                     ", ".join(AVAILABLE_SERVICES) + "\nUnknown service: " + args[0])

    parser.bailout_if_necessary()
#

Even if there is a discovery API, if you asked for a non-discovery service, argument errors are fatal.

  if len(args) > 0:
    if args[0] in AVAILABLE_SERVICES:
      parser.bailout_if_necessary()
    else:
      args = original_args

  return (options, args)
#

Base error for this module.

class Error():
#
  pass
#

Set a (presumably valid) OAuth token for the client to use.

def authenticate(auth_manager, options, config, section_header):
#

Args: auth_manager: Object handling the authentication process. options: Parsed command line options. config: Configuration file parser. section_header: Section header to look in for the configuration file.

Returns: True if authenticated, False otherwise.

Only try to set the access token if we're not forced to authenticate. XXX: logic in here is iffy. Don't bother checking access token if it's not set

  if not options.force_auth:
    set_token = auth_manager.set_access_token()
    if set_token:
      LOG.debug('Successfully set token')
      skip_auth = (options.skip_auth or
                   config.lazy_get(section_header, 'skip_auth',
                                   default=False, option_type=bool))
    else:
      LOG.debug('Failed to set token!')
      skip_auth = False
  else:
    LOG.debug('Forcing retrieval of new token')
    skip_auth = False

  if options.force_auth or not skip_auth:
    LOG.debug('Checking access token...')
    valid_token = auth_manager.check_access_token()
    if not valid_token:
      display_name = auth_manager.get_display_name(options.hostid)
      browser_str = config.lazy_get(section_header, 'auth_browser',
                                    default=None)
      if browser_str:
        if browser_str.lower() == 'disabled' or browser_str.lower() == 'none':
          browser = None
        else:
          try:
            browser = webbrowser.get(browser_str)
          except webbrowser.Error, err:
            LOG.warn(safe_encode(u'Could not get browser "%s": %s' %
                                 (browser_str, err)))
            browser = None
      else:
        try:
          browser = webbrowser.get()
        except webbrowser.Error, err:
          LOG.warn(safe_encode(u'Could not get default browser: %s' % err))
          browser = None

      valid_token = auth_manager.retrieve_access_token(display_name, browser)
    if valid_token:
      LOG.debug('Retrieved valid access token')
      config.set_missing_default(section_header, 'skip_auth', True)
      return True
    else:
      LOG.debug('Could not retrieve valid access token')
      return False
  else:
#

Already set an access token and we're not being forced to authenticate

    return True
#

I don't know if this and shlex.split() can replace expand_as_command_line because of the behavior of globbing characters that are in "" Expands arguments list.

def expand_args(args, on_linesep, on_glob, on_homepath):
#

Args: on_linesep: Set True to split on occurrences of os.linesep. This is reasonably safe -- line separators appear to be escaped when given to Python on the command line. on_glob: Set True to glob expressions. May not be safe! For example, if user passes in "A*" (including the quotes) the * should NOT be expanded. Recommended only if sys.platform == 'win32' on_homepath: Set True to replace a leading ~/ with the user's home directory. May not be safe! Same situation as on_glob, described above.

Returns: List of arguments that have been expanded

  new_args = []
  for arg in args:
    temp_arg_list = None
    if on_linesep:
      temp_arg_list = arg.split(os.linesep)
    if on_homepath:
      if temp_arg_list:
        tmp = []
        for sub_arg in temp_arg_list:
          tmp.append(os.path.expanduser(sub_arg))
        temp_arg_list = tmp
      else:
        arg = os.path.expanduser(arg)
#

Globbing needs to happen last, otherwise it wont be able to find any files.

    if on_glob:
      if temp_arg_list:
        tmp = []
        for sub_arg in temp_arg_list:
          tmp.extend(glob.glob(sub_arg))
        temp_arg_list = tmp
      else:
        temp_arg_list = glob.glob(arg)

    if temp_arg_list:
      new_args.extend(temp_arg_list)
    else:
      new_args.append(arg)
  return new_args
#

Expand a string as if it was entered at the command line.

def expand_as_command_line(command_string):
#

Mimics the shell expansion of '~', file globbing, and quotation marks. For example, 'picasa post -a "My album" ~/photos/*.png' will return ['picasa', 'post', '-a', 'My album', '$HOME/photos/myphoto1.png', etc.] It will not treat apostrophes specially, or handle environment variables.

Keyword arguments: command_string: String to be expanded.

Returns: A list of strings that (mostly) matches sys.argv as if command_string was entered on the command line.

  if not command_string:
    return []
#

Sub in the home path.

  home_path = os.path.expanduser('~/')
#

We may get a tilde that needs expansion in the middle of the string... (Replace with the space to make sure we don't screw up /really/~/weird/path

  command_string = command_string.replace(' ~/', ' ' + home_path)
#

Or, if we're given options.src to expand, it could be the first few characters.

  if command_string.startswith('~/'):
    command_string = home_path + command_string[2:]
  token_list = command_string.split()
  args_list = []
  while token_list:
    tmp = token_list.pop(0)
    start_of_quote = tmp[0] == '"' or tmp[0] == "'"
    start_of_dict = tmp[0] == '{'
    start_of_list = tmp[0] == '['
#

A test to see if the end of a quoted argument has been reached

    end_quote = lambda s: s[-1] == s[0] and len(s) > 1 and s[-2] != '\\'
    end_dict = lambda s: s[-1] == '}' and len(s) > 1 and s[-2] != '\\'
    end_list = lambda s: s[-1] == ']' and len(s) > 1 and s[-2] != '\\'
#

Don't need to worry about nesting because of natural syntax: ["foo", ["bar"], "baz"] -> stupid to have '["bar"] ,'

    while (start_of_quote and not end_quote(tmp)) or (start_of_dict 
      and not end_dict(tmp)) or (start_of_list and not end_list(tmp)):
      if token_list:
        tmp += ' ' + token_list.pop(0)
      else:
        if start_of_quote:
          raise Error('Encountered end of string without finding matching "')
        elif start_of_dict:
          raise Error('Encountered end of string without finding matching }')
        else:
          raise Error('Encountered end of string without finding matching ]')
    if start_of_quote:
#

Add the resulting arg, stripping the " off

      args_list.append(tmp[1:-1])
    else:
#

Grab all the tokens in a row that end with unescaped \

      while tmp[-1] == '\\' and len(tmp) > 1 and tmp[-2] != '\\':
        if token_list:
          tmp = tmp[:-1] + ' ' + token_list.pop(0)
        else:
          raise Error('Encountered end of string ending in \\')

      expanded_args = glob.glob(tmp)
      if expanded_args:
        args_list.extend(expanded_args)
      else:
        args_list.append(tmp)
  return args_list
#

Fill out required options via config file and command line prompts.

def fill_out_options(args, service_header, task, options, config):
#

If there are any required fields missing for a task, fill them in. This is attempted by checking the following sources, in order: 1) The service_header section of the preferences file. 2) The arguments list given to this function. 3) Prompting the user.

Note that 'user' and 'hostid' are special options -- they are always required, and they will skip step (2) when checking sources as mentioned above.

Keyword arguments: args: list Arguments that may be options. service_header: str Name of the section in the config file for the active service. task: Requirements of the task (see class googlecl.service.Task). options: Contains attributes that have been specified already, typically through options on the command line (see setup_parser()). config: Configuration file parser.

Returns: Nothing, though options may be modified to hold the required fields.

Retrieve value from config file or user prompt.

  def _retrieve_value(attr, service_header):
#
    value = config.lazy_get(service_header, attr)
    if value:
      return value
    else:
      return raw_input('Please specify ' + attr + ': ')

  if options.user is None:
    options.user = _retrieve_value('user', service_header)
  if options.hostid is None:
    options.hostid = _retrieve_value('hostid', service_header)
  missing_reqs = task.get_outstanding_requirements(options)
  LOG.debug('missing_reqs: ' + str(missing_reqs))

  for attr in missing_reqs:
    value = config.lazy_get(service_header, attr)
    if not value:
      if args:
        value = args.pop(0)
      else:
        value = raw_input('Please specify ' + attr + ': ')
    setattr(options, attr, value)
#

Expand those options that might be a filename in disguise.

  max_file_size = 500000    # Value picked arbitrarily - no idea what the max
#

size in bytes of a summary is.

  if options.summary and os.path.exists(os.path.expanduser(options.summary)):
    with open(options.summary, 'r') as summary_file:
      options.summary = summary_file.read(max_file_size)
  if options.devkey and os.path.exists(os.path.expanduser(options.devkey)):
    with open(options.devkey, 'r') as key_file:
      options.devkey = key_file.read(max_file_size).strip()
#
def get_task_help(service, tasks):
  help = 'Available tasks for service ' + service + \
         ': ' + str(tasks.keys())[1:-1] + '\n'
  for task_name in tasks.keys():
    help += ' ' + task_name + ': ' + tasks[task_name].description + '\n'
    help += '  ' + tasks[task_name].usage + '\n\n'

  return help
#

Imports a module/package.

def import_at_runtime(module):
#

Args: module: Module/package to import

Returns: Module or package.

Proper use of this function seems sketchy. Docs claim that only globals() is used, but it seems that if fromlist evaluates to False or is not passed in, nothing is actually imported. Needs to be a list type (at least to play nice with Jython?) and contain a string in the list.

  return __import__(module, globals(), fromlist=['0'])
#

Import vital information about a service.

def import_service(service, config_file_path):
#

The goal of this function is to allow expansion to other "service" classes in the future. In the same way that the v2 and v3 API python library uses a module called "client", googlecl will do the same.

Keyword arguments: service: Name of the service to import e.g. 'picasa', 'youtube' config_file_path: Path to config file.

Returns: Tuple of service_class, tasks, section header and config, where service_class is the class to instantiate for the service tasks is the dictionary mapping names to Task objects section_header is the name of the section in the config file that contains options specific to the service. config is a configuration file parser.

  LOG.debug('Your pythonpath: ' + str(os.environ.get('PYTHONPATH')))
  try:
    package = import_at_runtime('googlecl.' + service)
  except ImportError, err:
    LOG.error(err.args[0])
    LOG.error('Did you specify the service correctly? Must be one of ' +
              str(AVAILABLE_SERVICES)[1:-1])
    return (None, None, None, None)

  config = googlecl.config.load_configuration(config_file_path)
  force_gdata_v1 = config.lazy_get(package.SECTION_HEADER,
                                   'force_gdata_v1',
                                   default=False,
                                   option_type=bool)

  if force_gdata_v1:
    service_module = import_at_runtime('googlecl.' + service + '.service')
  else:
    try:
      service_module = import_at_runtime('googlecl.' + service + '.client')
    except ImportError:
      service_module = import_at_runtime('googlecl.' + service + '.service')
  return (service_module.SERVICE_CLASS,
          package.TASKS,
          package.SECTION_HEADER,
          config)
#

Insert stdin buffer into options or args.

def insert_stdin(options, args, single_arg_symbol='_', split_arg_symbol='__'):
#

Args: options: Object containing values for options. Will only be searched for single_arg_symbol. args: List of arguments. single_arg_symbol: Symbol indicating that stdin should be inserted as a single argument. split_arg_symbol: Symbol indicating that stdin should be inserted as a list of arguments. This symbol should only appear in args.

Returns: Nothing, but args and options are modified in place.

  try:
    i = args.index(single_arg_symbol)
  except ValueError:
    pass
  else:
    args[i] = sys.stdin.read()
    return

  try:
    i = args.index(split_arg_symbol)
  except ValueError:
    pass
  else:
    args[i:i+1] = expand_as_command_line(sys.stdin.read())
    return

  if single_arg_symbol in options.__dict__.values():
    for key, value in options.__dict__.iteritems():
      if value == single_arg_symbol:
        setattr(options, key, sys.stdin.read())
        break
#

Print help messages to the screen.

def print_help(service=None, tasks=None):
#

Keyword arguments: service: Service to get help on. (Default None, prints general help) tasks: Dictionary of tasks that can be done by the given service. (Default None)

  if not service:
    print 'Welcome to the Google CL tool!'
    print '  Commands are broken into several parts: '
    print '    service, task, options, and arguments.'
    print '  For example, in the command'
    print '      "> picasa post --title "My Cat Photos" photos/cats/*"'
    print '  the service is "picasa", the task is "post", the single'
    print '  option is a title of "My Cat Photos", and the argument is the '
    print '  path to the photos.'
    print ''
    print '  The available services are '
    print str(AVAILABLE_SERVICES)[1:-1]
    if apis:
     print '  and via Discovery:'
     print str(AVAILABLE_APIS)[1:-1]
     print '  Enter "> help more" for more detailed help.'
    print '  Enter "> help <service>" for more information on a service.'
    print '  Or, just "quit" to quit.'
  else:
    print get_task_help(service, tasks)
#

Prints additional help

def print_more_help():
#
  print """  Additional information:
    (For Discovery APIs)
  Enter "> help <service> <fields>" for additional info
  You may also add a '-v' or '--verbose' tag for even more detailed information.

  Enter "> refresh apis" to update the Discovery APIs list
  This will allow you to use the latest APIs by default.
  Older APIs may be used by calling '> <service> <version> <etc>'

  You may add more APIs by providing the path to their Discovery document in the
  config file, under the parameter 'local_apis'

  Global config values may be viewed and edited with "> edit config" """
#

Run an interactive shell for the google commands.

def run_interactive(parser):
#

Keyword arguments: parser: Object capable of parsing a list of arguments via parse_args.

  history_file = googlecl.get_data_path(googlecl.HISTORY_FILENAME,
                                        create_missing_dir=True)
  try:
    import readline
    try:
      readline.read_history_file(history_file)
    except EnvironmentError:
      LOG.debug('Could not read history file.')
  except ImportError:
    LOG.debug('Could not import readline module.')

  while True:
    try:
      command_string = raw_input('> ')
      if command_string.startswith('python '):
        LOG.info('HINT: No need to include "python" in interactive mode')
        command_string = command_string.replace('python ', '', 1)
      if command_string.startswith('google '):
        LOG.info('HINT: No need to include "google" in interactive mode')
        command_string = command_string.replace('google ', '', 1)
      if not command_string:
        continue
      elif command_string == '?':
        print_help()
      elif command_string == 'quit':
        break
      else:
        try:
          args_list = expand_as_command_line(command_string)
        except Error, err:
          LOG.error(err)
          continue

        (options, args) = parse_command_line(parser, args_list)
        run_once(options, args)

    except (KeyboardInterrupt, ValueError), err:
#

It would be nice if we could simply unregister or reset the signal handler defined in the initial if name block. Windows will raise a KeyboardInterrupt, GNU/Linux seems to also potentially raise a ValueError about I/O operation.

      if isinstance(err, ValueError) and \
         str(err).find('I/O operation on closed file') == -1:
        print "Error: " + str(err)
        LOG.error(err)
        raise err
      print ''
      print 'Quit via keyboard interrupt'
      break
    except EOFError:
      print ''
      break
    except SystemExit:
#

optparse.OptParser prints the usage statement and calls sys.exit when there are any option errors. Printing usage good, SystemExit bad. So catch it and do nothing.

      pass
    except BaseException:
      traceback.print_exc()
  if 'readline' in sys.modules:
    readline.write_history_file(history_file)
#

Run one command.

def run_once(options, args):
#

Keyword arguments: options: Options instance as built and returned by optparse. args: Arguments to GoogleCL, also as returned by optparse.

  global discovery
#

If we haven't gotten the list of discovery APIs yet, and they're asking for a discovery API, figure out their email address and then get a list of APIs.

  if apis and not discovery:
    if (args[0] not in AVAILABLE_SERVICES) or \
       (args[0] == 'help' and len(args) == 1) or \
       (args[0] == 'help' and len(args)>1 and \
        args[1] not in AVAILABLE_SERVICES):
#

Is there a better approach than using the calendar API to get the email address?

      service_class, tasks, section_header, config = import_service('calendar',
                                                                    None)
      email = config.lazy_get(section_header, 'user')
      discovery = DiscoveryManager(email)
      global AVAILABLE_APIS
      AVAILABLE_APIS = discovery.apis_list()

  init_args = args[:]
  try:
    service = args.pop(0)
    task_name = args.pop(0)
  except IndexError:
    if service == 'help':
      print_help()
    else:
      LOG.error('Must specify at least a service and a task!')
    return

  if apis and service == 'refresh' and task_name == 'apis':
    discovery.docManager.load(force=True)
    AVAILABLE_APIS = discovery.apis_list()
    return

  if apis and service == 'edit' and task_name == 'config':
    import subprocess
    subprocess.call((discovery.dataManager.editor,
                   googlecl.config.get_config_path()))
    return

  if service == 'help' and task_name == 'more':
    print_more_help()
    return
#

Detects if GData is not provided a version number or the path is too long

  conflict = (task_name[0] == 'v' and task_name[1].isdigit()) or (service == 'help' and args)
#

Prioritizes using existing GData APIs over Discovery. May have to change if/when those are brought over to Discovery...

  if service == 'help':
    if task_name in AVAILABLE_SERVICES and not conflict:
      service_class, tasks, section_header, config = import_service(task_name,
                                                                options.config)
      if tasks:
        print_help(task_name, tasks)
        return
    else:
      if apis and task_name in AVAILABLE_APIS:
        discovery.run(init_args)
      elif not apis:
        LOG.error('Did not recognize service.')
        LOG.error('If you wanted a Discovery service, make sure you have')
        LOG.error('google-api-python-client installed.')
      else:
        LOG.error('Did not recognize service.')
      return
  elif service in AVAILABLE_SERVICES and not conflict:
      service_class, tasks, section_header, config = import_service(service,
                                                                options.config)
  else:
    if apis and service in AVAILABLE_APIS:
      discovery.run(init_args)
    elif not apis:
      LOG.error('Did not recognize service.')
      LOG.error('If you wanted a Discovery service, make sure you have')
      LOG.error('google-api-python-client installed.')
    else:
      LOG.error('Did not recognize service.')
    return
  if not service_class:
    return
  client = service_class(config)
#

Activate debugging output from HTTP requests. "service" clients only! "client" versions need to set self.http_client.debug in their own init

  client.debug = config.lazy_get(section_header,
                                 'debug',
                                 default=options.debug,
                                 option_type=bool)
#

XXX: Not the best place for this.

  if hasattr(client, 'http_client'):
    client.http_client.debug = client.debug
  try:
    task = tasks[task_name]
    task.name = task_name
  except KeyError:
    LOG.error('Did not recognize task, please use one of ' + \
              str(tasks.keys()))
    return

  if 'devkey' in task.required:
#

If a devkey is required, and there is none specified via an option BEFORE fill_out_options, insert the key from file or the key given to GoogleCL. You can get your own key at http://code.google.com/apis/youtube/dashboard

    if not options.devkey:
      options.devkey = googlecl.read_devkey() or 'AI39si4d9dBo0dX7TnGyfQ68bNiKfEeO7wORCfY3HAgSStFboTgTgAi9nQwJMfMSizdGIs35W9wVGkygEw8ei3_fWGIiGSiqnQ'
#

fill_out_options will read the key from file if necessary, but will not set it since it will always get a non-empty value beforehand.

  fill_out_options(args, section_header, task, options, config)
  client.email = options.user

  if options.blog:
    config.set_missing_default(section_header, 'blog', options.blog)
  if options.devkey:
    client.developer_key = options.devkey
#

This may save an invalid dev key -- it's up to the user to specify a valid dev key eventually. TODO: It would be nice to make this more efficient.

    googlecl.write_devkey(options.devkey)
#

Unicode-ize options and args

  for attr_name in dir(options):
    attr = getattr(options, attr_name)
    if not attr_name.startswith('_') and isinstance(attr, str):
      setattr(options, attr_name, safe_decode(attr, googlecl.TERMINAL_ENCODING))
  if args:
    args = [safe_decode(string, googlecl.TERMINAL_ENCODING) for string in args]
#

Expand options.src. The goal is to expand things like --src=~/Photos/album1/* (which does not normally happen) XXX: This ought to be in fill_out_options(), along with unicode-ize above.

  if options.src:
    expanded_args = glob.glob(options.src)
    if expanded_args:
      options.src = expanded_args
    else:
      options.src = [options.src]
  else:
    options.src = []
#

Take a gander at the options filled in.

  if LOG.getEffectiveLevel() == logging.DEBUG:
    import inspect
    for attr_name in dir(options):
      if not attr_name.startswith('_'):
        attr = getattr(options, attr_name)
        if attr is not None and not inspect.ismethod(attr):
          LOG.debug(safe_encode('Option ' + attr_name + ': ' + unicode(attr)))
  LOG.debug(safe_encode('args: ' + unicode(args)))

  auth_manager = googlecl.authentication.AuthenticationManager(service, client)
  authenticated = authenticate(auth_manager, options, config, section_header)

  if not authenticated:
    LOG.debug('Authentication failed, exiting run_once')
    return -1
#

If we've authenticated, save the config values we've been setting. And remember the email address that worked!

  config.set_missing_default(section_header, 'user', client.email)
  config.write_out_parser()
  run_error = None
  try:
    task.run(client, options, args)
  except AttributeError, run_error:
    err_str = safe_decode(run_error)
    if err_str.startswith("'OAuth"):
      LOG.info('OAuth error.  Try re-running with --force-auth.')
    else:
      raise run_error
  if run_error and LOG.isEnabledFor(logging.DEBUG):
#

XXX: This will probably not work if googlecl gets threaded (unlikely)

    type, value, traceback_obj = sys.exc_info()
    LOG.debug(''.join(traceback.format_exception(type, value, traceback_obj)))
    return -1
  return 0
#

Setup the global (root, basic) configuration for logging.

def setup_logger(options):
#
  msg_format = '%(message)s'
  if options.debug:
    level = logging.DEBUG
    msg_format = '%(levelname)s:%(name)s:%(message)s'
  elif options.verbose:
    level = logging.DEBUG
  elif options.quiet:
    level = logging.ERROR
  else:
    level = logging.INFO
#

basicConfig does nothing if it's been called before (e.g. in run_interactive loop)

  logging.basicConfig(level=level, format=msg_format)
#

Redundant for single-runs, but necessary for run_interactive.

  LOG.setLevel(level)
#

XXX: Inappropriate location (style-wise).

  if options.debug or options.verbose:
    import gdata
    LOG.debug('Gdata will be imported from ' + gdata.__file__)
#

Set up the parser.

def setup_parser(loading_usage):
#

Returns: optparse.OptionParser with options configured.

  available_services = '[' + '|'.join(AVAILABLE_SERVICES) + ']'
#

NOTE: Usage string formatted to work with help2man. After changing it, please run: 'help2man -N -n "command-line access to (some) Google services" \ -i ../man/examples.help2man ./google > google.1' then 'man ./google.1' and make sure the generated manpage still looks reasonable. Then save it to man/google.1

  usage = ('Usage: ' + sys.argv[0] + ' ' + available_services +
           ' TASK [options]\n'
           '\n'
           'This program provides command-line access to\n'
           '(some) google services via their gdata APIs.\n'
           'Called without a service name, it starts an interactive session.\n'
           '\n'
           'NOTE: GoogleCL will interpret arguments as required options in the\n'
           'order they appear in the descriptions below, excluding options\n'
           'set in the configuration file and non-primary terms in '
           'parenthesized\n'
           'OR groups. For example:\n'
           '\n'
           '\t$ google picasa get my_album .\n'
           'is interpreted as "google picasa get --title=my_album --dest=.\n'
           '\n'
           '\t$ google contacts list john\n'
           'is interpreted as "$ google contacts list '
           '--fields=<config file def> --title=john --delimiter=,"\n'
           '(only true if you have not removed the default definition in the '
           'config file!)\n'
           '\n'
           '\t$ google docs get my_doc .\n'
           'is interpreted as "$ google docs get --title=my_doc --dest=.\n'
           '(folder is NOT set, since the title option is satisfied first.)\n\n'
           )

  if loading_usage:
    for service in AVAILABLE_SERVICES:
      if service == 'help':
        continue
      service_package = import_at_runtime('googlecl.' + service)
      usage += get_task_help(service, service_package.TASKS) + '\n'

  parser = NonFatalOptionParser(usage=usage, version=sys.argv[0] + VERSION)
  parser.add_option('--access', dest='access',
                    help='Specify access/visibility level of an upload')
  parser.add_option('--blog', dest='blog',
                    help='Blogger only - specify a blog other than your' +
                    ' primary.')
  parser.add_option('--cal', dest='cal',
                    help='Calendar only - specify a calendar other than your' +
                    ' primary.')
  parser.add_option('-c', '--category', dest='category',
                    help='YouTube only - specify video categories' +
                    ' as a comma-separated list, e.g. "Film, Travel"')
  parser.add_option('--commission', dest='commission',
                    help=("Finance only - specify commission for transaction"))
  parser.add_option('--config', dest='config',
                    help='Specify location of config file.')
  parser.add_option('--currency', dest='currency',
                    help=("Finance only - specify currency for portfolio"))
  parser.add_option('--devtags', dest='devtags',
                    help='YouTube only - specify developer tags' +
                    ' as a comma-separated list.')
  parser.add_option('--devkey', dest='devkey',
                    help='YouTube only - specify a developer key')
  parser.add_option('-d', '--date', dest='date',
                    help=('Calendar only - date of the event to add/look for. '
                          'Can also specify a range with a comma.\n'
                          'Picasa only - sets the date of the album\n'
                          'Finance only - transaction creation date'))
  parser.add_option('--debug', dest='debug',
                    action='store_true',
                    help=('Enable all debugging output, including HTTP data'))
  parser.add_option('--delimiter', dest='delimiter', default=',',
                    help='Specify a delimiter for the output of the list task.')
  parser.add_option('--dest', dest='dest',
                    help=('Destination. Typically, where to save data being'
                          ' downloaded.'))
  parser.add_option('--draft', dest='access',
                    action='store_const', const='draft',
                    help=('Blogger only - post as a draft. Shorthand for '
                          '--access=draft'))
  parser.add_option('--editor', dest='editor',
                    help='Docs only - editor to use on a file.')
  parser.add_option('--fields', dest='fields',
                    help='Fields to list with list task.')
  parser.add_option('-f', '--folder', dest='folder',
                    help='Docs only - specify folder(s) to upload to '+
                    '/ search in.')
  parser.add_option('--force-auth', dest='force_auth',
                    action='store_true',
                    help='Force validation step for re-used access tokens' +
                         ' (Overrides --skip-auth).')
  parser.add_option('--format', dest='format',
                    help='Docs only - format to download documents as.')
  parser.add_option('--hostid', dest='hostid',
                    help='Label the machine being used.')
  parser.add_option('-n', '--title', dest='title',
                    help='Title of the item')
  parser.add_option('--no-convert', dest='convert',
                    action='store_false', default=True,
                    help='Google Apps Premier only - do not convert the file' +
                    ' on upload. (Else converts to native Google Docs format)')
  parser.add_option('--notes', dest='notes',
                    help=("Finance only - specify notes for transaction"))
  parser.add_option('-o', '--owner', dest='owner',
                    help=('Username or ID of the owner of the resource. ' +
                          'For example,' +
                          " 'picasa list-albums -o bob' to list bob's albums"))
  parser.add_option('--photo', dest='photo',
                    help='Picasa only - specify title or name of photo(s)')
  parser.add_option('--price', dest='price',
                    help=("Finance only - specify price for transaction"))
  parser.add_option('-q', '--query', dest='query',
                    help=('Full text query string for specifying items.'
                          + ' Searches on titles, captions, and tags.'))
  parser.add_option('--quiet', dest='quiet',
                    action='store_true',
                    help='Print only prompts and error messages')
  parser.add_option('--reminder', dest='reminder',
                    help=("Calendar only - specify time for added event's " +
                          'reminder, e.g. "10m", "3h", "1d"'))
  parser.add_option('--shares', dest='shares',
                    help=("Finance only - specify amount of shares " +
                          "for transaction"))
  parser.add_option('--skip-auth', dest='skip_auth',
                    action='store_true',
                    help='Skip validation step for re-used access tokens.')
  parser.add_option('--src', dest='src',
                    help='Source. Typically files to upload.')
  parser.add_option('-s', '--summary', dest='summary',
                    help=('Description of the upload, ' +
                          'or file containing the description.'))
  parser.add_option('-t',  '--tags', dest='tags',
                    help='Tags for item, e.g. "Sunsets, Earth Day"')
  parser.add_option('--ticker', dest='ticker',
                    help=("Finance only - specify ticker"))
  parser.add_option('--ttype', dest='ttype',
                    help=("Finance only - specify transaction type, " +
                          'e.g. "Bye", "Sell", "Buy to Cover", "Sell Short"'))
  parser.add_option('--txnid', dest='txnid',
                    help=("Finance only - specify transaction id"))
  parser.add_option('-u', '--user', dest='user',
                    help='Username to log in with for the service.')
  parser.add_option('-v', '--verbose', dest='verbose',
                    action='store_true',
                    help='Print all messages.')
  parser.add_option('--yes', dest='prompt',
                    action='store_false', default=True,
                    help='Answer "yes" to all prompts')
  return parser
#

Entry point for GoogleCL script.

def main():
#
  loading_usage = '--help' in sys.argv
  parser = setup_parser(loading_usage)

  (options, args) = parse_command_line(parser, sys.argv[1:])

  setup_logger(options)
  if not args:
    run_interactive(parser)
  else:
    is_windows = sys.platform == 'win32'
    args = expand_args(args, True, is_windows, is_windows)
    insert_stdin(options, args)

    try:
      run_once(options, args)
    except KeyboardInterrupt:
      print ''
#

Handler for SIGINT signal.

def exit_from_int(*args):
#
  print ''
  exit(0)


if __name__ == '__main__':
  import signal
  signal.signal(signal.SIGINT, exit_from_int)
  main()