~romaimperator/keryx/devel

« back to all changes in this revision

Viewing changes to libkeryx/urlgrabber/mirror.py

  • Committer: Chris Oliver
  • Date: 2009-09-08 03:34:12 UTC
  • Revision ID: excid3@gmail.com-20090908033412-uaml2aaxcxxa7ryp
Added urlgrabber to libkeryx

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#   This library is free software; you can redistribute it and/or
 
2
#   modify it under the terms of the GNU Lesser General Public
 
3
#   License as published by the Free Software Foundation; either
 
4
#   version 2.1 of the License, or (at your option) any later version.
 
5
#
 
6
#   This library is distributed in the hope that it will be useful,
 
7
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
 
8
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
9
#   Lesser General Public License for more details.
 
10
#
 
11
#   You should have received a copy of the GNU Lesser General Public
 
12
#   License along with this library; if not, write to the 
 
13
#      Free Software Foundation, Inc., 
 
14
#      59 Temple Place, Suite 330, 
 
15
#      Boston, MA  02111-1307  USA
 
16
 
 
17
# This file is part of urlgrabber, a high-level cross-protocol url-grabber
 
18
# Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
 
19
 
 
20
"""Module for downloading files from a pool of mirrors
 
21
 
 
22
DESCRIPTION
 
23
 
 
24
  This module provides support for downloading files from a pool of
 
25
  mirrors with configurable failover policies.  To a large extent, the
 
26
  failover policy is chosen by using different classes derived from
 
27
  the main class, MirrorGroup.
 
28
 
 
29
  Instances of MirrorGroup (and cousins) act very much like URLGrabber
 
30
  instances in that they have urlread, urlgrab, and urlopen methods.
 
31
  They can therefore, be used in very similar ways.
 
32
 
 
33
    from urlgrabber.grabber import URLGrabber
 
34
    from urlgrabber.mirror import MirrorGroup
 
35
    gr = URLGrabber()
 
36
    mg = MirrorGroup(gr, ['http://foo.com/some/directory/',
 
37
                          'http://bar.org/maybe/somewhere/else/',
 
38
                          'ftp://baz.net/some/other/place/entirely/']
 
39
    mg.urlgrab('relative/path.zip')
 
40
 
 
41
  The assumption is that all mirrors are identical AFTER the base urls
 
42
  specified, so that any mirror can be used to fetch any file.
 
43
 
 
44
FAILOVER
 
45
 
 
46
  The failover mechanism is designed to be customized by subclassing
 
47
  from MirrorGroup to change the details of the behavior.  In general,
 
48
  the classes maintain a master mirror list and a "current mirror"
 
49
  index.  When a download is initiated, a copy of this list and index
 
50
  is created for that download only.  The specific failover policy
 
51
  depends on the class used, and so is documented in the class
 
52
  documentation.  Note that ANY behavior of the class can be
 
53
  overridden, so any failover policy at all is possible (although
 
54
  you may need to change the interface in extreme cases).
 
55
 
 
56
CUSTOMIZATION
 
57
 
 
58
  Most customization of a MirrorGroup object is done at instantiation
 
59
  time (or via subclassing).  There are four major types of
 
60
  customization:
 
61
 
 
62
    1) Pass in a custom urlgrabber - The passed in urlgrabber will be
 
63
       used (by default... see #2) for the grabs, so options to it
 
64
       apply for the url-fetching
 
65
 
 
66
    2) Custom mirror list - Mirror lists can simply be a list of
 
67
       stings mirrors (as shown in the example above) but each can
 
68
       also be a dict, allowing for more options.  For example, the
 
69
       first mirror in the list above could also have been:
 
70
 
 
71
         {'mirror': 'http://foo.com/some/directory/',
 
72
          'grabber': <a custom grabber to be used for this mirror>,
 
73
          'kwargs': { <a dict of arguments passed to the grabber> }}
 
74
 
 
75
       All mirrors are converted to this format internally.  If
 
76
       'grabber' is omitted, the default grabber will be used.  If
 
77
       kwargs are omitted, then (duh) they will not be used.
 
78
 
 
79
    3) Pass keyword arguments when instantiating the mirror group.
 
80
       See, for example, the failure_callback argument.
 
81
 
 
82
    4) Finally, any kwargs passed in for the specific file (to the
 
83
       urlgrab method, for example) will be folded in.  The options
 
84
       passed into the grabber's urlXXX methods will override any
 
85
       options specified in a custom mirror dict.
 
86
 
 
87
"""
 
