~ubuntu-branches/ubuntu/karmic/calibre/karmic

« back to all changes in this revision

Viewing changes to src/calibre/ebooks/lrf/web/profiles/__init__.py

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2009-07-30 12:49:41 UTC
  • mfrom: (1.3.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20090730124941-qjdsmri25zt8zocn
Tags: 0.6.3+dfsg-0ubuntu1
* New upstream release. Please see http://calibre.kovidgoyal.net/new_in_6/
  for the list of new features and changes.
* remove_postinstall.patch: Update for new version.
* build_debug.patch: Does not apply any more, disable for now. Might not be
  necessary any more.
* debian/copyright: Fix reference to versionless GPL.
* debian/rules: Drop obsolete dh_desktop call.
* debian/rules: Add workaround for weird Python 2.6 setuptools behaviour of
  putting compiled .so files into src/calibre/plugins/calibre/plugins
  instead of src/calibre/plugins.
* debian/rules: Drop hal fdi moving, new upstream version does not use hal
  any more. Drop hal dependency, too.
* debian/rules: Install udev rules into /lib/udev/rules.d.
* Add debian/calibre.preinst: Remove unmodified
  /etc/udev/rules.d/95-calibre.rules on upgrade.
* debian/control: Bump Python dependencies to 2.6, since upstream needs
  it now.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
__license__   = 'GPL v3'
2
 
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
3
 
'''
4
 
Contains the Base Profiles that can be used to easily create profiles to download
5
 
particular websites.  
6
 
'''
7
 
 
8
 
import tempfile, time, calendar, re, operator, atexit, shutil, os
9
 
from htmlentitydefs import name2codepoint
10
 
from email.utils import formatdate
11
 
 
12
 
from calibre import __appname__, iswindows, browser, strftime
13
 
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, NavigableString, CData, Tag
14
 
 
15
 
 
16
 
class DefaultProfile(object):
17
 
    
18
 
    #: The title to use for the LRF file
19
 
    #: @type: string    
20
 
    title                 = 'Default Profile'    
21
 
    
22
 
    #: Maximum number of articles to download from each feed
23
 
    #: @type: integer
24
 
    max_articles_per_feed = 10     
25
 
    
26
 
    #: If True process the <description> element of the feed as HTML
27
 
    #: @type: boolean
28
 
    html_description      = True  
29
 
    
30
 
    #: How many days old should the oldest article downloaded from the feeds be
31
 
    #: @type: integer
32
 
    oldest_article        = 7
33
 
    
34
 
    #: Recommend frequency at which to download this profile. In days.
35
 
    recommended_frequency = 7
36
 
    
37
 
    #: Number of levels of links to follow
38
 
    #: @type: integer
39
 
    max_recursions        = 1
40
 
    
41
 
    #: Maximum number of files to download
42
 
    #: @type: integer
43
 
    max_files             = 3000
44
 
    
45
 
    #: Delay between consecutive downloads in seconds
46
 
    #: @type: integer
47
 
    delay                 = 0
48
 
    
49
 
    #: Timeout for fetching files from server in seconds
50
 
    #: @type: integer
51
 
    timeout               = 10
52
 
    
53
 
    #: The format string for the date shown on the first page
54
 
    #: @type: string
55
 
    timefmt               = ' [%a %d %b %Y]'
56
 
    
57
 
    #: The order of elements to search for a URL when parsing the RSS feed. You 
58
 
    #: can replace these elements by completely arbitrary elements to customize
59
 
    #: feed processing. 
60
 
    #: @type: list of strings
61
 
    url_search_order      = ['guid', 'link']
62
 
    
63
 
    #: The format string used to parse the publication date in the RSS feed. 
64
 
    #: If set to None some default heuristics are used, these may fail, 
65
 
    #: in which case set this to the correct string or re-implement 
66
 
    #: L{DefaultProfile.strptime} in your subclass.
67
 
    #: @type: string or None 
68
 
    pubdate_fmt           = None  
69
 
    
70
 
    #: If True will look for a publication date for each article. 
71
 
    #: If False assumes the publication date is the current time.
72
 
    #: @type: boolean
73
 
    use_pubdate           = True, 
74
 
    
75
 
    #: Max number of characters in the short description.
76
 
    #: Used by L{FullContentProfile}
77
 
    #: @type: integer
78
 
    summary_length        = 500
79
 
    
80
 
    #: If True stylesheets are not downloaded and processed
81
 
    #: Convenient flag to disable loading of stylesheets for websites
82
 
    #: that have overly complex stylesheets unsuitable for conversion
83
 
    #: to ebooks formats
84
 
    #: @type: boolean
85
 
    no_stylesheets        = False
86
 
    
87
 
    #: If False articles with the same title in the same feed 
88
 
    #: are not downloaded multiple times
89
 
    #: @type: boolean 
90
 
    allow_duplicates      = False 
91
 
    
92
 
    #: If True the GUI will ask the user for a username and password 
93
 
    #: to use while downloading
94
 
    #: @type: boolean
95
 
    needs_subscription    = False
96
 
    
97
 
    #: Specify an override encoding for sites that have an incorrect
98
 
    #: charset specification. THe most common being specifying latin1 and
99
 
    #: using cp1252 
100
 
    encoding = None
101
 
    
102
 
    #: List of regular expressions that determines which links to follow
103
 
    #: If empty, it is ignored.
104
 
    #: Only one of L{match_regexps} or L{filter_regexps} should be defined
105
 
    #: @type: list of strings
106
 
    match_regexps         = []
107
 
    
108
 
    #: List of regular expressions that determines which links to ignore
109
 
    #: If empty it is ignored
110
 
    #: Only one of L{match_regexps} or L{filter_regexps} should be defined
111
 
    #: @type: list of strings
112
 
    filter_regexps        = []    
113
 
    
114
 
    #: List of options to pass to html2lrf, to customize conversion
115
 
    #: to LRF
116
 
    #: @type: list of strings
117
 
    html2lrf_options   = []
118
 
        
119
 
    #: List of regexp substitution rules to run on the downloaded HTML. Each element of the 
120
 
    #: list should be a two element tuple. The first element of the tuple should
121
 
    #: be a compiled regular expression and the second a callable that takes
122
 
    #: a single match object and returns a string to replace the match.
123
 
    #: @type: list of tuples
124
 
    preprocess_regexps = []
125
 
    
126
 
    # See the built-in profiles for examples of these settings.
127
 
    
128
 
    #: The URL of the website
129
 
    #: @type: string
130
 
    url                   = ''
131
 
    
132
 
    feeds = []
133
 
    CDATA_PAT = re.compile(r'<\!\[CDATA\[(.*?)\]\]>', re.DOTALL)
134
 
 
135
 
    def get_feeds(self):
136
 
        '''
137
 
        Return a list of RSS feeds to fetch for this profile. Each element of the list
138
 
        must be a 2-element tuple of the form (title, url).
139
 
        '''
140
 
        if not self.feeds:
141
 
            raise NotImplementedError
142
 
        return self.feeds
143
 
    
144
 
    @classmethod
145
 
    def print_version(cls, url):
146
 
        '''
147
 
        Take a URL pointing to an article and returns the URL pointing to the
148
 
        print version of the article.
149
 
        '''
150
 
        return url
151
 
    
152
 
    @classmethod
153
 
    def get_browser(cls):
154
 
        '''
155
 
        Return a browser instance used to fetch documents from the web.
156
 
        
157
 
        If your profile requires that you login first, override this method
158
 
        in your subclass. See for example the nytimes profile.
159
 
        '''
160
 
        return browser()
161
 
    
162
 
    
163
 
    
164
 
    
165
 
    def __init__(self, logger, verbose=False, username=None, password=None, lrf=True):
166
 
        self.logger = logger
167
 
        self.username = username
168
 
        self.password = password
169
 
        self.verbose  = verbose
170
 
        self.lrf = lrf
171
 
        self.temp_dir = tempfile.mkdtemp(prefix=__appname__+'_')
172
 
        self.browser = self.get_browser()
173
 
        try:
174
 
            self.url = 'file:'+ ('' if iswindows else '//') + self.build_index()
175
 
        except NotImplementedError:
176
 
            self.url = None
177
 
        atexit.register(cleanup, self.temp_dir)
178
 
    
179
 
    def build_index(self):
180
 
        '''Build an RSS based index.html'''
181
 
        articles = self.parse_feeds()
182
 
        encoding = 'utf-8' if self.encoding is None else self.encoding 
183
 
        def build_sub_index(title, items):
184
 
            ilist = ''
185
 
            li = u'<li><a href="%(url)s">%(title)s</a> <span style="font-size: x-small">[%(date)s]</span><br/>\n'+\
186
 
                u'<div style="font-size:small; font-family:sans">%(description)s<br /></div></li>\n'
187
 
            for item in items:
188
 
                if not item.has_key('date'):
189
 
                    item['date'] = time.strftime('%a, %d %b', time.localtime())
190
 
                ilist += li%item
191
 
            return u'''\
192
 
            <html>
193
 
            <body>
194
 
            <h2>%(title)s</h2>
195
 
            <ul>
196
 
            %(items)s
197
 
            </ul>
198
 
            </body>
199
 
            </html>
200
 
            '''%dict(title=title, items=ilist.rstrip())        
201
 
        
202
 
        cnum = 0
203
 
        clist = ''
204
 
        categories = articles.keys()
205
 
        categories.sort()
206
 
        for category in categories:
207
 
            cnum  += 1
208
 
            cfile = os.path.join(self.temp_dir, 'category'+str(cnum)+'.html')
209
 
            prefix = 'file:' if iswindows else ''
210
 
            clist += u'<li><a href="%s">%s</a></li>\n'%(prefix+cfile, category)
211
 
            src = build_sub_index(category, articles[category])
212
 
            open(cfile, 'wb').write(src.encode(encoding))
213
 
                        
214
 
        title = self.title
215
 
        if not isinstance(title, unicode):
216
 
            title = unicode(title, 'utf-8', 'replace')
217
 
        src = u'''\
218
 
        <html>
219
 
        <body>
220
 
        <h1>%(title)s</h1>
221
 
        <div style='text-align: right; font-weight: bold'>%(date)s</div>
222
 
        <ul>
223
 
        %(categories)s
224
 
        </ul>
225
 
        </body>
226
 
        </html>
227
 
        '''%dict(date=strftime('%a, %d %B, %Y'), 
228
 
                 categories=clist, title=title)
229
 
        index = os.path.join(self.temp_dir, 'index.html')
230
 
        open(index, 'wb').write(src.encode(encoding))
231
 
        
232
 
        return index
233
 
 
234
 
    
235
 
    @classmethod
236
 
    def tag_to_string(cls, tag, use_alt=True):
237
 
        '''
238
 
        Convenience method to take a BeautifulSoup Tag and extract the text from it
239
 
        recursively, including any CDATA sections and alt tag attributes.
240
 
        @param use_alt: If True try to use the alt attribute for tags that don't have any textual content
241
 
        @type use_alt: boolean
242
 
        @return: A unicode (possibly empty) object
243
 
        @rtype: unicode string
244
 
        '''
245
 
        if not tag:
246
 
            return ''
247
 
        if isinstance(tag, basestring):
248
 
            return tag
249
 
        strings = []
250
 
        for item in tag.contents:
251
 
            if isinstance(item, (NavigableString, CData)):
252
 
                strings.append(item.string)
253
 
            elif isinstance(item, Tag):
254
 
                res = cls.tag_to_string(item)
255
 
                if res:
256
 
                    strings.append(res)
257
 
                elif use_alt and item.has_key('alt'):
258
 
                    strings.append(item['alt'])
259
 
        return u''.join(strings) 
260
 
    
261
 
    def get_article_url(self, item):
262
 
        '''
263
 
        Return the article URL given an item Tag from a feed, or None if no valid URL is found
264
 
        @type item: BeatifulSoup.Tag
265
 
        @param item: A BeautifulSoup Tag instance corresponding to the <item> tag from a feed.
266
 
        @rtype: string or None
267
 
        '''
268
 
        url = None
269
 
        for element in self.url_search_order:
270
 
            url = item.find(element.lower())
271
 
            if url:
272
 
                break
273
 
        return url
274
 
        
275
 
    
276
 
    def parse_feeds(self, require_url=True):
277
 
        '''
278
 
        Create list of articles from a list of feeds.
279
 
        @param require_url: If True skip articles that don't have a link to a HTML page with the full article contents.
280
 
        @type require_url: boolean
281
 
        @rtype: dictionary
282
 
        @return: A dictionary whose keys are feed titles and whose values are each
283
 
        a list of dictionaries. Each list contains dictionaries of the form::
284
 
            {
285
 
            'title'       : article title,
286
 
            'url'         : URL of print version,
287
 
            'date'        : The publication date of the article as a string,
288
 
            'description' : A summary of the article
289
 
            'content'     : The full article (can be an empty string). This is used by FullContentProfile
290
 
            }
291
 
        '''
292
 
        added_articles = {}
293
 
        feeds = self.get_feeds()
294
 
        articles = {}
295
 
        for title, url in feeds:
296
 
            try:
297
 
                src = self.browser.open(url).read()
298
 
            except Exception, err:
299
 
                self.logger.error('Could not fetch feed: %s\nError: %s'%(url, err))
300
 
                if self.verbose:
301
 
                    self.logger.exception(' ')
302
 
                continue
303
 
            
304
 
            articles[title] = []
305
 
            added_articles[title] = []
306
 
            soup = BeautifulStoneSoup(src)
307
 
            for item in soup.findAll('item'):
308
 
                try:
309
 
                    atitle = item.find('title')
310
 
                    if not atitle:
311
 
                        continue
312
 
                    
313
 
                    atitle = self.tag_to_string(atitle)
314
 
                    if self.use_pubdate:
315
 
                        pubdate = item.find('pubdate')
316
 
                        if not pubdate:
317
 
                            pubdate = item.find('dc:date')
318
 
                        if not pubdate or not pubdate.string:
319
 
                            pubdate = formatdate()
320
 
                        pubdate = self.tag_to_string(pubdate)
321
 
                        pubdate = pubdate.replace('+0000', 'GMT')
322
 
                    
323
 
                    
324
 
                    url = self.get_article_url(item)
325
 
                    url = self.tag_to_string(url)
326
 
                    if require_url and not url:
327
 
                        self.logger.debug('Skipping article %s as it does not have a link url'%atitle)
328
 
                        continue
329
 
                    purl = url
330
 
                    try:
331
 
                        purl = self.print_version(url)
332
 
                    except Exception, err:
333
 
                        self.logger.debug('Skipping %s as could not find URL for print version. Error:\n%s'%(url, err))
334
 
                        continue
335
 
                    
336
 
                    content = item.find('content:encoded')
337
 
                    if not content:
338
 
                        content = item.find('description')
339
 
                    if content:
340
 
                        content = self.process_html_description(content, strip_links=False)
341
 
                    else:
342
 
                        content = ''
343
 
                        
344
 
                    d = { 
345
 
                        'title'    : atitle,                 
346
 
                        'url'      : purl,
347
 
                        'timestamp': self.strptime(pubdate) if self.use_pubdate else time.time(),
348
 
                        'date'     : pubdate if self.use_pubdate else formatdate(),
349
 
                        'content'  : content,
350
 
                        }
351
 
                    delta = time.time() - d['timestamp']
352
 
                    if not self.allow_duplicates:
353
 
                        if d['title'] in added_articles[title]:
354
 
                            continue
355
 
                        added_articles[title].append(d['title'])
356
 
                    if delta > self.oldest_article*3600*24:
357
 
                        continue
358
 
                    
359
 
                except Exception, err:
360
 
                    if self.verbose:
361
 
                        self.logger.exception('Error parsing article:\n%s'%(item,))
362
 
                    continue
363
 
                try:
364
 
                    desc = ''
365
 
                    for c in item.findAll('description'):
366
 
                        desc = self.tag_to_string(c)
367
 
                        if desc:
368
 
                            break
369
 
                    d['description'] = self.process_html_description(desc) if  self.html_description else desc.string                    
370
 
                except:
371
 
                    d['description'] = ''
372
 
                articles[title].append(d)
373
 
            articles[title].sort(key=operator.itemgetter('timestamp'), reverse=True)
374
 
            articles[title] = articles[title][:self.max_articles_per_feed+1]
375
 
            #for item in articles[title]:
376
 
            #    item.pop('timestamp')
377
 
            if not articles[title]:
378
 
                articles.pop(title)
379
 
        return articles
380
 
 
381
 
    
382
 
    def cleanup(self):
383
 
        '''
384
 
        Called after LRF file has been generated. Use it to do any cleanup like 
385
 
        logging out of subscription sites, etc.
386
 
        '''
387
 
        pass
388
 
    
389
 
    @classmethod
390
 
    def process_html_description(cls, tag, strip_links=True):
391
 
        '''
392
 
        Process a <description> tag that contains HTML markup, either 
393
 
        entity encoded or escaped in a CDATA section. 
394
 
        @return: HTML
395
 
        @rtype: string
396
 
        '''
397
 
        src = '\n'.join(tag.contents) if hasattr(tag, 'contents') else tag
398
 
        match = cls.CDATA_PAT.match(src.lstrip())
399
 
        if match:
400
 
            src = match.group(1)
401
 
        else:
402
 
            replaced_entities = [ 'amp', 'lt', 'gt' , 'ldquo', 'rdquo', 'lsquo', 'rsquo' ]
403
 
            for e in replaced_entities:
404
 
                ent = '&'+e+';'
405
 
                src = src.replace(ent, unichr(name2codepoint[e]))
406
 
        if strip_links:
407
 
            src = re.compile(r'<a.*?>(.*?)</a>', re.IGNORECASE|re.DOTALL).sub(r'\1', src)
408
 
        
409
 
        return src 
410
 
 
411
 
    
412
 
    DAY_MAP        = dict(Sun=0, Mon=1, Tue=2, Wed=3, Thu=4, Fri=5, Sat=6)
413
 
    FULL_DAY_MAP   = dict(Sunday=0, Monday=1, Tueday=2, Wednesday=3, Thursday=4, Friday=5, Saturday=6) 
414
 
    MONTH_MAP      = dict(Jan=1, Feb=2, Mar=3, Apr=4, May=5, Jun=6, Jul=7, Aug=8, Sep=9, Oct=10, Nov=11, Dec=12)
415
 
    FULL_MONTH_MAP = dict(January=1, February=2, March=3, April=4, May=5, June=6, 
416
 
                      July=7, August=8, September=9, October=10, 
417
 
                      November=11, December=12)
418
 
        
419
 
    @classmethod
420
 
    def strptime(cls, src):
421
 
        ''' 
422
 
        Take a string and return the date that string represents, in UTC as
423
 
        an epoch (i.e. number of seconds since Jan 1, 1970). This function uses
424
 
        a bunch of heuristics and is a prime candidate for being overridden in a 
425
 
        subclass.
426
 
        @param src: Timestamp as a string
427
 
        @type src: string
428
 
        @return: time ans a epoch
429
 
        @rtype: number 
430
 
        '''        
431
 
        delta = 0
432
 
        zone = re.search(r'\s*(\+\d\d\:{0,1}\d\d)', src)
433
 
        if zone:
434
 
            delta = zone.group(1)
435
 
            hrs, mins = int(delta[1:3]), int(delta[-2:].rstrip())
436
 
            delta = 60*(hrs*60 + mins) * (-1 if delta.startswith('-') else 1)
437
 
            src = src.replace(zone.group(), '')
438
 
        if cls.pubdate_fmt is None:
439
 
            src = src.strip().split()
440
 
            try:
441
 
                src[0] = str(cls.DAY_MAP[src[0][:-1]])+','
442
 
            except KeyError:
443
 
                src[0] = str(cls.FULL_DAY_MAP[src[0][:-1]])+','
444
 
            try:
445
 
                src[2] = str(cls.MONTH_MAP[src[2]])
446
 
            except KeyError:
447
 
                src[2] = str(cls.FULL_MONTH_MAP[src[2]])
448
 
            fmt = '%w, %d %m %Y %H:%M:%S'
449
 
            src = src[:5] # Discard extra information
450
 
            try:
451
 
                time_t = time.strptime(' '.join(src), fmt)
452
 
            except ValueError:
453
 
                time_t = time.strptime(' '.join(src), fmt.replace('%Y', '%y'))
454
 
            return calendar.timegm(time_t)-delta
455
 
        else:
456
 
            return calendar.timegm(time.strptime(src, cls.pubdate_fmt))
457
 
    
458
 
    def command_line_options(self):
459
 
        args = []
460
 
        args.append('--max-recursions='+str(self.max_recursions))
461
 
        args.append('--delay='+str(self.delay))
462
 
        args.append('--max-files='+str(self.max_files))
463
 
        for i in self.match_regexps:
464
 
            args.append('--match-regexp="'+i+'"')
465
 
        for i in self.filter_regexps:
466
 
            args.append('--filter-regexp="'+i+'"')
467
 
        return args
468
 
        
469
 
    
470
 
class FullContentProfile(DefaultProfile):
471
 
    '''
472
 
    This profile is designed for feeds that embed the full article content in the RSS file.
473
 
    '''
474
 
    
475
 
    max_recursions = 0
476
 
    article_counter = 0
477
 
    
478
 
    
479
 
    def build_index(self):
480
 
        '''Build an RSS based index.html. '''
481
 
        articles = self.parse_feeds(require_url=False)
482
 
        
483
 
        def build_sub_index(title, items):
484
 
            ilist = ''
485
 
            li = u'<li><a href="%(url)s">%(title)s</a> <span style="font-size: x-small">[%(date)s]</span><br/>\n'+\
486
 
                u'<div style="font-size:small; font-family:sans">%(description)s<br /></div></li>\n'
487
 
            for item in items:
488
 
                content = item['content']
489
 
                if not content:
490
 
                    self.logger.debug('Skipping article as it has no content:%s'%item['title'])
491
 
                    continue
492
 
                item['description'] = cutoff(item['description'], self.summary_length)+'&hellip;'
493
 
                self.article_counter = self.article_counter + 1
494
 
                url = os.path.join(self.temp_dir, 'article%d.html'%self.article_counter)
495
 
                item['url'] = url
496
 
                open(url, 'wb').write((u'''\
497
 
                    <html>
498
 
                    <body>
499
 
                    <h2>%s</h2>
500
 
                    <div>
501
 
                    %s
502
 
                    </div>
503
 
                    </body>
504
 
                    </html>'''%(item['title'], content)).encode('utf-8')
505
 
                    )
506
 
                ilist += li%item
507
 
            return u'''\
508
 
            <html>
509
 
            <body>
510
 
            <h2>%(title)s</h2>
511
 
            <ul>
512
 
            %(items)s
513
 
            </ul>
514
 
            </body>
515
 
            </html>
516
 
            '''%dict(title=title, items=ilist.rstrip())        
517
 
        
518
 
        cnum = 0
519
 
        clist = ''
520
 
        categories = articles.keys()
521
 
        categories.sort()
522
 
        for category in categories:
523
 
            cnum  += 1
524
 
            cfile = os.path.join(self.temp_dir, 'category'+str(cnum)+'.html')
525
 
            prefix = 'file:' if iswindows else ''
526
 
            clist += u'<li><a href="%s">%s</a></li>\n'%(prefix+cfile, category)
527
 
            src = build_sub_index(category, articles[category])
528
 
            open(cfile, 'wb').write(src.encode('utf-8'))        
529
 
        
530
 
        src = '''\
531
 
        <html>
532
 
        <body>
533
 
        <h1>%(title)s</h1>
534
 
        <div style='text-align: right; font-weight: bold'>%(date)s</div>
535
 
        <ul>
536
 
        %(categories)s
537
 
        </ul>
538
 
        </body>
539
 
        </html>
540
 
        '''%dict(date=time.strftime('%a, %d %B, %Y', time.localtime()), 
541
 
                 categories=clist, title=self.title)
542
 
        index = os.path.join(self.temp_dir, 'index.html')
543
 
        open(index, 'wb').write(src.encode('utf-8'))
544
 
        return index
545
 
 
546
 
def cutoff(src, pos, fuzz=50):
547
 
    si = src.find(';', pos)
548
 
    if si > 0 and si-pos > fuzz:
549
 
        si = -1
550
 
    gi = src.find('>', pos)
551
 
    if gi > 0 and gi-pos > fuzz:
552
 
        gi = -1
553
 
    npos = max(si, gi)
554
 
    if npos < 0:
555
 
        npos = pos
556
 
    return src[:npos+1]
557
 
 
558
 
def create_class(src):
559
 
    environment = {'FullContentProfile':FullContentProfile, 'DefaultProfile':DefaultProfile}
560
 
    exec src in environment
561
 
    for item in environment.values():
562
 
        if hasattr(item, 'build_index'):
563
 
            if item.__name__ not in ['DefaultProfile', 'FullContentProfile']:
564
 
                return item
565
 
   
566
 
def cleanup(tdir):
567
 
    try:
568
 
        if os.path.isdir(tdir):
569
 
            shutil.rmtree(tdir)
570
 
    except:
571
 
        pass
572
 
    
 
 
b'\\ No newline at end of file'