PageRenderTime 69ms CodeModel.GetById 17ms app.highlight 43ms RepoModel.GetById 1ms app.codeStats 0ms

/gdata/books/service.py

http://radioappz.googlecode.com/
Python | 266 lines | 218 code | 9 blank | 39 comment | 0 complexity | 95b54f77c048a3032b160ca3e0e7a086 MD5 | raw file
  1#!/usr/bin/python
  2
  3"""
  4    Extend gdata.service.GDataService to support authenticated CRUD ops on 
  5    Books API
  6
  7    http://code.google.com/apis/books/docs/getting-started.html
  8    http://code.google.com/apis/books/docs/gdata/developers_guide_protocol.html
  9
 10    TODO: (here and __init__)
 11        * search based on label, review, or other annotations (possible?)
 12        * edit (specifically, Put requests) seem to fail effect a change
 13
 14    Problems With API:
 15        * Adding a book with a review to the library adds a note, not a review.
 16          This does not get included in the returned item. You see this by
 17          looking at My Library through the website.
 18        * Editing a review never edits a review (unless it is freshly added, but 
 19          see above). More generally,
 20        * a Put request with changed annotations (label/rating/review) does NOT
 21          change the data. Note: Put requests only work on the href from 
 22          GetEditLink (as per the spec). Do not try to PUT to the annotate or 
 23          library feeds, this will cause a 400 Invalid URI Bad Request response.
 24          Attempting to Post to one of the feeds with the updated annotations
 25          does not update them. See the following for (hopefully) a follow up:
 26          google.com/support/forum/p/booksearch-apis/thread?tid=27fd7f68de438fc8
 27        * Attempts to workaround the edit problem continue to fail. For example,
 28          removing the item, editing the data, readding the item, gives us only
 29          our originally added data (annotations). This occurs even if we
 30          completely shut python down, refetch the book from the public feed,
 31          and re-add it. There is some kind of persistence going on that I
 32          cannot change. This is likely due to the annotations being cached in
 33          the annotation feed and the inability to edit (see Put, above)
 34        * GetAnnotationLink has www.books.... as the server, but hitting www...
 35          results in a bad URI error.
 36        * Spec indicates there may be multiple labels, but there does not seem
 37          to be a way to get the server to accept multiple labels, nor does the
 38          web interface have an obvious way to have multiple labels. Multiple 
 39          labels are never returned.
 40"""
 41
 42__author__ = "James Sams <sams.james@gmail.com>"
 43__copyright__ = "Apache License v2.0"
 44
 45from shlex import split
 46
 47import gdata.service
 48try:
 49    import books
 50except ImportError:
 51    import gdata.books as books
 52
 53
 54BOOK_SERVER       = "books.google.com"
 55GENERAL_FEED      = "/books/feeds/volumes"
 56ITEM_FEED         = "/books/feeds/volumes/"
 57LIBRARY_FEED      = "/books/feeds/users/%s/collections/library/volumes"
 58ANNOTATION_FEED   = "/books/feeds/users/%s/volumes"
 59PARTNER_FEED      = "/books/feeds/p/%s/volumes"
 60BOOK_SERVICE      = "print"
 61ACCOUNT_TYPE      = "HOSTED_OR_GOOGLE"
 62
 63
 64class BookService(gdata.service.GDataService):
 65
 66    def __init__(self, email=None, password=None, source=None,
 67                server=BOOK_SERVER, account_type=ACCOUNT_TYPE,
 68                exception_handlers=tuple(), **kwargs):
 69        """source should be of form 'ProgramCompany - ProgramName - Version'"""
 70
 71        gdata.service.GDataService.__init__(self, email=email, 
 72                    password=password, service=BOOK_SERVICE, source=source,
 73                    server=server, **kwargs) 
 74        self.exception_handlers = exception_handlers
 75
 76    def search(self, q, start_index="1", max_results="10", 
 77                min_viewability="none", feed=GENERAL_FEED,
 78                converter=books.BookFeed.FromString):
 79        """
 80        Query the Public search feed. q is either a search string or a
 81        gdata.service.Query instance with a query set.
 82        
 83        min_viewability must be "none", "partial", or "full".
 84        
 85        If you change the feed to a single item feed, note that you will 
 86        probably need to change the converter to be Book.FromString
 87        """
 88
 89        if not isinstance(q, gdata.service.Query):
 90            q = gdata.service.Query(text_query=q)
 91        if feed:
 92            q.feed = feed
 93        q['start-index'] = start_index
 94        q['max-results'] = max_results
 95        q['min-viewability'] = min_viewability
 96        return self.Get(uri=q.ToUri(),converter=converter)
 97    
 98    def search_by_keyword(self, q='', feed=GENERAL_FEED, start_index="1",
 99                max_results="10", min_viewability="none", **kwargs):