88
 
 
89
# $Id: mirror.py,v 1.14 2006/02/22 18:26:46 mstenner Exp $
 
90
 
 
91
import random
 
92
import thread  # needed for locking to make this threadsafe
 
93
 
 
94
from grabber import URLGrabError, CallbackObject, DEBUG
 
95
 
 
96
try:
 
97
    from i18n import _
 
98
except ImportError, msg:
 
99
    def _(st): return st
 
100
 
 
101
class GrabRequest:
 
102
    """This is a dummy class used to hold information about the specific
 
103
    request.  For example, a single file.  By maintaining this information
 
104
    separately, we can accomplish two things:
 
105
 
 
106
      1) make it a little easier to be threadsafe
 
107
      2) have request-specific parameters
 
108
    """
 
109
    pass
 
110
 
 
111
class MirrorGroup:
 
112
    """Base Mirror class
 
113
 
 
114
    Instances of this class are built with a grabber object and a list
 
115
    of mirrors.  Then all calls to urlXXX should be passed relative urls.
 
116
    The requested file will be searched for on the first mirror.  If the
 
117
    grabber raises an exception (possibly after some retries) then that
 
118
    mirror will be removed from the list, and the next will be attempted.
 
119
    If all mirrors are exhausted, then an exception will be raised.
 
120
 
 
121
    MirrorGroup has the following failover policy:
 
122
 
 
123
      * downloads begin with the first mirror
 
124
 
 
125
      * by default (see default_action below) a failure (after retries)
 
126
        causes it to increment the local AND master indices.  Also,
 
127
        the current mirror is removed from the local list (but NOT the
 
128
        master list - the mirror can potentially be used for other
 
129
        files)
 
130
 
 
131
      * if the local list is ever exhausted, a URLGrabError will be
 
132
        raised (errno=256, no more mirrors)
 
133
 
 
134
    OPTIONS
 
135
 
 
136
      In addition to the required arguments "grabber" and "mirrors",
 
137
      MirrorGroup also takes the following optional arguments:
 
138
      
 
139
      default_action
 
140
 
 
141
        A dict that describes the actions to be taken upon failure
 
142
        (after retries).  default_action can contain any of the
 
143
        following keys (shown here with their default values):
 
144
 
 
145
          default_action = {'increment': 1,
 
146
                            'increment_master': 1,
 
147
                            'remove': 1,
 
148
                            'remove_master': 0,
 
149
                            'fail': 0}
 
150
 
 
151
        In this context, 'increment' means "use the next mirror" and
 
152
        'remove' means "never use this mirror again".  The two
 
153
        'master' values refer to the instance-level mirror list (used
 
154
        for all files), whereas the non-master values refer to the
 
155
        current download only.
 
156
 
 
157
        The 'fail' option will cause immediate failure by re-raising
 
158
        the exception and no further attempts to get the current
 
159
        download.
 
160
 
 
161
        This dict can be set at instantiation time,
 
162
          mg = MirrorGroup(grabber, mirrors, default_action={'fail':1})
 
163
        at method-execution time (only applies to current fetch),
 
164
          filename = mg.urlgrab(url, default_action={'increment': 0})
 
165
        or by returning an action dict from the failure_callback
 
166
          return {'fail':0}
 
167
        in increasing precedence.
 
168
        
 
169
        If all three of these were done, the net result would be:
 
170
              {'increment': 0,         # set in method
 
171
               'increment_master': 1,  # class default
 
172
               'remove': 1,            # class default
 
173
               'remove_master': 0,     # class default
 
174
               'fail': 0}              # set at instantiation, reset
 
175
                                       # from callback
 
176
 
 
177
      failure_callback
 
178
 
 
179
        this is a callback that will be called when a mirror "fails",
 
180
        meaning the grabber raises some URLGrabError.  If this is a
 
181
        tuple, it is interpreted to be of the form (cb, args, kwargs)
 
182
        where cb is the actual callable object (function, method,
 
183
        etc).  Otherwise, it is assumed to be the callable object
 
184
        itself.  The callback will be passed a grabber.CallbackObject
 
185
        instance along with args and kwargs (if present).  The following
 
186
        attributes are defined withing the instance:
 
187
 
 
188
           obj.exception    = < exception that was raised >
 
189
           obj.mirror       = < the mirror that was tried >
 
190
           obj.relative_url = < url relative to the mirror >
 
191
           obj.url          = < full url that failed >
 
192
                              # .url is just the combination of .mirror
 
193
                              # and .relative_url
 
194
 
 
195
        The failure callback can return an action dict, as described
 
196
        above.
 
197
 
 
198
        Like default_action, the failure_callback can be set at
 
199
        instantiation time or when the urlXXX method is called.  In
 
200
        the latter case, it applies only for that fetch.
 
201
 
 
202
        The callback can re-raise the exception quite easily.  For
 
203
        example, this is a perfectly adequate callback function:
 
204
 
 
205
          def callback(obj): raise obj.exception
 
206
 
 
207
        WARNING: do not save the exception object (or the
 
208
        CallbackObject instance).  As they contain stack frame
 
209
        references, they can lead to circular references.
 
210
 
 
211
    Notes:
 
212
      * The behavior can be customized by deriving and overriding the
 
213
        'CONFIGURATION METHODS'
 
214
      * The 'grabber' instance is kept as a reference, not copied.
 
215
        Therefore, the grabber instance can be modified externally
 
216
        and changes will take effect immediately.
 
217
    """
 
