100 lines
3.7 KiB
Python
100 lines
3.7 KiB
Python
"""Convenient client for all PyPI APIs.
|
|
|
|
This module provides a ClientWrapper class which will use the "simple"
|
|
or XML-RPC API to request information or files from an index.
|
|
"""
|
|
|
|
from packaging.pypi import simple, xmlrpc
|
|
|
|
_WRAPPER_MAPPINGS = {'get_release': 'simple',
|
|
'get_releases': 'simple',
|
|
'search_projects': 'simple',
|
|
'get_metadata': 'xmlrpc',
|
|
'get_distributions': 'simple'}
|
|
|
|
_WRAPPER_INDEXES = {'xmlrpc': xmlrpc.Client,
|
|
'simple': simple.Crawler}
|
|
|
|
|
|
def switch_index_if_fails(func, wrapper):
|
|
"""Decorator that switch of index (for instance from xmlrpc to simple)
|
|
if the first mirror return an empty list or raises an exception.
|
|
"""
|
|
def decorator(*args, **kwargs):
|
|
retry = True
|
|
exception = None
|
|
methods = [func]
|
|
for f in wrapper._indexes.values():
|
|
if f != func.__self__ and hasattr(f, func.__name__):
|
|
methods.append(getattr(f, func.__name__))
|
|
for method in methods:
|
|
try:
|
|
response = method(*args, **kwargs)
|
|
retry = False
|
|
except Exception as e:
|
|
exception = e
|
|
if not retry:
|
|
break
|
|
if retry and exception:
|
|
raise exception
|
|
else:
|
|
return response
|
|
return decorator
|
|
|
|
|
|
class ClientWrapper:
|
|
"""Wrapper around simple and xmlrpc clients,
|
|
|
|
Choose the best implementation to use depending the needs, using the given
|
|
mappings.
|
|
If one of the indexes returns an error, tries to use others indexes.
|
|
|
|
:param index: tell which index to rely on by default.
|
|
:param index_classes: a dict of name:class to use as indexes.
|
|
:param indexes: a dict of name:index already instantiated
|
|
:param mappings: the mappings to use for this wrapper
|
|
"""
|
|
|
|
def __init__(self, default_index='simple', index_classes=_WRAPPER_INDEXES,
|
|
indexes={}, mappings=_WRAPPER_MAPPINGS):
|
|
self._projects = {}
|
|
self._mappings = mappings
|
|
self._indexes = indexes
|
|
self._default_index = default_index
|
|
|
|
# instantiate the classes and set their _project attribute to the one
|
|
# of the wrapper.
|
|
for name, cls in index_classes.items():
|
|
obj = self._indexes.setdefault(name, cls())
|
|
obj._projects = self._projects
|
|
obj._index = self
|
|
|
|
def __getattr__(self, method_name):
|
|
"""When asking for methods of the wrapper, return the implementation of
|
|
the wrapped classes, depending the mapping.
|
|
|
|
Decorate the methods to switch of implementation if an error occurs
|
|
"""
|
|
real_method = None
|
|
if method_name in _WRAPPER_MAPPINGS:
|
|
obj = self._indexes[_WRAPPER_MAPPINGS[method_name]]
|
|
real_method = getattr(obj, method_name)
|
|
else:
|
|
# the method is not defined in the mappings, so we try first to get
|
|
# it via the default index, and rely on others if needed.
|
|
try:
|
|
real_method = getattr(self._indexes[self._default_index],
|
|
method_name)
|
|
except AttributeError:
|
|
other_indexes = [i for i in self._indexes
|
|
if i != self._default_index]
|
|
for index in other_indexes:
|
|
real_method = getattr(self._indexes[index], method_name,
|
|
None)
|
|
if real_method:
|
|
break
|
|
if real_method:
|
|
return switch_index_if_fails(real_method, self)
|
|
else:
|
|
raise AttributeError("No index have attribute '%s'" % method_name)
|