295 lines
9.4 KiB
Python
295 lines
9.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright(C) 2010-2011 Romain Bignon
|
|
#
|
|
# This file is part of weboob.
|
|
#
|
|
# weboob is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# weboob is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
import os
|
|
from threading import RLock
|
|
from copy import copy
|
|
|
|
from weboob.capabilities.base import CapBaseObject, FieldNotFound, IBaseCap, NotLoaded
|
|
from weboob.tools.misc import iter_fields
|
|
from weboob.tools.log import getLogger
|
|
from weboob.tools.value import ValuesDict
|
|
|
|
|
|
__all__ = ['BaseBackend', 'ObjectNotAvailable']
|
|
|
|
|
|
class ObjectNotAvailable(Exception):
|
|
pass
|
|
|
|
|
|
class BackendStorage(object):
|
|
def __init__(self, name, storage):
|
|
self.name = name
|
|
self.storage = storage
|
|
|
|
def set(self, *args):
|
|
if self.storage:
|
|
return self.storage.set('backends', self.name, *args)
|
|
|
|
def delete(self, *args):
|
|
if self.storage:
|
|
return self.storage.delete('backends', self.name, *args)
|
|
|
|
def get(self, *args, **kwargs):
|
|
if self.storage:
|
|
return self.storage.get('backends', self.name, *args, **kwargs)
|
|
else:
|
|
return kwargs.get('default', None)
|
|
|
|
def load(self, default):
|
|
if self.storage:
|
|
return self.storage.load('backends', self.name, default)
|
|
|
|
def save(self):
|
|
if self.storage:
|
|
return self.storage.save('backends', self.name)
|
|
|
|
class BackendConfig(ValuesDict):
|
|
modname = None
|
|
instname = None
|
|
weboob = None
|
|
|
|
def load(self, weboob, modname, instname, config, nofail=False):
|
|
"""
|
|
Load configuration from dict to create an instance.
|
|
|
|
@param weboob [Weboob] weboob object
|
|
@param modname [str] name of module
|
|
@param instname [str] name of instance of this backend
|
|
@param params [dict] parameters to load
|
|
@param nofail [bool] if true, this call can't fail.
|
|
@return [BackendConfig]
|
|
"""
|
|
cfg = BackendConfig()
|
|
cfg.modname = modname
|
|
cfg.instname = instname
|
|
cfg.weboob = weboob
|
|
for name, field in self.iteritems():
|
|
value = config.get(name, None)
|
|
|
|
if value is None:
|
|
if not nofail and field.required:
|
|
raise BaseBackend.ConfigError('Backend(%s): Configuration error: Missing parameter "%s" (%s)'
|
|
% (cfg.instname, name, field.description))
|
|
value = field.default
|
|
|
|
field = copy(field)
|
|
try:
|
|
field.load(cfg.instname, value, cfg.weboob.callbacks)
|
|
except ValueError, v:
|
|
if not nofail:
|
|
raise BaseBackend.ConfigError('Backend(%s): Configuration error for field "%s": %s' % (cfg.instname, name, v))
|
|
|
|
cfg[name] = field
|
|
return cfg
|
|
|
|
def dump(self):
|
|
settings = {}
|
|
for name, value in self.iteritems():
|
|
settings[name] = value.dump()
|
|
return settings
|
|
|
|
def save(self, edit=True, params=None):
|
|
assert self.modname is not None
|
|
assert self.instname is not None
|
|
assert self.weboob is not None
|
|
|
|
dump = self.dump()
|
|
if params is not None:
|
|
dump.update(params)
|
|
|
|
self.weboob.backends_config.add_backend(self.instname, self.modname, dump, edit)
|
|
|
|
class BaseBackend(object):
|
|
# Backend name.
|
|
NAME = None
|
|
# Name of the maintainer of this backend.
|
|
MAINTAINER = '<unspecified>'
|
|
# Email address of the maintainer.
|
|
EMAIL = '<unspecified>'
|
|
# Version of backend (for information only).
|
|
VERSION = '<unspecified>'
|
|
# Description
|
|
DESCRIPTION = '<unspecified>'
|
|
# License of this backend.
|
|
LICENSE = '<unspecified>'
|
|
# Configuration required for this backend.
|
|
# Values must be weboob.tools.value.Value objects.
|
|
CONFIG = BackendConfig()
|
|
# Storage
|
|
STORAGE = {}
|
|
# Browser class
|
|
BROWSER = None
|
|
# URL to an optional icon.
|
|
# If you want to create your own icon, create a 'favicon.ico' ico in
|
|
# the module's directory, and keep the ICON value to None.
|
|
ICON = None
|
|
# Supported objects to fill
|
|
# The key is the class and the value the method to call to fill
|
|
# Method prototype: method(object, fields)
|
|
# When the method is called, fields are only the one which are
|
|
# NOT yet filled.
|
|
OBJECTS = {}
|
|
|
|
class ConfigError(Exception): pass
|
|
|
|
def __enter__(self):
|
|
self.lock.acquire()
|
|
|
|
def __exit__(self, t, v, tb):
|
|
self.lock.release()
|
|
|
|
def __repr__(self):
|
|
return u"<Backend '%s'>" % self.name
|
|
|
|
def __init__(self, weboob, name, config=None, storage=None, logger=None):
|
|
self.logger = getLogger(name, parent=logger)
|
|
self.weboob = weboob
|
|
self.name = name
|
|
self.lock = RLock()
|
|
|
|
# Private fields (which start with '_')
|
|
self._private_config = dict((key, value) for key, value in config.iteritems() if key.startswith('_'))
|
|
|
|
# Load configuration of backend.
|
|
self.config = self.CONFIG.load(weboob, self.NAME, self.name, config)
|
|
|
|
self.storage = BackendStorage(self.name, storage)
|
|
self.storage.load(self.STORAGE)
|
|
|
|
def deinit(self):
|
|
"""
|
|
This abstract method is called when the backend is unloaded.
|
|
"""
|
|
pass
|
|
|
|
class classprop(object):
|
|
def __init__(self, fget):
|
|
self.fget = fget
|
|
def __get__(self, inst, objtype=None):
|
|
if inst:
|
|
return self.fget(inst)
|
|
else:
|
|
return self.fget(objtype)
|
|
|
|
_browser = None
|
|
|
|
@property
|
|
def browser(self):
|
|
"""
|
|
Attribute 'browser'. The browser is created at the first call
|
|
of this attribute, to avoid useless pages access.
|
|
|
|
Note that the 'create_default_browser' method is called to create it.
|
|
"""
|
|
if self._browser is None:
|
|
self._browser = self.create_default_browser()
|
|
return self._browser
|
|
|
|
def create_default_browser(self):
|
|
"""
|
|
Method to overload to build the default browser in
|
|
attribute 'browser'.
|
|
"""
|
|
return self.create_browser()
|
|
|
|
def create_browser(self, *args, **kwargs):
|
|
"""
|
|
Build a browser from the BROWSER class attribute and the
|
|
given arguments.
|
|
"""
|
|
if not self.BROWSER:
|
|
return None
|
|
|
|
if '_proxy' in self._private_config:
|
|
kwargs['proxy'] = self._private_config['_proxy']
|
|
elif 'http_proxy' in os.environ:
|
|
kwargs['proxy'] = os.environ['http_proxy']
|
|
elif 'HTTP_PROXY' in os.environ:
|
|
kwargs['proxy'] = os.environ['HTTP_PROXY']
|
|
kwargs['logger'] = self.logger
|
|
|
|
if self.BROWSER.SAVE_RESPONSES and self.BROWSER.responses_dirname:
|
|
kwargs.setdefault('responses_dirname', os.path.join(self.BROWSER.responses_dirname, self.name))
|
|
|
|
return self.BROWSER(*args, **kwargs)
|
|
|
|
@classmethod
|
|
def iter_caps(klass):
|
|
def iter_caps(cls):
|
|
for base in cls.__bases__:
|
|
if issubclass(base, IBaseCap) and base != IBaseCap:
|
|
yield base
|
|
for cap in iter_caps(base):
|
|
yield cap
|
|
return iter_caps(klass)
|
|
|
|
def has_caps(self, *caps):
|
|
for c in caps:
|
|
if (isinstance(c, basestring) and c in [cap.__name__ for cap in self.iter_caps()]) or \
|
|
isinstance(self, c):
|
|
return True
|
|
return False
|
|
|
|
def fillobj(self, obj, fields=None):
|
|
"""
|
|
@param fields which fields to fill; if None, all fields are filled (list)
|
|
"""
|
|
def not_loaded(v):
|
|
return (v is NotLoaded or isinstance(v, CapBaseObject) and not v.__iscomplete__())
|
|
|
|
if isinstance(fields, basestring):
|
|
fields = (fields,)
|
|
|
|
missing_fields = []
|
|
if fields is None:
|
|
# Select all fields
|
|
if isinstance(obj, CapBaseObject):
|
|
fields = [item[0] for item in obj.iter_fields()]
|
|
else:
|
|
fields = [item[0] for item in iter_fields(obj)]
|
|
|
|
for field in fields:
|
|
if not hasattr(obj, field):
|
|
raise FieldNotFound(obj, field)
|
|
value = getattr(obj, field)
|
|
|
|
missing = False
|
|
if hasattr(value, '__iter__'):
|
|
for v in (value.itervalues() if isinstance(value, dict) else value):
|
|
if not_loaded(v):
|
|
missing = True
|
|
break
|
|
elif not_loaded(value):
|
|
missing = True
|
|
|
|
if missing:
|
|
missing_fields.append(field)
|
|
|
|
if not missing_fields:
|
|
return obj
|
|
|
|
assert type(obj) in self.OBJECTS, 'The object of type %s is not supported by the backend %s' % (type(obj), self)
|
|
|
|
for key, value in self.OBJECTS.iteritems():
|
|
if isinstance(obj, key):
|
|
self.logger.debug(u'Fill %r with fields: %s' % (obj, missing_fields))
|
|
return value(self, obj, missing_fields) or obj
|