218
 
 
219
    # notes on thread-safety:
 
220
 
 
221
    #   A GrabRequest should never be shared by multiple threads because
 
222
    #   it's never saved inside the MG object and never returned outside it.
 
223
    #   therefore, it should be safe to access/modify grabrequest data
 
224
    #   without a lock.  However, accessing the mirrors and _next attributes
 
225
    #   of the MG itself must be done when locked to prevent (for example)
 
226
    #   removal of the wrong mirror.
 
227
 
 
228
    ##############################################################
 
229
    #  CONFIGURATION METHODS  -  intended to be overridden to
 
230
    #                            customize behavior
 
231
    def __init__(self, grabber, mirrors, **kwargs):
 
232
        """Initialize the MirrorGroup object.
 
233
 
 
234
        REQUIRED ARGUMENTS
 
235
 
 
236
          grabber  - URLGrabber instance
 
237
          mirrors  - a list of mirrors
 
238
 
 
239
        OPTIONAL ARGUMENTS
 
240
 
 
241
          failure_callback  - callback to be used when a mirror fails
 
242
          default_action    - dict of failure actions
 
243
 
 
244
        See the module-level and class level documentation for more
 
245
        details.
 
246
        """
 
247
 
 
248
        # OVERRIDE IDEAS:
 
249
        #   shuffle the list to randomize order
 
250
        self.grabber = grabber
 
251
        self.mirrors = self._parse_mirrors(mirrors)
 
252
        self._next = 0
 
253
        self._lock = thread.allocate_lock()
 
254
        self.default_action = None
 
255
        self._process_kwargs(kwargs)
 
256
 
 
257
    # if these values are found in **kwargs passed to one of the urlXXX
 
258
    # methods, they will be stripped before getting passed on to the
 
259
    # grabber
 
260
    options = ['default_action', 'failure_callback']
 
261
    
 
262
    def _process_kwargs(self, kwargs):
 
263
        self.failure_callback = kwargs.get('failure_callback')
 
264
        self.default_action   = kwargs.get('default_action')
 
265
       
 
266
    def _parse_mirrors(self, mirrors):
 
267
        parsed_mirrors = []
 
268
        for m in mirrors:
 
269
            if type(m) == type(''): m = {'mirror': m}
 
270
            parsed_mirrors.append(m)
 
271
        return parsed_mirrors
 
272
    
 
273
    def _load_gr(self, gr):
 
