PageRenderTime 99ms CodeModel.GetById 0ms RepoModel.GetById 0ms app.codeStats 0ms

/src/google.py

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