100        """
101            Query the Public Search Feed by keyword. Non-keyword strings can be
102            set in q. This is quite fragile. Is there a function somewhere in
103            the Google library that will parse a query the same way that Google
104            does?
105
106            Legal Identifiers are listed below and correspond to their meaning
107            at http://books.google.com/advanced_book_search:
108                all_words 
109                exact_phrase 
110                at_least_one 
111                without_words 
112                title
113                author
114                publisher
115                subject
116                isbn
117                lccn
118                oclc
119                seemingly unsupported:
120                publication_date: a sequence of two, two tuples:
121                    ((min_month,min_year),(max_month,max_year))
122                    where month is one/two digit month, year is 4 digit, eg:
123                    (('1','2000'),('10','2003')). Lower bound is inclusive,
124                    upper bound is exclusive
125        """
126
127        for k, v in kwargs.items():
128            if not v:
129                continue
130            k = k.lower()
131            if k == 'all_words':
132                q = "%s %s" % (q, v)
133            elif k == 'exact_phrase':
134                q = '%s "%s"' % (q, v.strip('"'))
135            elif k == 'at_least_one':
136                q = '%s %s' % (q, ' '.join(['OR "%s"' % x for x in split(v)]))
137            elif k == 'without_words':
138                q = '%s %s' % (q, ' '.join(['-"%s"' % x for x in split(v)]))
139            elif k in ('author','title', 'publisher'):
140                q = '%s %s' % (q, ' '.join(['in%s:"%s"'%(k,x) for x in split(v)]))
141            elif k == 'subject':
142                q = '%s %s' % (q, ' '.join(['%s:"%s"' % (k,x) for x in split(v)]))
143            elif k == 'isbn':
144                q = '%s ISBN%s' % (q, v)
145            elif k == 'issn':
146                q = '%s ISSN%s' % (q,v)
147            elif k == 'oclc':
148                q = '%s OCLC%s' % (q,v)
149            else:
150                raise ValueError("Unsupported search keyword")
151        return self.search(q.strip(),start_index=start_index, feed=feed, 
152                            max_results=max_results, 
153                            min_viewability=min_viewability)
154    
155    def search_library(self, q, id='me', **kwargs):
156        """Like search, but in a library feed. Default is the authenticated
157        user's feed. Change by setting id."""
158
159        if 'feed' in kwargs:
160            raise ValueError("kwarg 'feed' conflicts with library_id")
161        feed = LIBRARY_FEED % id
162        return self.search(q, feed=feed, **kwargs)
163   
164    def search_library_by_keyword(self, id='me', **kwargs):
165        """Hybrid of search_by_keyword and search_library
166        """
167
168        if 'feed' in kwargs:
169            raise ValueError("kwarg 'feed' conflicts with library_id")
170        feed = LIBRARY_FEED % id
171        return self.search_by_keyword(feed=feed,**kwargs)
172    
173    def search_annotations(self, q, id='me', **kwargs): 
174        """Like search, but in an annotation feed. Default is the authenticated
175        user's feed. Change by setting id."""
176
177        if 'feed' in kwargs:
178            raise ValueError("kwarg 'feed' conflicts with library_id")
179        feed = ANNOTATION_FEED % id
180        return self.search(q, feed=feed, **kwargs)
181    
182    def search_annotations_by_keyword(self, id='me', **kwargs):
183        """Hybrid of search_by_keyword and search_annotations
184        """
185
186        if 'feed' in kwargs:
187            raise ValueError("kwarg 'feed' conflicts with library_id")
188        feed = ANNOTATION_FEED % id
189        return self.search_by_keyword(feed=feed,**kwargs)
190    
191    def add_item_to_library(self, item):
192        """Add the item, either an XML string or books.Book instance, to the 
193        user's library feed"""
194
195        feed = LIBRARY_FEED % 'me'
196        return self.Post(data=item, uri=feed, converter=books.Book.FromString)
197    
198    def remove_item_from_library(self, item):
199        """
200        Remove the item, a books.Book instance, from the authenticated user's 
201        library feed. Using an item retrieved from a public search will fail.
202        """
203
204        return self.Delete(item.GetEditLink().href)
205    
206    def add_annotation(self, item):
207        """
208        Add the item, either an XML string or books.Book instance, to the 
209        user's annotation feed.
210        """
211        # do not use GetAnnotationLink, results in 400 Bad URI due to www
212        return self.Post(data=item, uri=ANNOTATION_FEED % 'me', 
213                        converter=books.Book.FromString)
214    
215    def edit_annotation(self, item):
216        """
217        Send an edited item, a books.Book instance, to the user's annotation 
218        feed. Note that whereas extra annotations in add_annotations, minus 
219        ratings which are immutable once set, are simply added to the item in 
220        the annotation feed, if an annotation has been removed from the item, 
221        sending an edit request will remove that annotation. This should not 
222        happen with add_annotation.
223        """
224
225        return self.Put(data=item, uri=item.GetEditLink().href,
226                    converter=books.Book.FromString) 
227    
228    def get_by_google_id(self, id):
229        return self.Get(ITEM_FEED + id, converter=books.Book.FromString)
230    
231    def get_library(self, id='me',feed=LIBRARY_FEED, start_index="1", 
232                max_results="100", min_viewability="none",
233                converter=books.BookFeed.FromString):
234        """
235        Return a generator object that will return gbook.Book instances until
236        the search feed no longer returns an item from the GetNextLink method.
237        Thus max_results is not the maximum number of items that will be
238        returned, but rather the number of items per page of searches. This has
239        been set high to reduce the required number of network requests.
240        """
241
242        q = gdata.service.Query()
243        q.feed = feed % id
244        q['start-index'] = start_index
245        q['max-results'] = max_results
246        q['min-viewability'] = min_viewability
247        x = self.Get(uri=q.ToUri(), converter=converter)
248        while 1:
249            for entry in x.entry:
250                yield entry
251            else:
252                l = x.GetNextLink()
253                if l: # hope the server preserves our preferences
254                    x = self.Get(uri=l.href, converter=converter)
255                else:
256                    break
257
258    def get_annotations(self, id='me', start_index="1", max_results="100",
259                min_viewability="none", converter=books.BookFeed.FromString):
260        """
261        Like get_library, but for the annotation feed
262        """
263
264        return self.get_library(id=id, feed=ANNOTATION_FEED,
265                    max_results=max_results, min_viewability = min_viewability,
266                    converter=converter)