274
        # OVERRIDE IDEAS:
 
275
        #   shuffle gr list
 
276
        self._lock.acquire()
 
277
        gr.mirrors = list(self.mirrors)
 
278
        gr._next = self._next
 
279
        self._lock.release()
 
280
 
 
281
    def _get_mirror(self, gr):
 
282
        # OVERRIDE IDEAS:
 
283
        #   return a random mirror so that multiple mirrors get used
 
284
        #   even without failures.
 
285
        if not gr.mirrors:
 
286
            raise URLGrabError(256, _('No more mirrors to try.'))
 
287
        return gr.mirrors[gr._next]
 
288
 
 
289
    def _failure(self, gr, cb_obj):
 
290
        # OVERRIDE IDEAS:
 
291
        #   inspect the error - remove=1 for 404, remove=2 for connection
 
292
        #                       refused, etc. (this can also be done via
 
293
        #                       the callback)
 
294
        cb = gr.kw.get('failure_callback') or self.failure_callback
 
295
        if cb:
 
296
            if type(cb) == type( () ):
 
297
                cb, args, kwargs = cb
 
298
            else:
 
299
                args, kwargs = (), {}
 
300
            action = cb(cb_obj, *args, **kwargs) or {}
 
301
        else:
 
302
            action = {}
 
303
        # XXXX - decide - there are two ways to do this
 
304
        # the first is action-overriding as a whole - use the entire action
 
305
        # or fall back on module level defaults
 
306
        #action = action or gr.kw.get('default_action') or self.default_action
 
307
        # the other is to fall through for each element in the action dict
 
308
        a = dict(self.default_action or {})
 
309
        a.update(gr.kw.get('default_action', {}))
 
310
        a.update(action)
 
311
        action = a
 
312
        self.increment_mirror(gr, action)
 
313
        if action and action.get('fail', 0): raise
 
314
 
 
315
    def increment_mirror(self, gr, action={}):
 
316
        """Tell the mirror object increment the mirror index
 
317
 
 
318
        This increments the mirror index, which amounts to telling the
 
319
        mirror object to use a different mirror (for this and future
 
320
        downloads).
 
321
 
 
322
        This is a SEMI-public method.  It will be called internally,
 
323
        and you may never need to call it.  However, it is provided
 
324
        (and is made public) so that the calling program can increment
 
325
        the mirror choice for methods like urlopen.  For example, with
 
326
        urlopen, there's no good way for the mirror group to know that
 
327
        an error occurs mid-download (it's already returned and given
 
328
        you the file object).
 
329
        
 
330
        remove  ---  can have several values
 
331
           0   do not remove the mirror from the list
 
332
           1   remove the mirror for this download only
 
333
           2   remove the mirror permanently
 
334
 
 
335
        beware of remove=0 as it can lead to infinite loops
 
336
        """
 
337
        badmirror = gr.mirrors[gr._next]
 
338
 
 
339
        self._lock.acquire()
 
340
        try:
 
341
            ind = self.mirrors.index(badmirror)
 
342
        except ValueError:
 
343
            pass
 
344
        else:
 
345
            if action.get('remove_master', 0):
 
346
                del self.mirrors[ind]
 
347
            elif self._next == ind and action.get('increment_master', 1):
 
348
                self._next += 1
 
349
            if self._next >= len(self.mirrors): self._next = 0
 
350
        self._lock.release()
 
351
        
 
352
        if action.get('remove', 1):
 
353
            del gr.mirrors[gr._next]
 
354
        elif action.get('increment', 1):
 
355
            gr._next += 1
 
356
        if gr._next >= len(gr.mirrors): gr._next = 0
 
357
 
 
358
        if DEBUG:
 
359
            grm = [m['mirror'] for m in gr.mirrors]
 
360
            DEBUG.info('GR   mirrors: [%s] %i', ' '.join(grm), gr._next)
 
361
            selfm = [m['mirror'] for m in self.mirrors]
 
362
            DEBUG.info('MAIN mirrors: [%s] %i', ' '.join(selfm), self._next)
 
363
 
 
364
    #####################################################################
 
365
    # NON-CONFIGURATION METHODS
 
