/src/google.py
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()