blob: 945d08abb78aa4e621155b16af9403a08428ea59 [file] [log] [blame]
Tarek Ziade1231a4e2011-05-19 13:07:25 +02001"""Convenient client for all PyPI APIs.
2
3This module provides a ClientWrapper class which will use the "simple"
4or XML-RPC API to request information or files from an index.
5"""
6
7from packaging.pypi import simple, xmlrpc
8
9_WRAPPER_MAPPINGS = {'get_release': 'simple',
10 'get_releases': 'simple',
11 'search_projects': 'simple',
12 'get_metadata': 'xmlrpc',
13 'get_distributions': 'simple'}
14
15_WRAPPER_INDEXES = {'xmlrpc': xmlrpc.Client,
16 'simple': simple.Crawler}
17
18
19def switch_index_if_fails(func, wrapper):
20 """Decorator that switch of index (for instance from xmlrpc to simple)
21 if the first mirror return an empty list or raises an exception.
22 """
23 def decorator(*args, **kwargs):
24 retry = True
25 exception = None
26 methods = [func]
27 for f in wrapper._indexes.values():
28 if f != func.__self__ and hasattr(f, func.__name__):
29 methods.append(getattr(f, func.__name__))
30 for method in methods:
31 try:
32 response = method(*args, **kwargs)
33 retry = False
34 except Exception as e:
35 exception = e
36 if not retry:
37 break
38 if retry and exception:
39 raise exception
40 else:
41 return response
42 return decorator
43
44
45class ClientWrapper:
46 """Wrapper around simple and xmlrpc clients,
47
48 Choose the best implementation to use depending the needs, using the given
49 mappings.
50 If one of the indexes returns an error, tries to use others indexes.
51
52 :param index: tell which index to rely on by default.
53 :param index_classes: a dict of name:class to use as indexes.
54 :param indexes: a dict of name:index already instantiated
55 :param mappings: the mappings to use for this wrapper
56 """
57
58 def __init__(self, default_index='simple', index_classes=_WRAPPER_INDEXES,
59 indexes={}, mappings=_WRAPPER_MAPPINGS):
60 self._projects = {}
61 self._mappings = mappings
62 self._indexes = indexes
63 self._default_index = default_index
64
65 # instantiate the classes and set their _project attribute to the one
66 # of the wrapper.
67 for name, cls in index_classes.items():
68 obj = self._indexes.setdefault(name, cls())
69 obj._projects = self._projects
70 obj._index = self
71
72 def __getattr__(self, method_name):
73 """When asking for methods of the wrapper, return the implementation of
74 the wrapped classes, depending the mapping.
75
76 Decorate the methods to switch of implementation if an error occurs
77 """
78 real_method = None
79 if method_name in _WRAPPER_MAPPINGS:
80 obj = self._indexes[_WRAPPER_MAPPINGS[method_name]]
81 real_method = getattr(obj, method_name)
82 else:
83 # the method is not defined in the mappings, so we try first to get
84 # it via the default index, and rely on others if needed.
85 try:
86 real_method = getattr(self._indexes[self._default_index],
87 method_name)
88 except AttributeError:
89 other_indexes = [i for i in self._indexes
90 if i != self._default_index]
91 for index in other_indexes:
92 real_method = getattr(self._indexes[index], method_name,
93 None)
94 if real_method:
95 break
96 if real_method:
97 return switch_index_if_fails(real_method, self)
98 else:
99 raise AttributeError("No index have attribute '%s'" % method_name)