366
    # these methods are designed to be largely workhorse methods that
 
367
    # are not intended to be overridden.  That doesn't mean you can't;
 
368
    # if you want to, feel free, but most things can be done by
 
369
    # by overriding the configuration methods :)
 
370
 
 
371
    def _join_url(self, base_url, rel_url):
 
372
        if base_url.endswith('/') or rel_url.startswith('/'):
 
373
            return base_url + rel_url
 
374
        else:
 
375
            return base_url + '/' + rel_url
 
376
        
 
377
    def _mirror_try(self, func, url, kw):
 
378
        gr = GrabRequest()
 
379
        gr.func = func
 
380
        gr.url  = url
 
381
        gr.kw   = dict(kw)
 
382
        self._load_gr(gr)
 
383
 
 
384
        for k in self.options:
 
385
            try: del kw[k]
 
386
            except KeyError: pass
 
387
 
 
388
        while 1:
 
389
            mirrorchoice = self._get_mirror(gr)
 
390
            fullurl = self._join_url(mirrorchoice['mirror'], gr.url)
 
391
            kwargs = dict(mirrorchoice.get('kwargs', {}))
 
392
            kwargs.update(kw)
 
393
            grabber = mirrorchoice.get('grabber') or self.grabber
 
394
            func_ref = getattr(grabber, func)
 
395
            if DEBUG: DEBUG.info('MIRROR: trying %s -> %s', url, fullurl)
 
396
            try:
 
397
                return func_ref( *(fullurl,), **kwargs )
 
398
            except URLGrabError, e:
 
399
                if DEBUG: DEBUG.info('MIRROR: failed')
 
400
                obj = CallbackObject()
 
401
                obj.exception = e
 
402
                obj.mirror = mirrorchoice['mirror']
 
403
                obj.relative_url = gr.url
 
404
                obj.url = fullurl
 
405
                self._failure(gr, obj)
 
406
 
 
407
    def urlgrab(self, url, filename=None, **kwargs):
 
408
        kw = dict(kwargs)
 
409
        kw['filename'] = filename
 
410
        func = 'urlgrab'
 
411
        return self._mirror_try(func, url, kw)
 
412
    
 
413
    def urlopen(self, url, **kwargs):
 
414
        kw = dict(kwargs)
 
415
        func = 'urlopen'
 
416
        return self._mirror_try(func, url, kw)
 
417
 
 
418
    def urlread(self, url, limit=None, **kwargs):
 
419
        kw = dict(kwargs)
 
420
        kw['limit'] = limit
 
421
        func = 'urlread'
 
422
        return self._mirror_try(func, url, kw)
 
423
            
 
424
 
 
425
class MGRandomStart(MirrorGroup):
 
426
    """A mirror group that starts at a random mirror in the list.
 
427
 
 
428
    This behavior of this class is identical to MirrorGroup, except that
 
429
    it starts at a random location in the mirror list.
 
430
    """
 
431
 
 
432
    def __init__(self, grabber, mirrors, **kwargs):
 
433
        """Initialize the object
 
434
 
 
435
        The arguments for intialization are the same as for MirrorGroup
 
436
        """
 
437
        MirrorGroup.__init__(self, grabber, mirrors, **kwargs)
 
438
        self._next = random.randrange(len(mirrors))
 
439
 
 
440
class MGRandomOrder(MirrorGroup):
 
441
    """A mirror group that uses mirrors in a random order.
 
442
 
 
443
    This behavior of this class is identical to MirrorGroup, except that
 
444
    it uses the mirrors in a random order.  Note that the order is set at
 
445
    initialization time and fixed thereafter.  That is, it does not pick a
 
446
    random mirror after each failure.
 
447
    """
 
448
 
 
449
    def __init__(self, grabber, mirrors, **kwargs):
 
450
        """Initialize the object
 
451
 
 
452
        The arguments for intialization are the same as for MirrorGroup
 
453
        """
 
454
        MirrorGroup.__init__(self, grabber, mirrors, **kwargs)
 
455
        random.shuffle(self.mirrors)
 
456
 
 
457
if __name__ == '__main__':
 
458
    pass