PageRenderTime 8ms CodeModel.GetById 259ms app.highlight 516ms RepoModel.GetById 239ms app.codeStats 1ms

/src/google.py

http://googlecl.googlecode.com/
Python | 1007 lines | 934 code | 19 blank | 54 comment | 55 complexity | acefa02af2e3d8dabec84913decdfe61 MD5 | raw file
   1#!/usr/bin/python
   2#
   3# Copyright (C) 2010 Google Inc.
   4#
   5# Licensed under the Apache License, Version 2.0 (the "License");
   6# you may not use this file except in compliance with the License.
   7# You may obtain a copy of the License at
   8#
   9#      http://www.apache.org/licenses/LICENSE-2.0
  10#
  11# Unless required by applicable law or agreed to in writing, software
  12# distributed under the License is distributed on an "AS IS" BASIS,
  13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14# See the License for the specific language governing permissions and
  15# limitations under the License.
  16
  17
  18"""Main function for the Google command line tool, GoogleCL.
  19
  20This program provides some functionality for a number of Google services from
  21the command line.
  22
  23Example usage (omitting the initial "./google"):
  24  # Create a photo album with tags "Vermont" and name "Summer Vacation 2009"
  25  picasa create -n "Summer Vacation 2009" -t Vermont ~/photos/vacation2009/*
  26
  27  # Post photos to an existing album
  28  picasa post -n "Summer Vacation 2008" ~/old_photos/*.jpg
  29
  30  # Download another user's albums whose titles match a regular expression
  31  picasa get --user my.friend.joe --name ".*computer.*" ~/photos/joes_computer
  32
  33  # Delete some posts you accidentally put up
  34  blogger delete -n "Silly post, number [0-9]*"
  35
  36  # Post your latest film endeavor to YouTube
  37  youtube post --category Film --tag "Jane Austen, zombies" ~/final_project.mp4
  38
  39Some terminology in use:
  40  service: The Google service being accessed (e.g. Picasa, Blogger, YouTube).
  41  task: What the client wants done by the service (e.g. post, get, delete).
  42
  43"""
  44from __future__ import with_statement
  45
  46try:
  47    import gdata
  48except:
  49    print "Unable to find python gdata library."
  50    print "See http://code.google.com/p/googlecl/wiki/Install"
  51    exit(5)
  52
  53
  54__author__ = 'tom.h.miller@gmail.com (Tom Miller)'
  55import glob
  56import logging
  57import optparse
  58import os
  59import sys
  60import traceback
  61import webbrowser
  62import googlecl
  63import googlecl.authentication
  64import googlecl.config
  65try: # Fails if Discovery stuff is unavailable
  66  from googlecl.discovery import DiscoveryManager
  67  apis = True
  68except:
  69  apis = False
  70# Renamed here to reduce verbosity in other sections
  71safe_encode = googlecl.safe_encode
  72safe_decode = googlecl.safe_decode
  73
  74
  75VERSION = '0.9.14'
  76
  77AVAILABLE_SERVICES = ['help', 'picasa', 'blogger', 'youtube', 'docs',
  78                      'contacts', 'calendar', 'finance', 'sites']
  79LOG = logging.getLogger(googlecl.LOGGER_NAME)
  80
  81discovery = None
  82AVAILABLE_APIS = None
  83
  84class NonFatalOptionParser(optparse.OptionParser):
  85  def error(self, message):
  86    self.error_message = message
  87
  88  def bailout(self, message):
  89    print self.usage, "\n\n", "FATAL ERROR:\n", message, "\n"
  90    exit(1)
  91
  92  def bailout_if_necessary(self):
  93    if hasattr(self, 'error_message'):
  94      print self.usage, "\n\n", "FATAL ERROR:\n", self.error_message, "\n"
  95      exit(1)
  96
  97# Attempts to sanely parse the command line, considering both the legacy gdata
  98# services and the new discovery services which aren't known at runtime.
  99def parse_command_line(parser, original_args):
 100  (options, args) = parser.parse_args(original_args)
 101
 102  # If the discovery API is available and we're using it, then it needs to do
 103  # its own argument parsing.
 104
 105  # If there's no discovery API, then any argument errors are fatal.
 106  if not apis:
 107    if len(args) > 0 and args[0] not in AVAILABLE_SERVICES:
 108      parser.bailout("Discovery API unavailable.  Services limited to:" +
 109                     ", ".join(AVAILABLE_SERVICES) + "\nUnknown service: " + args[0])
 110
 111    parser.bailout_if_necessary()
 112
 113  # Even if there is a discovery API, if you asked for a non-discovery service,
 114  # argument errors are fatal.
 115  if len(args) > 0:
 116    if args[0] in AVAILABLE_SERVICES:
 117      parser.bailout_if_necessary()
 118    else:
 119      args = original_args
 120
 121  return (options, args)
 122
 123class Error():
 124  """Base error for this module."""
 125  pass
 126
 127
 128def authenticate(auth_manager, options, config, section_header):
 129  """Set a (presumably valid) OAuth token for the client to use.
 130
 131  Args:
 132    auth_manager: Object handling the authentication process.
 133    options: Parsed command line options.
 134    config: Configuration file parser.
 135    section_header: Section header to look in for the configuration file.
 136
 137  Returns:
 138    True if authenticated, False otherwise.
 139  """
 140  # Only try to set the access token if we're not forced to authenticate.
 141  # XXX: logic in here is iffy. Don't bother checking access token if it's not
 142  # set
 143  if not options.force_auth:
 144    set_token = auth_manager.set_access_token()
 145    if set_token:
 146      LOG.debug('Successfully set token')
 147      skip_auth = (options.skip_auth or
 148                   config.lazy_get(section_header, 'skip_auth',
 149                                   default=False, option_type=bool))
 150    else:
 151      LOG.debug('Failed to set token!')
 152      skip_auth = False
 153  else:
 154    LOG.debug('Forcing retrieval of new token')
 155    skip_auth = False
 156
 157  if options.force_auth or not skip_auth:
 158    LOG.debug('Checking access token...')
 159    valid_token = auth_manager.check_access_token()
 160    if not valid_token:
 161      display_name = auth_manager.get_display_name(options.hostid)
 162      browser_str = config.lazy_get(section_header, 'auth_browser',
 163                                    default=None)
 164      if browser_str:
 165        if browser_str.lower() == 'disabled' or browser_str.lower() == 'none':
 166          browser = None
 167        else:
 168          try:
 169            browser = webbrowser.get(browser_str)
 170          except webbrowser.Error, err:
 171            LOG.warn(safe_encode(u'Could not get browser "%s": %s' %
 172                                 (browser_str, err)))
 173            browser = None
 174      else:
 175        try:
 176          browser = webbrowser.get()
 177        except webbrowser.Error, err:
 178          LOG.warn(safe_encode(u'Could not get default browser: %s' % err))
 179          browser = None
 180
 181      valid_token = auth_manager.retrieve_access_token(display_name, browser)
 182    if valid_token:
 183      LOG.debug('Retrieved valid access token')
 184      config.set_missing_default(section_header, 'skip_auth', True)
 185      return True
 186    else:
 187      LOG.debug('Could not retrieve valid access token')
 188      return False
 189  else:
 190    # Already set an access token and we're not being forced to authenticate
 191    return True
 192
 193
 194# I don't know if this and shlex.split() can replace expand_as_command_line
 195# because of the behavior of globbing characters that are in ""
 196def expand_args(args, on_linesep, on_glob, on_homepath):
 197  """Expands arguments list.
 198
 199  Args:
 200    on_linesep: Set True to split on occurrences of os.linesep. This is
 201        reasonably safe -- line separators appear to be escaped when given to
 202        Python on the command line.
 203    on_glob: Set True to glob expressions. May not be safe! For example, if user
 204        passes in "A*" (including the quotes) the * should NOT be expanded.
 205        Recommended only if sys.platform == 'win32'
 206    on_homepath: Set True to replace a leading ~/ with the user's home
 207        directory. May not be safe! Same situation as on_glob, described above.
 208
 209  Returns:
 210    List of arguments that have been expanded
 211  """
 212  new_args = []
 213  for arg in args:
 214    temp_arg_list = None
 215    if on_linesep:
 216      temp_arg_list = arg.split(os.linesep)
 217    if on_homepath:
 218      if temp_arg_list:
 219        tmp = []
 220        for sub_arg in temp_arg_list:
 221          tmp.append(os.path.expanduser(sub_arg))
 222        temp_arg_list = tmp
 223      else:
 224        arg = os.path.expanduser(arg)
 225    # Globbing needs to happen last, otherwise it wont be able to find
 226    # any files.
 227    if on_glob:
 228      if temp_arg_list:
 229        tmp = []
 230        for sub_arg in temp_arg_list:
 231          tmp.extend(glob.glob(sub_arg))
 232        temp_arg_list = tmp
 233      else:
 234        temp_arg_list = glob.glob(arg)
 235
 236    if temp_arg_list:
 237      new_args.extend(temp_arg_list)
 238    else:
 239      new_args.append(arg)
 240  return new_args
 241
 242
 243def expand_as_command_line(command_string):
 244  """Expand a string as if it was entered at the command line.
 245
 246  Mimics the shell expansion of '~', file globbing, and quotation marks.
 247  For example, 'picasa post -a "My album" ~/photos/*.png' will return
 248  ['picasa', 'post', '-a', 'My album', '$HOME/photos/myphoto1.png', etc.]
 249  It will not treat apostrophes specially, or handle environment variables.
 250
 251  Keyword arguments:
 252    command_string: String to be expanded.
 253
 254  Returns:
 255    A list of strings that (mostly) matches sys.argv as if command_string
 256    was entered on the command line.
 257
 258  """
 259  if not command_string:
 260    return []
 261  # Sub in the home path.
 262  home_path = os.path.expanduser('~/')
 263  # We may get a tilde that needs expansion in the middle of the string...
 264  # (Replace with the space to make sure we don't screw up /really/~/weird/path
 265  command_string = command_string.replace(' ~/', ' ' + home_path)
 266  # Or, if we're given options.src to expand, it could be the first few
 267  # characters.
 268  if command_string.startswith('~/'):
 269    command_string = home_path + command_string[2:]
 270  token_list = command_string.split()
 271  args_list = []
 272  while token_list:
 273    tmp = token_list.pop(0)
 274    start_of_quote = tmp[0] == '"' or tmp[0] == "'"
 275    start_of_dict = tmp[0] == '{'
 276    start_of_list = tmp[0] == '['
 277    # A test to see if the end of a quoted argument has been reached
 278    end_quote = lambda s: s[-1] == s[0] and len(s) > 1 and s[-2] != '\\'
 279    end_dict = lambda s: s[-1] == '}' and len(s) > 1 and s[-2] != '\\'
 280    end_list = lambda s: s[-1] == ']' and len(s) > 1 and s[-2] != '\\'
 281    # Don't need to worry about nesting because of natural syntax:
 282    # ["foo", ["bar"], "baz"] -> stupid to have '["bar"] ,'
 283    while (start_of_quote and not end_quote(tmp)) or (start_of_dict 
 284      and not end_dict(tmp)) or (start_of_list and not end_list(tmp)):
 285      if token_list:
 286        tmp += ' ' + token_list.pop(0)
 287      else:
 288        if start_of_quote:
 289          raise Error('Encountered end of string without finding matching "')
 290        elif start_of_dict:
 291          raise Error('Encountered end of string without finding matching }')
 292        else:
 293          raise Error('Encountered end of string without finding matching ]')
 294    if start_of_quote:
 295      # Add the resulting arg, stripping the " off
 296      args_list.append(tmp[1:-1])
 297    else:
 298      # Grab all the tokens in a row that end with unescaped \
 299      while tmp[-1] == '\\' and len(tmp) > 1 and tmp[-2] != '\\':
 300        if token_list:
 301          tmp = tmp[:-1] + ' ' + token_list.pop(0)
 302        else:
 303          raise Error('Encountered end of string ending in \\')
 304
 305      expanded_args = glob.glob(tmp)
 306      if expanded_args:
 307        args_list.extend(expanded_args)
 308      else:
 309        args_list.append(tmp)
 310  return args_list
 311
 312
 313def fill_out_options(args, service_header, task, options, config):
 314  """Fill out required options via config file and command line prompts.
 315
 316  If there are any required fields missing for a task, fill them in.
 317  This is attempted by checking the following sources, in order:
 318  1) The service_header section of the preferences file.
 319  2) The arguments list given to this function.
 320  3) Prompting the user.
 321
 322  Note that 'user' and 'hostid' are special options -- they are always
 323  required, and they will skip step (2) when checking sources as mentioned
 324  above.
 325
 326  Keyword arguments:
 327    args: list Arguments that may be options.
 328    service_header: str Name of the section in the config file for the
 329                    active service.
 330    task: Requirements of the task (see class googlecl.service.Task).
 331    options: Contains attributes that have been specified already, typically
 332             through options on the command line (see setup_parser()).
 333    config: Configuration file parser.
 334
 335  Returns:
 336    Nothing, though options may be modified to hold the required fields.
 337
 338  """
 339  def _retrieve_value(attr, service_header):
 340    """Retrieve value from config file or user prompt."""
 341    value = config.lazy_get(service_header, attr)
 342    if value:
 343      return value
 344    else:
 345      return raw_input('Please specify ' + attr + ': ')
 346
 347  if options.user is None:
 348    options.user = _retrieve_value('user', service_header)
 349  if options.hostid is None:
 350    options.hostid = _retrieve_value('hostid', service_header)
 351  missing_reqs = task.get_outstanding_requirements(options)
 352  LOG.debug('missing_reqs: ' + str(missing_reqs))
 353
 354  for attr in missing_reqs:
 355    value = config.lazy_get(service_header, attr)
 356    if not value:
 357      if args:
 358        value = args.pop(0)
 359      else:
 360        value = raw_input('Please specify ' + attr + ': ')
 361    setattr(options, attr, value)
 362
 363  # Expand those options that might be a filename in disguise.
 364  max_file_size = 500000    # Value picked arbitrarily - no idea what the max
 365                            # size in bytes of a summary is.
 366  if options.summary and os.path.exists(os.path.expanduser(options.summary)):
 367    with open(options.summary, 'r') as summary_file:
 368      options.summary = summary_file.read(max_file_size)
 369  if options.devkey and os.path.exists(os.path.expanduser(options.devkey)):
 370    with open(options.devkey, 'r') as key_file:
 371      options.devkey = key_file.read(max_file_size).strip()
 372
 373
 374def get_task_help(service, tasks):
 375  help = 'Available tasks for service ' + service + \
 376         ': ' + str(tasks.keys())[1:-1] + '\n'
 377  for task_name in tasks.keys():
 378    help += ' ' + task_name + ': ' + tasks[task_name].description + '\n'
 379    help += '  ' + tasks[task_name].usage + '\n\n'
 380
 381  return help
 382
 383
 384def import_at_runtime(module):
 385  """Imports a module/package.
 386
 387  Args:
 388    module: Module/package to import
 389
 390  Returns:
 391    Module or package.
 392  """
 393  # Proper use of this function seems sketchy. Docs claim that only globals() is
 394  # used, but it seems that if fromlist evaluates to False or is not passed in,
 395  # nothing is actually imported. Needs to be a list type (at least to play nice
 396  # with Jython?) and contain a string in the list.
 397  return __import__(module, globals(), fromlist=['0'])
 398
 399def import_service(service, config_file_path):
 400  """Import vital information about a service.
 401
 402  The goal of this function is to allow expansion to other "service" classes
 403  in the future. In the same way that the v2 and v3 API python library uses
 404  a module called "client", googlecl will do the same.
 405
 406  Keyword arguments:
 407    service: Name of the service to import e.g. 'picasa', 'youtube'
 408    config_file_path: Path to config file.
 409
 410  Returns:
 411    Tuple of service_class, tasks, section header and config, where
 412      service_class is the class to instantiate for the service
 413      tasks is the dictionary mapping names to Task objects
 414      section_header is the name of the section in the config file that contains
 415                     options specific to the service.
 416      config is a configuration file parser.
 417  """
 418  LOG.debug('Your pythonpath: ' + str(os.environ.get('PYTHONPATH')))
 419  try:
 420    package = import_at_runtime('googlecl.' + service)
 421  except ImportError, err:
 422    LOG.error(err.args[0])
 423    LOG.error('Did you specify the service correctly? Must be one of ' +
 424              str(AVAILABLE_SERVICES)[1:-1])
 425    return (None, None, None, None)
 426
 427  config = googlecl.config.load_configuration(config_file_path)
 428  force_gdata_v1 = config.lazy_get(package.SECTION_HEADER,
 429                                   'force_gdata_v1',
 430                                   default=False,
 431                                   option_type=bool)
 432
 433  if force_gdata_v1:
 434    service_module = import_at_runtime('googlecl.' + service + '.service')
 435  else:
 436    try:
 437      service_module = import_at_runtime('googlecl.' + service + '.client')
 438    except ImportError:
 439      service_module = import_at_runtime('googlecl.' + service + '.service')
 440  return (service_module.SERVICE_CLASS,
 441          package.TASKS,
 442          package.SECTION_HEADER,
 443          config)
 444
 445
 446def insert_stdin(options, args, single_arg_symbol='_', split_arg_symbol='__'):
 447  """Insert stdin buffer into options or args.
 448
 449  Args:
 450    options: Object containing values for options. Will only be searched for
 451        single_arg_symbol.
 452    args: List of arguments.
 453    single_arg_symbol: Symbol indicating that stdin should be inserted as a
 454        single argument.
 455    split_arg_symbol: Symbol indicating that stdin should be inserted as a
 456        list of arguments. This symbol should only appear in args.
 457
 458  Returns:
 459    Nothing, but args and options are modified in place.
 460  """
 461  try:
 462    i = args.index(single_arg_symbol)
 463  except ValueError:
 464    pass
 465  else:
 466    args[i] = sys.stdin.read()
 467    return
 468
 469  try:
 470    i = args.index(split_arg_symbol)
 471  except ValueError:
 472    pass
 473  else:
 474    args[i:i+1] = expand_as_command_line(sys.stdin.read())
 475    return
 476
 477  if single_arg_symbol in options.__dict__.values():
 478    for key, value in options.__dict__.iteritems():
 479      if value == single_arg_symbol:
 480        setattr(options, key, sys.stdin.read())
 481        break
 482
 483
 484def print_help(service=None, tasks=None):
 485  """Print help messages to the screen.
 486
 487  Keyword arguments:
 488    service: Service to get help on. (Default None, prints general help)
 489    tasks: Dictionary of tasks that can be done by the given service.
 490           (Default None)
 491
 492  """
 493  if not service:
 494    print 'Welcome to the Google CL tool!'
 495    print '  Commands are broken into several parts: '
 496    print '    service, task, options, and arguments.'
 497    print '  For example, in the command'
 498    print '      "> picasa post --title "My Cat Photos" photos/cats/*"'
 499    print '  the service is "picasa", the task is "post", the single'
 500    print '  option is a title of "My Cat Photos", and the argument is the '
 501    print '  path to the photos.'
 502    print ''
 503    print '  The available services are '
 504    print str(AVAILABLE_SERVICES)[1:-1]
 505    if apis:
 506     print '  and via Discovery:'
 507     print str(AVAILABLE_APIS)[1:-1]
 508     print '  Enter "> help more" for more detailed help.'
 509    print '  Enter "> help <service>" for more information on a service.'
 510    print '  Or, just "quit" to quit.'
 511  else:
 512    print get_task_help(service, tasks)
 513
 514def print_more_help():
 515  """ Prints additional help """
 516  print """  Additional information:
 517    (For Discovery APIs)
 518  Enter "> help <service> <fields>" for additional info
 519  You may also add a '-v' or '--verbose' tag for even more detailed information.
 520
 521  Enter "> refresh apis" to update the Discovery APIs list
 522  This will allow you to use the latest APIs by default.
 523  Older APIs may be used by calling '> <service> <version> <etc>'
 524
 525  You may add more APIs by providing the path to their Discovery document in the
 526  config file, under the parameter 'local_apis'
 527
 528  Global config values may be viewed and edited with "> edit config" """
 529
 530def run_interactive(parser):
 531  """Run an interactive shell for the google commands.
 532
 533  Keyword arguments:
 534    parser: Object capable of parsing a list of arguments via parse_args.
 535
 536  """
 537  history_file = googlecl.get_data_path(googlecl.HISTORY_FILENAME,
 538                                        create_missing_dir=True)
 539  try:
 540    import readline
 541    try:
 542      readline.read_history_file(history_file)
 543    except EnvironmentError:
 544      LOG.debug('Could not read history file.')
 545  except ImportError:
 546    LOG.debug('Could not import readline module.')
 547
 548  while True:
 549    try:
 550      command_string = raw_input('> ')
 551      if command_string.startswith('python '):
 552        LOG.info('HINT: No need to include "python" in interactive mode')
 553        command_string = command_string.replace('python ', '', 1)
 554      if command_string.startswith('google '):
 555        LOG.info('HINT: No need to include "google" in interactive mode')
 556        command_string = command_string.replace('google ', '', 1)
 557      if not command_string:
 558        continue
 559      elif command_string == '?':
 560        print_help()
 561      elif command_string == 'quit':
 562        break
 563      else:
 564        try:
 565          args_list = expand_as_command_line(command_string)
 566        except Error, err:
 567          LOG.error(err)
 568          continue
 569
 570        (options, args) = parse_command_line(parser, args_list)
 571        run_once(options, args)
 572
 573    except (KeyboardInterrupt, ValueError), err:
 574      # It would be nice if we could simply unregister or reset the
 575      # signal handler defined in the initial if __name__ block.
 576      # Windows will raise a KeyboardInterrupt, GNU/Linux seems to also
 577      # potentially raise a ValueError about I/O operation.
 578      if isinstance(err, ValueError) and \
 579         str(err).find('I/O operation on closed file') == -1:
 580        print "Error: " + str(err)
 581        LOG.error(err)
 582        raise err
 583      print ''
 584      print 'Quit via keyboard interrupt'
 585      break
 586    except EOFError:
 587      print ''
 588      break
 589    except SystemExit:
 590      # optparse.OptParser prints the usage statement and calls
 591      # sys.exit when there are any option errors.
 592      # Printing usage good, SystemExit bad. So catch it and do nothing.
 593      pass
 594    except BaseException:
 595      traceback.print_exc()
 596  if 'readline' in sys.modules:
 597    readline.write_history_file(history_file)
 598
 599
 600def run_once(options, args):
 601  """Run one command.
 602
 603  Keyword arguments:
 604    options: Options instance as built and returned by optparse.
 605    args: Arguments to GoogleCL, also as returned by optparse.
 606
 607  """
 608  global discovery
 609
 610  # If we haven't gotten the list of discovery APIs yet, and they're asking for
 611  # a discovery API, figure out their email address and then get a list of
 612  # APIs.
 613  if apis and not discovery:
 614    if (args[0] not in AVAILABLE_SERVICES) or \
 615       (args[0] == 'help' and len(args) == 1) or \
 616       (args[0] == 'help' and len(args)>1 and \
 617        args[1] not in AVAILABLE_SERVICES):
 618      # Is there a better approach than using the calendar API to get the email
 619      # address?
 620      service_class, tasks, section_header, config = import_service('calendar',
 621                                                                    None)
 622      email = config.lazy_get(section_header, 'user')
 623      discovery = DiscoveryManager(email)
 624      global AVAILABLE_APIS
 625      AVAILABLE_APIS = discovery.apis_list()
 626
 627  init_args = args[:]
 628  try:
 629    service = args.pop(0)
 630    task_name = args.pop(0)
 631  except IndexError:
 632    if service == 'help':
 633      print_help()
 634    else:
 635      LOG.error('Must specify at least a service and a task!')
 636    return
 637
 638  if apis and service == 'refresh' and task_name == 'apis':
 639    discovery.docManager.load(force=True)
 640    AVAILABLE_APIS = discovery.apis_list()
 641    return
 642
 643  if apis and service == 'edit' and task_name == 'config':
 644    import subprocess
 645    subprocess.call((discovery.dataManager.editor,
 646                   googlecl.config.get_config_path()))
 647    return
 648
 649  if service == 'help' and task_name == 'more':
 650    print_more_help()
 651    return
 652
 653  # Detects if GData is not provided a version number or the path is too long
 654  conflict = (task_name[0] == 'v' and task_name[1].isdigit()) or (service == 'help' and args)
 655  # Prioritizes using existing GData APIs over Discovery.
 656  # May have to change if/when those are brought over to Discovery...
 657  if service == 'help':
 658    if task_name in AVAILABLE_SERVICES and not conflict:
 659      service_class, tasks, section_header, config = import_service(task_name,
 660                                                                options.config)
 661      if tasks:
 662        print_help(task_name, tasks)
 663        return
 664    else:
 665      if apis and task_name in AVAILABLE_APIS:
 666        discovery.run(init_args)
 667      elif not apis:
 668        LOG.error('Did not recognize service.')
 669        LOG.error('If you wanted a Discovery service, make sure you have')
 670        LOG.error('google-api-python-client installed.')
 671      else:
 672        LOG.error('Did not recognize service.')
 673      return
 674  elif service in AVAILABLE_SERVICES and not conflict:
 675      service_class, tasks, section_header, config = import_service(service,
 676                                                                options.config)
 677  else:
 678    if apis and service in AVAILABLE_APIS:
 679      discovery.run(init_args)
 680    elif not apis:
 681      LOG.error('Did not recognize service.')
 682      LOG.error('If you wanted a Discovery service, make sure you have')
 683      LOG.error('google-api-python-client installed.')
 684    else:
 685      LOG.error('Did not recognize service.')
 686    return
 687  if not service_class:
 688    return
 689  client = service_class(config)
 690  # Activate debugging output from HTTP requests. "service" clients only!
 691  # "client" versions need to set self.http_client.debug in their own __init__
 692  client.debug = config.lazy_get(section_header,
 693                                 'debug',
 694                                 default=options.debug,
 695                                 option_type=bool)
 696  # XXX: Not the best place for this.
 697  if hasattr(client, 'http_client'):
 698    client.http_client.debug = client.debug
 699  try:
 700    task = tasks[task_name]
 701    task.name = task_name
 702  except KeyError:
 703    LOG.error('Did not recognize task, please use one of ' + \
 704              str(tasks.keys()))
 705    return
 706
 707  if 'devkey' in task.required:
 708    # If a devkey is required, and there is none specified via an option
 709    # BEFORE fill_out_options, insert the key from file or the key given
 710    # to GoogleCL.
 711    # You can get your own key at http://code.google.com/apis/youtube/dashboard
 712    if not options.devkey:
 713      options.devkey = googlecl.read_devkey() or 'AI39si4d9dBo0dX7TnGyfQ68bNiKfEeO7wORCfY3HAgSStFboTgTgAi9nQwJMfMSizdGIs35W9wVGkygEw8ei3_fWGIiGSiqnQ'
 714
 715  # fill_out_options will read the key from file if necessary, but will not set
 716  # it since it will always get a non-empty value beforehand.
 717  fill_out_options(args, section_header, task, options, config)
 718  client.email = options.user
 719
 720  if options.blog:
 721    config.set_missing_default(section_header, 'blog', options.blog)
 722  if options.devkey:
 723    client.developer_key = options.devkey
 724    # This may save an invalid dev key -- it's up to the user to specify a
 725    # valid dev key eventually.
 726    # TODO: It would be nice to make this more efficient.
 727    googlecl.write_devkey(options.devkey)
 728
 729  # Unicode-ize options and args
 730  for attr_name in dir(options):
 731    attr = getattr(options, attr_name)
 732    if not attr_name.startswith('_') and isinstance(attr, str):
 733      setattr(options, attr_name, safe_decode(attr, googlecl.TERMINAL_ENCODING))
 734  if args:
 735    args = [safe_decode(string, googlecl.TERMINAL_ENCODING) for string in args]
 736
 737  # Expand options.src. The goal is to expand things like
 738  # --src=~/Photos/album1/* (which does not normally happen)
 739  # XXX: This ought to be in fill_out_options(), along with unicode-ize above.
 740  if options.src:
 741    expanded_args = glob.glob(options.src)
 742    if expanded_args:
 743      options.src = expanded_args
 744    else:
 745      options.src = [options.src]
 746  else:
 747    options.src = []
 748
 749  # Take a gander at the options filled in.
 750  if LOG.getEffectiveLevel() == logging.DEBUG:
 751    import inspect
 752    for attr_name in dir(options):
 753      if not attr_name.startswith('_'):
 754        attr = getattr(options, attr_name)
 755        if attr is not None and not inspect.ismethod(attr):
 756          LOG.debug(safe_encode('Option ' + attr_name + ': ' + unicode(attr)))
 757  LOG.debug(safe_encode('args: ' + unicode(args)))
 758
 759  auth_manager = googlecl.authentication.AuthenticationManager(service, client)
 760  authenticated = authenticate(auth_manager, options, config, section_header)
 761
 762  if not authenticated:
 763    LOG.debug('Authentication failed, exiting run_once')
 764    return -1
 765
 766  # If we've authenticated, save the config values we've been setting.
 767  # And remember the email address that worked!
 768  config.set_missing_default(section_header, 'user', client.email)
 769  config.write_out_parser()
 770  run_error = None
 771  try:
 772    task.run(client, options, args)
 773  except AttributeError, run_error:
 774    err_str = safe_decode(run_error)
 775    if err_str.startswith("'OAuth"):
 776      LOG.info('OAuth error.  Try re-running with --force-auth.')
 777    else:
 778      raise run_error
 779  if run_error and LOG.isEnabledFor(logging.DEBUG):
 780    # XXX: This will probably not work if googlecl gets threaded (unlikely)
 781    type, value, traceback_obj = sys.exc_info()
 782    LOG.debug(''.join(traceback.format_exception(type, value, traceback_obj)))
 783    return -1
 784  return 0
 785
 786
 787def setup_logger(options):
 788  """Setup the global (root, basic) configuration for logging."""
 789  msg_format = '%(message)s'
 790  if options.debug:
 791    level = logging.DEBUG
 792    msg_format = '%(levelname)s:%(name)s:%(message)s'
 793  elif options.verbose:
 794    level = logging.DEBUG
 795  elif options.quiet:
 796    level = logging.ERROR
 797  else:
 798    level = logging.INFO
 799  # basicConfig does nothing if it's been called before
 800  # (e.g. in run_interactive loop)
 801  logging.basicConfig(level=level, format=msg_format)
 802  # Redundant for single-runs, but necessary for run_interactive.
 803  LOG.setLevel(level)
 804  # XXX: Inappropriate location (style-wise).
 805  if options.debug or options.verbose:
 806    import gdata
 807    LOG.debug('Gdata will be imported from ' + gdata.__file__)
 808
 809
 810def setup_parser(loading_usage):
 811  """Set up the parser.
 812
 813  Returns:
 814    optparse.OptionParser with options configured.
 815
 816  """
 817  available_services = '[' + '|'.join(AVAILABLE_SERVICES) + ']'
 818  # NOTE: Usage string formatted to work with help2man.  After changing it,
 819  # please run:
 820  # 'help2man -N -n "command-line access to (some) Google services" \
 821  #  -i ../man/examples.help2man  ./google > google.1'
 822  # then 'man ./google.1' and make sure the generated manpage still looks
 823  # reasonable.  Then save it to man/google.1
 824  usage = ('Usage: ' + sys.argv[0] + ' ' + available_services +
 825           ' TASK [options]\n'
 826           '\n'
 827           'This program provides command-line access to\n'
 828           '(some) google services via their gdata APIs.\n'
 829           'Called without a service name, it starts an interactive session.\n'
 830           '\n'
 831           'NOTE: GoogleCL will interpret arguments as required options in the\n'
 832           'order they appear in the descriptions below, excluding options\n'
 833           'set in the configuration file and non-primary terms in '
 834           'parenthesized\n'
 835           'OR groups. For example:\n'
 836           '\n'
 837           '\t$ google picasa get my_album .\n'
 838           'is interpreted as "google picasa get --title=my_album --dest=.\n'
 839           '\n'
 840           '\t$ google contacts list john\n'
 841           'is interpreted as "$ google contacts list '
 842           '--fields=<config file def> --title=john --delimiter=,"\n'
 843           '(only true if you have not removed the default definition in the '
 844           'config file!)\n'
 845           '\n'
 846           '\t$ google docs get my_doc .\n'
 847           'is interpreted as "$ google docs get --title=my_doc --dest=.\n'
 848           '(folder is NOT set, since the title option is satisfied first.)\n\n'
 849           )
 850
 851  if loading_usage:
 852    for service in AVAILABLE_SERVICES:
 853      if service == 'help':
 854        continue
 855      service_package = import_at_runtime('googlecl.' + service)
 856      usage += get_task_help(service, service_package.TASKS) + '\n'
 857
 858  parser = NonFatalOptionParser(usage=usage, version=sys.argv[0] + VERSION)
 859  parser.add_option('--access', dest='access',
 860                    help='Specify access/visibility level of an upload')
 861  parser.add_option('--blog', dest='blog',
 862                    help='Blogger only - specify a blog other than your' +
 863                    ' primary.')
 864  parser.add_option('--cal', dest='cal',
 865                    help='Calendar only - specify a calendar other than your' +
 866                    ' primary.')
 867  parser.add_option('-c', '--category', dest='category',
 868                    help='YouTube only - specify video categories' +
 869                    ' as a comma-separated list, e.g. "Film, Travel"')
 870  parser.add_option('--commission', dest='commission',
 871                    help=("Finance only - specify commission for transaction"))
 872  parser.add_option('--config', dest='config',
 873                    help='Specify location of config file.')
 874  parser.add_option('--currency', dest='currency',
 875                    help=("Finance only - specify currency for portfolio"))
 876  parser.add_option('--devtags', dest='devtags',
 877                    help='YouTube only - specify developer tags' +
 878                    ' as a comma-separated list.')
 879  parser.add_option('--devkey', dest='devkey',
 880                    help='YouTube only - specify a developer key')
 881  parser.add_option('-d', '--date', dest='date',
 882                    help=('Calendar only - date of the event to add/look for. '
 883                          'Can also specify a range with a comma.\n'
 884                          'Picasa only - sets the date of the album\n'
 885                          'Finance only - transaction creation date'))
 886  parser.add_option('--debug', dest='debug',
 887                    action='store_true',
 888                    help=('Enable all debugging output, including HTTP data'))
 889  parser.add_option('--delimiter', dest='delimiter', default=',',
 890                    help='Specify a delimiter for the output of the list task.')
 891  parser.add_option('--dest', dest='dest',
 892                    help=('Destination. Typically, where to save data being'
 893                          ' downloaded.'))
 894  parser.add_option('--domain', dest='domain', help='Sites only - specify domain')
 895  parser.add_option('--draft', dest='access',
 896                    action='store_const', const='draft',
 897                    help=('Blogger only - post as a draft. Shorthand for '
 898                          '--access=draft'))
 899  parser.add_option('--editor', dest='editor',
 900                    help='Docs only - editor to use on a file.')
 901  parser.add_option('--fields', dest='fields',
 902                    help='Fields to list with list task.')
 903  parser.add_option('-f', '--folder', dest='folder',
 904                    help='Sites: sites page (folder) to upload under. Docs - specify folder(s) to upload to '+
 905                    '/ search in.')
 906  parser.add_option('--force-auth', dest='force_auth',
 907                    action='store_true',
 908                    help='Force validation step for re-used access tokens' +
 909                         ' (Overrides --skip-auth).')
 910  parser.add_option('--format', dest='format',
 911                    help='Sites - sites page type to upload as. Docs - format to download documents as.')
 912  parser.add_option('--gid', dest='gid',
 913                    help=("Docs only - Spreadsheet Grid ID. Corresponds to a specific worksheet. Can be found in the browser URL when "+
 914                          "viewing a worksheet. Note that GID's do not correspond to indexes, which are not available as worksheet identifiers."))
 915  parser.add_option('--hostid', dest='hostid',
 916                    help='Label the machine being used.')
 917  parser.add_option('--max_results', dest='max_results',
 918                    help='Sites: max results to return for list. Overrides config.')
 919  parser.add_option('-n', '--title', dest='title',
 920                    help='Title of the item')
 921  parser.add_option('--no-convert', dest='convert',
 922                    action='store_false', default=True,
 923                    help='Google Apps Premier only - do not convert the file' +
 924                    ' on upload. (Else converts to native Google Docs format)')
 925  parser.add_option('--notes', dest='notes',
 926                    help=("Finance only - specify notes for transaction"))
 927  parser.add_option('-o', '--owner', dest='owner',
 928                    help=('Username or ID of the owner of the resource. ' +
 929                          'For example,' +
 930                          " 'picasa list-albums -o bob' to list bob's albums"))
 931  parser.add_option('--photo', dest='photo',
 932                    help='Picasa only - specify title or name of photo(s)')
 933  parser.add_option('--price', dest='price',
 934                    help=("Finance only - specify price for transaction"))
 935  parser.add_option('-q', '--query', dest='query',
 936                    help=('Sites, Picasa: full text search with this string.'
 937                          + ' Picasa: searches on titles, captions, and tags.'))
 938  parser.add_option('--quiet', dest='quiet',
 939                    action='store_true',
 940                    help='Print only prompts and error messages')
 941  parser.add_option('--reminder', dest='reminder',
 942                    help=("Calendar only - specify time for added event's " +
 943                          'reminder, e.g. "10m", "3h", "1d"'))
 944  parser.add_option('--shares', dest='shares',
 945                    help=("Finance only - specify amount of shares " +
 946                          "for transaction"))
 947  parser.add_option('--site', dest='site', help='Sites only - specify site')
 948  parser.add_option('--skip-auth', dest='skip_auth',
 949                    action='store_true',
 950                    help='Skip validation step for re-used access tokens.')
 951  parser.add_option('--src', dest='src',
 952                    help='Source. Typically files to upload.')
 953  parser.add_option('-s', '--summary', dest='summary',
 954                    help=('Description of the upload, ' +
 955                          'or file containing the description.'))
 956  parser.add_option('-t',  '--tags', dest='tags',
 957                    help='Tags for item, e.g. "Sunsets, Earth Day"')
 958  parser.add_option('--ticker', dest='ticker',
 959                    help=("Finance only - specify ticker"))
 960  parser.add_option('--ttype', dest='ttype',
 961                    help=("Finance only - specify transaction type, " +
 962                          'e.g. "Bye", "Sell", "Buy to Cover", "Sell Short"'))
 963  parser.add_option('--txnid', dest='txnid',
 964                    help=("Finance only - specify transaction id"))
 965  parser.add_option('-u', '--user', dest='user',
 966                    help=('Username to log in with for the service. '+  
 967                          'If not provided full email address (e.g. "foo"), than it is assumed to be in gmail.com domain (e.g. "foo@gmail.com"). ' +
 968                          'If you want to use another domain, provide full email address like "foo@bar.com"'))
 969  parser.add_option('-v', '--verbose', dest='verbose',
 970                    action='store_true',
 971                    help='Print all messages.')
 972  parser.add_option('--yes', dest='prompt',
 973                    action='store_false', default=True,
 974                    help='Answer "yes" to all prompts')
 975  return parser
 976
 977def main():
 978  """Entry point for GoogleCL script."""
 979  loading_usage = '--help' in sys.argv
 980  parser = setup_parser(loading_usage)
 981
 982  (options, args) = parse_command_line(parser, sys.argv[1:])
 983
 984  setup_logger(options)
 985  if not args:
 986    run_interactive(parser)
 987  else:
 988    is_windows = sys.platform == 'win32'
 989    args = expand_args(args, True, is_windows, is_windows)
 990    insert_stdin(options, args)
 991
 992    try:
 993      run_once(options, args)
 994    except KeyboardInterrupt:
 995      print ''
 996
 997
 998def exit_from_int(*args):
 999  """Handler for SIGINT signal."""
1000  print ''
1001  exit(0)
1002
1003
1004if __name__ == '__main__':
1005  import signal
1006  signal.signal(signal.SIGINT, exit_from_int)
1007  main()