/gdata/docs/service.py

http://radioappz.googlecode.com/ · Python · 611 lines · 425 code · 35 blank · 151 comment · 24 complexity · b60eb6616bbd94bb7e61d9a0aa532ab6 MD5 · raw file

  1. #!/usr/bin/python
  2. #
  3. # Copyright 2009 Google Inc. All Rights Reserved.
  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. """DocsService extends the GDataService to streamline Google Documents
  17. operations.
  18. DocsService: Provides methods to query feeds and manipulate items.
  19. Extends GDataService.
  20. DocumentQuery: Queries a Google Document list feed.
  21. DocumentAclQuery: Queries a Google Document Acl feed.
  22. """
  23. __author__ = ('api.jfisher (Jeff Fisher), '
  24. 'e.bidelman (Eric Bidelman)')
  25. import re
  26. import atom
  27. import gdata.service
  28. import gdata.docs
  29. import urllib
  30. # XML Namespaces used in Google Documents entities.
  31. DATA_KIND_SCHEME = gdata.GDATA_NAMESPACE + '#kind'
  32. DOCUMENT_LABEL = 'document'
  33. SPREADSHEET_LABEL = 'spreadsheet'
  34. PRESENTATION_LABEL = 'presentation'
  35. FOLDER_LABEL = 'folder'
  36. PDF_LABEL = 'pdf'
  37. LABEL_SCHEME = gdata.GDATA_NAMESPACE + '/labels'
  38. STARRED_LABEL_TERM = LABEL_SCHEME + '#starred'
  39. TRASHED_LABEL_TERM = LABEL_SCHEME + '#trashed'
  40. HIDDEN_LABEL_TERM = LABEL_SCHEME + '#hidden'
  41. MINE_LABEL_TERM = LABEL_SCHEME + '#mine'
  42. PRIVATE_LABEL_TERM = LABEL_SCHEME + '#private'
  43. SHARED_WITH_DOMAIN_LABEL_TERM = LABEL_SCHEME + '#shared-with-domain'
  44. VIEWED_LABEL_TERM = LABEL_SCHEME + '#viewed'
  45. FOLDERS_SCHEME_PREFIX = gdata.docs.DOCUMENTS_NAMESPACE + '/folders/'
  46. # File extensions of documents that are permitted to be uploaded or downloaded.
  47. SUPPORTED_FILETYPES = {
  48. 'CSV': 'text/csv',
  49. 'TSV': 'text/tab-separated-values',
  50. 'TAB': 'text/tab-separated-values',
  51. 'DOC': 'application/msword',
  52. 'DOCX': ('application/vnd.openxmlformats-officedocument.'
  53. 'wordprocessingml.document'),
  54. 'ODS': 'application/x-vnd.oasis.opendocument.spreadsheet',
  55. 'ODT': 'application/vnd.oasis.opendocument.text',
  56. 'RTF': 'application/rtf',
  57. 'SXW': 'application/vnd.sun.xml.writer',
  58. 'TXT': 'text/plain',
  59. 'XLS': 'application/vnd.ms-excel',
  60. 'XLSX': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  61. 'PDF': 'application/pdf',
  62. 'PNG': 'image/png',
  63. 'PPT': 'application/vnd.ms-powerpoint',
  64. 'PPS': 'application/vnd.ms-powerpoint',
  65. 'HTM': 'text/html',
  66. 'HTML': 'text/html',
  67. 'ZIP': 'application/zip',
  68. 'SWF': 'application/x-shockwave-flash'
  69. }
  70. class DocsService(gdata.service.GDataService):
  71. """Client extension for the Google Documents service Document List feed."""
  72. __FILE_EXT_PATTERN = re.compile('.*\.([a-zA-Z]{3,}$)')
  73. __RESOURCE_ID_PATTERN = re.compile('^([a-z]*)(:|%3A)([\w-]*)$')
  74. def __init__(self, email=None, password=None, source=None,
  75. server='docs.google.com', additional_headers=None, **kwargs):
  76. """Creates a client for the Google Documents service.
  77. Args:
  78. email: string (optional) The user's email address, used for
  79. authentication.
  80. password: string (optional) The user's password.
  81. source: string (optional) The name of the user's application.
  82. server: string (optional) The name of the server to which a connection
  83. will be opened. Default value: 'docs.google.com'.
  84. **kwargs: The other parameters to pass to gdata.service.GDataService
  85. constructor.
  86. """
  87. gdata.service.GDataService.__init__(
  88. self, email=email, password=password, service='writely', source=source,
  89. server=server, additional_headers=additional_headers, **kwargs)
  90. def _MakeKindCategory(self, label):
  91. if label is None:
  92. return None
  93. return atom.Category(scheme=DATA_KIND_SCHEME,
  94. term=gdata.docs.DOCUMENTS_NAMESPACE + '#' + label, label=label)
  95. def _MakeContentLinkFromId(self, resource_id):
  96. match = self.__RESOURCE_ID_PATTERN.match(resource_id)
  97. label = match.group(1)
  98. doc_id = match.group(3)
  99. if label == DOCUMENT_LABEL:
  100. return '/feeds/download/documents/Export?docId=%s' % doc_id
  101. if label == PRESENTATION_LABEL:
  102. return '/feeds/download/presentations/Export?docId=%s' % doc_id
  103. if label == SPREADSHEET_LABEL:
  104. return ('http://spreadsheets.google.com/feeds/download/spreadsheets/'
  105. 'Export?key=%s' % doc_id)
  106. raise ValueError, 'Invalid resource id: %s' % resource_id
  107. def _UploadFile(self, media_source, title, category, folder_or_uri=None):
  108. """Uploads a file to the Document List feed.
  109. Args:
  110. media_source: A gdata.MediaSource object containing the file to be
  111. uploaded.
  112. title: string The title of the document on the server after being
  113. uploaded.
  114. category: An atom.Category object specifying the appropriate document
  115. type.
  116. folder_or_uri: DocumentListEntry or string (optional) An object with a
  117. link to a folder or a uri to a folder to upload to.
  118. Note: A valid uri for a folder is of the form:
  119. /feeds/folders/private/full/folder%3Afolder_id
  120. Returns:
  121. A DocumentListEntry containing information about the document created on
  122. the Google Documents service.
  123. """
  124. if folder_or_uri:
  125. try:
  126. uri = folder_or_uri.content.src
  127. except AttributeError:
  128. uri = folder_or_uri
  129. else:
  130. uri = '/feeds/documents/private/full'
  131. entry = gdata.docs.DocumentListEntry()
  132. entry.title = atom.Title(text=title)
  133. if category is not None:
  134. entry.category.append(category)
  135. entry = self.Post(entry, uri, media_source=media_source,
  136. extra_headers={'Slug': media_source.file_name},
  137. converter=gdata.docs.DocumentListEntryFromString)
  138. return entry
  139. def _DownloadFile(self, uri, file_path):
  140. """Downloads a file.
  141. Args:
  142. uri: string The full Export URL to download the file from.
  143. file_path: string The full path to save the file to.
  144. Raises:
  145. RequestError: on error response from server.
  146. """
  147. server_response = self.request('GET', uri)
  148. response_body = server_response.read()
  149. if server_response.status != 200:
  150. raise gdata.service.RequestError, {'status': server_response.status,
  151. 'reason': server_response.reason,
  152. 'body': response_body}
  153. f = open(file_path, 'wb')
  154. f.write(response_body)
  155. f.flush()
  156. f.close()
  157. def MoveIntoFolder(self, source_entry, folder_entry):
  158. """Moves a document into a folder in the Document List Feed.
  159. Args:
  160. source_entry: DocumentListEntry An object representing the source
  161. document/folder.
  162. folder_entry: DocumentListEntry An object with a link to the destination
  163. folder.
  164. Returns:
  165. A DocumentListEntry containing information about the document created on
  166. the Google Documents service.
  167. """
  168. entry = gdata.docs.DocumentListEntry()
  169. entry.id = source_entry.id
  170. entry = self.Post(entry, folder_entry.content.src,
  171. converter=gdata.docs.DocumentListEntryFromString)
  172. return entry
  173. def Query(self, uri, converter=gdata.docs.DocumentListFeedFromString):
  174. """Queries the Document List feed and returns the resulting feed of
  175. entries.
  176. Args:
  177. uri: string The full URI to be queried. This can contain query
  178. parameters, a hostname, or simply the relative path to a Document
  179. List feed. The DocumentQuery object is useful when constructing
  180. query parameters.
  181. converter: func (optional) A function which will be executed on the
  182. retrieved item, generally to render it into a Python object.
  183. By default the DocumentListFeedFromString function is used to
  184. return a DocumentListFeed object. This is because most feed
  185. queries will result in a feed and not a single entry.
  186. """
  187. return self.Get(uri, converter=converter)
  188. def QueryDocumentListFeed(self, uri):
  189. """Retrieves a DocumentListFeed by retrieving a URI based off the Document
  190. List feed, including any query parameters. A DocumentQuery object can
  191. be used to construct these parameters.
  192. Args:
  193. uri: string The URI of the feed being retrieved possibly with query
  194. parameters.
  195. Returns:
  196. A DocumentListFeed object representing the feed returned by the server.
  197. """
  198. return self.Get(uri, converter=gdata.docs.DocumentListFeedFromString)
  199. def GetDocumentListEntry(self, uri):
  200. """Retrieves a particular DocumentListEntry by its unique URI.
  201. Args:
  202. uri: string The unique URI of an entry in a Document List feed.
  203. Returns:
  204. A DocumentListEntry object representing the retrieved entry.
  205. """
  206. return self.Get(uri, converter=gdata.docs.DocumentListEntryFromString)
  207. def GetDocumentListFeed(self, uri=None):
  208. """Retrieves a feed containing all of a user's documents.
  209. Args:
  210. uri: string A full URI to query the Document List feed.
  211. """
  212. if not uri:
  213. uri = gdata.docs.service.DocumentQuery().ToUri()
  214. return self.QueryDocumentListFeed(uri)
  215. def GetDocumentListAclEntry(self, uri):
  216. """Retrieves a particular DocumentListAclEntry by its unique URI.
  217. Args:
  218. uri: string The unique URI of an entry in a Document List feed.
  219. Returns:
  220. A DocumentListAclEntry object representing the retrieved entry.
  221. """
  222. return self.Get(uri, converter=gdata.docs.DocumentListAclEntryFromString)
  223. def GetDocumentListAclFeed(self, uri):
  224. """Retrieves a feed containing all of a user's documents.
  225. Args:
  226. uri: string The URI of a document's Acl feed to retrieve.
  227. Returns:
  228. A DocumentListAclFeed object representing the ACL feed
  229. returned by the server.
  230. """
  231. return self.Get(uri, converter=gdata.docs.DocumentListAclFeedFromString)
  232. def Upload(self, media_source, title, folder_or_uri=None, label=None):
  233. """Uploads a document inside of a MediaSource object to the Document List
  234. feed with the given title.
  235. Args:
  236. media_source: MediaSource The gdata.MediaSource object containing a
  237. document file to be uploaded.
  238. title: string The title of the document on the server after being
  239. uploaded.
  240. folder_or_uri: DocumentListEntry or string (optional) An object with a
  241. link to a folder or a uri to a folder to upload to.
  242. Note: A valid uri for a folder is of the form:
  243. /feeds/folders/private/full/folder%3Afolder_id
  244. label: optional label describing the type of the document to be created.
  245. Returns:
  246. A DocumentListEntry containing information about the document created
  247. on the Google Documents service.
  248. """
  249. return self._UploadFile(media_source, title, self._MakeKindCategory(label),
  250. folder_or_uri)
  251. def Download(self, entry_or_id_or_url, file_path, export_format=None,
  252. gid=None, extra_params=None):
  253. """Downloads a document from the Document List.
  254. Args:
  255. entry_or_id_or_url: a DocumentListEntry, or the resource id of an entry,
  256. or a url to download from (such as the content src).
  257. file_path: string The full path to save the file to.
  258. export_format: the format to convert to, if conversion is required.
  259. gid: grid id, for downloading a single grid of a spreadsheet
  260. extra_params: a map of any further parameters to control how the document
  261. is downloaded
  262. Raises:
  263. RequestError if the service does not respond with success
  264. """
  265. if isinstance(entry_or_id_or_url, gdata.docs.DocumentListEntry):
  266. url = entry_or_id_or_url.content.src
  267. else:
  268. if self.__RESOURCE_ID_PATTERN.match(entry_or_id_or_url):
  269. url = self._MakeContentLinkFromId(entry_or_id_or_url)
  270. else:
  271. url = entry_or_id_or_url
  272. if export_format is not None:
  273. if url.find('/Export?') == -1:
  274. raise gdata.service.Error, ('This entry cannot be exported '
  275. 'as a different format')
  276. url += '&exportFormat=%s' % export_format
  277. if gid is not None:
  278. if url.find('spreadsheets') == -1:
  279. raise gdata.service.Error, 'grid id param is not valid for this entry'
  280. url += '&gid=%s' % gid
  281. if extra_params:
  282. url += '&' + urllib.urlencode(extra_params)
  283. self._DownloadFile(url, file_path)
  284. def Export(self, entry_or_id_or_url, file_path, gid=None, extra_params=None):
  285. """Downloads a document from the Document List in a different format.
  286. Args:
  287. entry_or_id_or_url: a DocumentListEntry, or the resource id of an entry,
  288. or a url to download from (such as the content src).
  289. file_path: string The full path to save the file to. The export
  290. format is inferred from the the file extension.
  291. gid: grid id, for downloading a single grid of a spreadsheet
  292. extra_params: a map of any further parameters to control how the document
  293. is downloaded
  294. Raises:
  295. RequestError if the service does not respond with success
  296. """
  297. ext = None
  298. match = self.__FILE_EXT_PATTERN.match(file_path)
  299. if match:
  300. ext = match.group(1)
  301. self.Download(entry_or_id_or_url, file_path, ext, gid, extra_params)
  302. def CreateFolder(self, title, folder_or_uri=None):
  303. """Creates a folder in the Document List feed.
  304. Args:
  305. title: string The title of the folder on the server after being created.
  306. folder_or_uri: DocumentListEntry or string (optional) An object with a
  307. link to a folder or a uri to a folder to upload to.
  308. Note: A valid uri for a folder is of the form:
  309. /feeds/folders/private/full/folder%3Afolder_id
  310. Returns:
  311. A DocumentListEntry containing information about the folder created on
  312. the Google Documents service.
  313. """
  314. if folder_or_uri:
  315. try:
  316. uri = folder_or_uri.content.src
  317. except AttributeError:
  318. uri = folder_or_uri
  319. else:
  320. uri = '/feeds/documents/private/full'
  321. folder_entry = gdata.docs.DocumentListEntry()
  322. folder_entry.title = atom.Title(text=title)
  323. folder_entry.category.append(self._MakeKindCategory(FOLDER_LABEL))
  324. folder_entry = self.Post(folder_entry, uri,
  325. converter=gdata.docs.DocumentListEntryFromString)
  326. return folder_entry
  327. def MoveOutOfFolder(self, source_entry):
  328. """Moves a document into a folder in the Document List Feed.
  329. Args:
  330. source_entry: DocumentListEntry An object representing the source
  331. document/folder.
  332. Returns:
  333. True if the entry was moved out.
  334. """
  335. return self.Delete(source_entry.GetEditLink().href)
  336. # Deprecated methods
  337. #@atom.deprecated('Please use Upload instead')
  338. def UploadPresentation(self, media_source, title, folder_or_uri=None):
  339. """Uploads a presentation inside of a MediaSource object to the Document
  340. List feed with the given title.
  341. This method is deprecated, use Upload instead.
  342. Args:
  343. media_source: MediaSource The MediaSource object containing a
  344. presentation file to be uploaded.
  345. title: string The title of the presentation on the server after being
  346. uploaded.
  347. folder_or_uri: DocumentListEntry or string (optional) An object with a
  348. link to a folder or a uri to a folder to upload to.
  349. Note: A valid uri for a folder is of the form:
  350. /feeds/folders/private/full/folder%3Afolder_id
  351. Returns:
  352. A DocumentListEntry containing information about the presentation created
  353. on the Google Documents service.
  354. """
  355. return self._UploadFile(
  356. media_source, title, self._MakeKindCategory(PRESENTATION_LABEL),
  357. folder_or_uri=folder_or_uri)
  358. UploadPresentation = atom.deprecated('Please use Upload instead')(
  359. UploadPresentation)
  360. #@atom.deprecated('Please use Upload instead')
  361. def UploadSpreadsheet(self, media_source, title, folder_or_uri=None):
  362. """Uploads a spreadsheet inside of a MediaSource object to the Document
  363. List feed with the given title.
  364. This method is deprecated, use Upload instead.
  365. Args:
  366. media_source: MediaSource The MediaSource object containing a spreadsheet
  367. file to be uploaded.
  368. title: string The title of the spreadsheet on the server after being
  369. uploaded.
  370. folder_or_uri: DocumentListEntry or string (optional) An object with a
  371. link to a folder or a uri to a folder to upload to.
  372. Note: A valid uri for a folder is of the form:
  373. /feeds/folders/private/full/folder%3Afolder_id
  374. Returns:
  375. A DocumentListEntry containing information about the spreadsheet created
  376. on the Google Documents service.
  377. """
  378. return self._UploadFile(
  379. media_source, title, self._MakeKindCategory(SPREADSHEET_LABEL),
  380. folder_or_uri=folder_or_uri)
  381. UploadSpreadsheet = atom.deprecated('Please use Upload instead')(
  382. UploadSpreadsheet)
  383. #@atom.deprecated('Please use Upload instead')
  384. def UploadDocument(self, media_source, title, folder_or_uri=None):
  385. """Uploads a document inside of a MediaSource object to the Document List
  386. feed with the given title.
  387. This method is deprecated, use Upload instead.
  388. Args:
  389. media_source: MediaSource The gdata.MediaSource object containing a
  390. document file to be uploaded.
  391. title: string The title of the document on the server after being
  392. uploaded.
  393. folder_or_uri: DocumentListEntry or string (optional) An object with a
  394. link to a folder or a uri to a folder to upload to.
  395. Note: A valid uri for a folder is of the form:
  396. /feeds/folders/private/full/folder%3Afolder_id
  397. Returns:
  398. A DocumentListEntry containing information about the document created
  399. on the Google Documents service.
  400. """
  401. return self._UploadFile(
  402. media_source, title, self._MakeKindCategory(DOCUMENT_LABEL),
  403. folder_or_uri=folder_or_uri)
  404. UploadDocument = atom.deprecated('Please use Upload instead')(
  405. UploadDocument)
  406. """Calling any of these functions is the same as calling Export"""
  407. DownloadDocument = atom.deprecated('Please use Export instead')(Export)
  408. DownloadPresentation = atom.deprecated('Please use Export instead')(Export)
  409. DownloadSpreadsheet = atom.deprecated('Please use Export instead')(Export)
  410. """Calling any of these functions is the same as calling MoveIntoFolder"""
  411. MoveDocumentIntoFolder = atom.deprecated(
  412. 'Please use MoveIntoFolder instead')(MoveIntoFolder)
  413. MovePresentationIntoFolder = atom.deprecated(
  414. 'Please use MoveIntoFolder instead')(MoveIntoFolder)
  415. MoveSpreadsheetIntoFolder = atom.deprecated(
  416. 'Please use MoveIntoFolder instead')(MoveIntoFolder)
  417. MoveFolderIntoFolder = atom.deprecated(
  418. 'Please use MoveIntoFolder instead')(MoveIntoFolder)
  419. class DocumentQuery(gdata.service.Query):
  420. """Object used to construct a URI to query the Google Document List feed"""
  421. def __init__(self, feed='/feeds/documents', visibility='private',
  422. projection='full', text_query=None, params=None,
  423. categories=None):
  424. """Constructor for Document List Query
  425. Args:
  426. feed: string (optional) The path for the feed. (e.g. '/feeds/documents')
  427. visibility: string (optional) The visibility chosen for the current feed.
  428. projection: string (optional) The projection chosen for the current feed.
  429. text_query: string (optional) The contents of the q query parameter. This
  430. string is URL escaped upon conversion to a URI.
  431. params: dict (optional) Parameter value string pairs which become URL
  432. params when translated to a URI. These parameters are added to
  433. the query's items.
  434. categories: list (optional) List of category strings which should be
  435. included as query categories. See gdata.service.Query for
  436. additional documentation.
  437. Yields:
  438. A DocumentQuery object used to construct a URI based on the Document
  439. List feed.
  440. """
  441. self.visibility = visibility
  442. self.projection = projection
  443. gdata.service.Query.__init__(self, feed, text_query, params, categories)
  444. def ToUri(self):
  445. """Generates a URI from the query parameters set in the object.
  446. Returns:
  447. A string containing the URI used to retrieve entries from the Document
  448. List feed.
  449. """
  450. old_feed = self.feed
  451. self.feed = '/'.join([old_feed, self.visibility, self.projection])
  452. new_feed = gdata.service.Query.ToUri(self)
  453. self.feed = old_feed
  454. return new_feed
  455. def AddNamedFolder(self, email, folder_name):
  456. """Adds a named folder category, qualified by a schema.
  457. This function lets you query for documents that are contained inside a
  458. named folder without fear of collision with other categories.
  459. Args:
  460. email: string The email of the user who owns the folder.
  461. folder_name: string The name of the folder.
  462. Returns:
  463. The string of the category that was added to the object.
  464. """
  465. category = '{%s%s}%s' % (FOLDERS_SCHEME_PREFIX, email, folder_name)
  466. self.categories.append(category)
  467. return category
  468. def RemoveNamedFolder(self, email, folder_name):
  469. """Removes a named folder category, qualified by a schema.
  470. Args:
  471. email: string The email of the user who owns the folder.
  472. folder_name: string The name of the folder.
  473. Returns:
  474. The string of the category that was removed to the object.
  475. """
  476. category = '{%s%s}%s' % (FOLDERS_SCHEME_PREFIX, email, folder_name)
  477. self.categories.remove(category)
  478. return category
  479. class DocumentAclQuery(gdata.service.Query):
  480. """Object used to construct a URI to query a Document's ACL feed"""
  481. def __init__(self, resource_id, feed='/feeds/acl/private/full'):
  482. """Constructor for Document ACL Query
  483. Args:
  484. resource_id: string The resource id. (e.g. 'document%3Adocument_id',
  485. 'spreadsheet%3Aspreadsheet_id', etc.)
  486. feed: string (optional) The path for the feed.
  487. (e.g. '/feeds/acl/private/full')
  488. Yields:
  489. A DocumentAclQuery object used to construct a URI based on the Document
  490. ACL feed.
  491. """
  492. self.resource_id = resource_id
  493. gdata.service.Query.__init__(self, feed)
  494. def ToUri(self):
  495. """Generates a URI from the query parameters set in the object.
  496. Returns:
  497. A string containing the URI used to retrieve entries from the Document
  498. ACL feed.
  499. """
  500. return '%s/%s' % (gdata.service.Query.ToUri(self), self.resource_id)