1# -*- coding: utf-8 -*- 2# Copyright(C) 2010-2011 Romain Bignon 3# 4# This file is part of weboob. 5# 6# weboob is free software: you can redistribute it and/or modify 7# it under the terms of the GNU Lesser General Public License as published by 8# the Free Software Foundation, either version 3 of the License, or 9# (at your option) any later version. 10# 11# weboob is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU Lesser General Public License for more details. 15# 16# You should have received a copy of the GNU Lesser General Public License 17# along with weboob. If not, see <http://www.gnu.org/licenses/>. 18 19 20import os 21from copy import copy 22from threading import RLock 23 24from weboob.capabilities.base import BaseObject, Capability, FieldNotFound, NotAvailable, NotLoaded 25from weboob.exceptions import ModuleInstallError 26from weboob.tools.compat import basestring, getproxies 27from weboob.tools.log import getLogger 28from weboob.tools.json import json 29from weboob.tools.misc import iter_fields 30from weboob.tools.value import ValuesDict 31 32__all__ = ['BackendStorage', 'BackendConfig', 'Module'] 33 34 35class BackendStorage(object): 36 """ 37 This is an abstract layer to store data in storages (:mod:`weboob.tools.storage`) 38 easily. 39 40 It is instancied automatically in constructor of :class:`Module`, in the 41 :attr:`Module.storage` attribute. 42 43 :param name: name of backend 44 :param storage: storage object 45 :type storage: :class:`weboob.tools.storage.IStorage` 46 """ 47 48 def __init__(self, name, storage): 49 self.name = name 50 self.storage = storage 51 52 def set(self, *args): 53 """ 54 Set value in the storage. 55 56 Example: 57 58 >>> from weboob.tools.storage import StandardStorage 59 >>> backend = BackendStorage('blah', StandardStorage('/tmp/cfg')) 60 >>> backend.storage.set('config', 'nb_of_threads', 10) # doctest: +SKIP 61 >>> 62 63 :param args: the path where to store value 64 """ 65 if self.storage: 66 return self.storage.set('backends', self.name, *args) 67 68 def delete(self, *args): 69 """ 70 Delete a value from the storage. 71 72 :param args: path to delete. 73 """ 74 if self.storage: 75 return self.storage.delete('backends', self.name, *args) 76 77 def get(self, *args, **kwargs): 78 """ 79 Get a value or a dict of values in storage. 80 81 Example: 82 83 >>> from weboob.tools.storage import StandardStorage 84 >>> backend = BackendStorage('blah', StandardStorage('/tmp/cfg')) 85 >>> backend.storage.get('config', 'nb_of_threads') # doctest: +SKIP 86 10 87 >>> backend.storage.get('config', 'unexistant', 'path', default='lol') # doctest: +SKIP 88 'lol' 89 >>> backend.storage.get('config') # doctest: +SKIP 90 {'nb_of_threads': 10, 'other_things': 'blah'} 91 92 :param args: path to get 93 :param default: if specified, default value when path is not found 94 """ 95 if self.storage: 96 return self.storage.get('backends', self.name, *args, **kwargs) 97 else: 98 return kwargs.get('default', None) 99 100 def load(self, default): 101 """ 102 Load storage. 103 104 It is made automatically when your backend is created, and use the 105 ``STORAGE`` class attribute as default. 106 107 :param default: this is the default tree if storage is empty 108 :type default: :class:`dict` 109 """ 110 if self.storage: 111 return self.storage.load('backends', self.name, default) 112 113 def save(self): 114 """ 115 Save storage. 116 """ 117 if self.storage: 118 return self.storage.save('backends', self.name) 119 120 121class BackendConfig(ValuesDict): 122 """ 123 Configuration of a backend. 124 125 This class is firstly instanced as a :class:`weboob.tools.value.ValuesDict`, 126 containing some :class:`weboob.tools.value.Value` (and derivated) objects. 127 128 Then, using the :func:`load` method will load configuration from file and 129 create a copy of the :class:`BackendConfig` object with the loaded values. 130 """ 131 modname = None 132 instname = None 133 weboob = None 134 135 def load(self, weboob, modname, instname, config, nofail=False): 136 """ 137 Load configuration from dict to create an instance. 138 139 :param weboob: weboob object 140 :type weboob: :class:`weboob.core.ouiboube.Weboob` 141 :param modname: name of the module 142 :type modname: :class:`str` 143 :param instname: name of this backend 144 :type instname: :class:`str` 145 :param params: parameters to load 146 :type params: :class:`dict` 147 :param nofail: if true, this call can't fail 148 :type nofail: :class:`bool` 149 :rtype: :class:`BackendConfig` 150 """ 151 cfg = BackendConfig() 152 cfg.modname = modname 153 cfg.instname = instname 154 cfg.weboob = weboob 155 for name, field in self.items(): 156 value = config.get(name, None) 157 158 if value is None: 159 if not nofail and field.required: 160 raise Module.ConfigError('Backend(%s): Configuration error: Missing parameter "%s" (%s)' 161 % (cfg.instname, name, field.description)) 162 value = field.default 163 164 field = copy(field) 165 try: 166 field.load(cfg.instname, value, cfg.weboob.requests) 167 except ValueError as v: 168 if not nofail: 169 raise Module.ConfigError( 170 'Backend(%s): Configuration error for field "%s": %s' % (cfg.instname, name, v)) 171 172 cfg[name] = field 173 return cfg 174 175 def dump(self): 176 """ 177 Dump config in a dictionary. 178 179 :rtype: :class:`dict` 180 """ 181 settings = {} 182 for name, value in self.items(): 183 if not value.transient: 184 settings[name] = value.dump() 185 return settings 186 187 def save(self, edit=True, params=None): 188 """ 189 Save backend config. 190 191 :param edit: if true, it changes config of an existing backend 192 :type edit: :class:`bool` 193 :param params: if specified, params to merge with the ones of the current object 194 :type params: :class:`dict` 195 """ 196 assert self.modname is not None 197 assert self.instname is not None 198 assert self.weboob is not None 199 200 dump = self.dump() 201 if params is not None: 202 dump.update(params) 203 204 if edit: 205 self.weboob.backends_config.edit_backend(self.instname, dump) 206 else: 207 self.weboob.backends_config.add_backend(self.instname, self.modname, dump) 208 209 210class Module(object): 211 """ 212 Base class for modules. 213 214 You may derivate it, and also all capabilities you want to implement. 215 216 :param weboob: weboob instance 217 :type weboob: :class:`weboob.core.ouiboube.Weboob` 218 :param name: name of backend 219 :type name: :class:`str` 220 :param config: configuration of backend 221 :type config: :class:`dict` 222 :param storage: storage object 223 :type storage: :class:`weboob.tools.storage.IStorage` 224 :param logger: logger 225 :type logger: :class:`logging.Logger` 226 """ 227 # Module name. 228 NAME = None 229 """Name of the maintainer of this module.""" 230 231 MAINTAINER = u'<unspecified>' 232 233 EMAIL = '<unspecified>' 234 """Email address of the maintainer.""" 235 236 VERSION = '<unspecified>' 237 """Version of module (for information only).""" 238 239 DESCRIPTION = '<unspecified>' 240 """Description""" 241 242 # License of this module. 243 LICENSE = '<unspecified>' 244 245 CONFIG = BackendConfig() 246 """Configuration required for backends. 247 248 Values must be weboob.tools.value.Value objects. 249 """ 250 251 STORAGE = {} 252 """Storage""" 253 254 BROWSER = None 255 """Browser class""" 256 257 ICON = None 258 """URL to an optional icon. 259 260 If you want to create your own icon, create a 'favicon.png' icon in 261 the module's directory, and keep the ICON value to None. 262 """ 263 264 OBJECTS = {} 265 """Supported objects to fill 266 267 The key is the class and the value the method to call to fill 268 Method prototype: method(object, fields) 269 When the method is called, fields are only the one which are 270 NOT yet filled. 271 """ 272 273 class ConfigError(Exception): 274 """ 275 Raised when the config can't be loaded. 276 """ 277 278 def __enter__(self): 279 self.lock.acquire() 280 281 def __exit__(self, t, v, tb): 282 self.lock.release() 283 284 def __repr__(self): 285 return "<Backend %r>" % self.name 286 287 def __new__(cls, *args, **kwargs): 288 """ Accept any arguments, necessary for AbstractModule __new__ override. 289 290 AbstractModule, in its overridden __new__, removes itself from class hierarchy 291 so its __new__ is called only once. In python 3, default (object) __new__ is 292 then used for next instantiations but it's a slot/"fixed" version supporting 293 only one argument (type to instanciate). 294 """ 295 return object.__new__(cls) 296 297 def __init__(self, weboob, name, config=None, storage=None, logger=None, nofail=False): 298 self.logger = getLogger(name, parent=logger) 299 self.weboob = weboob 300 self.name = name 301 self.lock = RLock() 302 if config is None: 303 config = {} 304 305 # Private fields (which start with '_') 306 self._private_config = dict((key, value) for key, value in config.items() if key.startswith('_')) 307 308 # Load configuration of backend. 309 self.config = self.CONFIG.load(weboob, self.NAME, self.name, config, nofail) 310 311 self.storage = BackendStorage(self.name, storage) 312 self.storage.load(self.STORAGE) 313 314 def dump_state(self): 315 if hasattr(self.browser, 'dump_state'): 316 self.storage.set('browser_state', self.browser.dump_state()) 317 self.storage.save() 318 319 def deinit(self): 320 """ 321 This abstract method is called when the backend is unloaded. 322 """ 323 if self._browser is None: 324 return 325 326 try: 327 self.dump_state() 328 finally: 329 if hasattr(self.browser, 'deinit'): 330 self.browser.deinit() 331 332 _browser = None 333 334 @property 335 def browser(self): 336 """ 337 Attribute 'browser'. The browser is created at the first call 338 of this attribute, to avoid useless pages access. 339 340 Note that the :func:`create_default_browser` method is called to create it. 341 """ 342 if self._browser is None: 343 self._browser = self.create_default_browser() 344 return self._browser 345 346 def create_default_browser(self): 347 """ 348 Method to overload to build the default browser in 349 attribute 'browser'. 350 """ 351 return self.create_browser() 352 353 def create_browser(self, *args, **kwargs): 354 """ 355 Build a browser from the BROWSER class attribute and the 356 given arguments. 357 358 :param klass: optional parameter to give another browser class to instanciate 359 :type klass: :class:`weboob.browser.browsers.Browser` 360 """ 361 362 klass = kwargs.pop('klass', self.BROWSER) 363 364 if not klass: 365 return None 366 367 kwargs['proxy'] = self.get_proxy() 368 if '_proxy_headers' in self._private_config: 369 kwargs['proxy_headers'] = self._private_config['_proxy_headers'] 370 if isinstance(kwargs['proxy_headers'], basestring): 371 kwargs['proxy_headers'] = json.loads(kwargs['proxy_headers']) 372 373 kwargs['logger'] = self.logger 374 375 if self.logger.settings['responses_dirname']: 376 kwargs.setdefault('responses_dirname', os.path.join(self.logger.settings['responses_dirname'], 377 self._private_config.get('_debug_dir', self.name))) 378 elif os.path.isabs(self._private_config.get('_debug_dir', '')): 379 kwargs.setdefault('responses_dirname', self._private_config['_debug_dir']) 380 if self._private_config.get('_highlight_el', ''): 381 kwargs.setdefault('highlight_el', bool(int(self._private_config['_highlight_el']))) 382 383 browser = klass(*args, **kwargs) 384 385 if hasattr(browser, 'load_state'): 386 browser.load_state(self.storage.get('browser_state', default={})) 387 388 return browser 389 390 def get_proxy(self): 391 # Get proxies from environment variables 392 proxies = getproxies() 393 # Override them with backend-specific config 394 if '_proxy' in self._private_config: 395 proxies['http'] = self._private_config['_proxy'] 396 if '_proxy_ssl' in self._private_config: 397 proxies['https'] = self._private_config['_proxy_ssl'] 398 # Remove empty values 399 for key in list(proxies.keys()): 400 if not proxies[key]: 401 del proxies[key] 402 return proxies 403 404 @classmethod 405 def iter_caps(klass): 406 """ 407 Iter capabilities implemented by this backend. 408 409 :rtype: iter[:class:`weboob.capabilities.base.Capability`] 410 """ 411 for base in klass.mro(): 412 if issubclass(base, Capability) and base != Capability and base != klass and not issubclass(base, Module): 413 yield base 414 415 def has_caps(self, *caps): 416 """ 417 Check if this backend implements at least one of these capabilities. 418 """ 419 for c in caps: 420 if (isinstance(c, basestring) and c in [cap.__name__ for cap in self.iter_caps()]) or \ 421 isinstance(self, c): 422 return True 423 return False 424 425 def fillobj(self, obj, fields=None): 426 """ 427 Fill an object with the wanted fields. 428 429 :param fields: what fields to fill; if None, all fields are filled 430 :type fields: :class:`list` 431 """ 432 if obj is None: 433 return obj 434 435 def not_loaded_or_incomplete(v): 436 return (v is NotLoaded or isinstance(v, BaseObject) and not v.__iscomplete__()) 437 438 def not_loaded(v): 439 return v is NotLoaded 440 441 def filter_missing_fields(obj, fields, check_cb): 442 missing_fields = [] 443 if fields is None: 444 # Select all fields 445 if isinstance(obj, BaseObject): 446 fields = [item[0] for item in obj.iter_fields()] 447 else: 448 fields = [item[0] for item in iter_fields(obj)] 449 450 for field in fields: 451 if not hasattr(obj, field): 452 raise FieldNotFound(obj, field) 453 value = getattr(obj, field) 454 455 missing = False 456 if hasattr(value, '__iter__'): 457 for v in (value.values() if isinstance(value, dict) else value): 458 if check_cb(v): 459 missing = True 460 break 461 elif check_cb(value): 462 missing = True 463 464 if missing: 465 missing_fields.append(field) 466 467 return missing_fields 468 469 if isinstance(fields, basestring): 470 fields = (fields,) 471 472 missing_fields = filter_missing_fields(obj, fields, not_loaded_or_incomplete) 473 474 if not missing_fields: 475 return obj 476 477 for key, value in self.OBJECTS.items(): 478 if isinstance(obj, key): 479 self.logger.debug(u'Fill %r with fields: %s' % (obj, missing_fields)) 480 obj = value(self, obj, missing_fields) or obj 481 break 482 483 missing_fields = filter_missing_fields(obj, fields, not_loaded) 484 485 # Object is not supported by backend. Do not notice it to avoid flooding user. 486 # That's not so bad. 487 for field in missing_fields: 488 setattr(obj, field, NotAvailable) 489 490 return obj 491 492 493class AbstractModuleMissingParentError(Exception): 494 pass 495 496 497class AbstractModule(Module): 498 """ Abstract module allow inheritance between modules. 499 500 Sometimes, several websites are based on the same website engine. This module 501 allow to simplify code with a fake inheritance: weboob will install (if needed) and 502 load a PARENT module and build our AbstractModule on top of this class. 503 504 PARENT is a mandatory attribute of any AbstractModule. 505 506 By default an AbstractModule inherits its parent backends CONFIG. 507 To add backend values, use ADDITIONAL_CONFIG. 508 To remove backend values, you must override CONFIG definition. 509 510 Note that you must pass a valid weboob instance as first argument of the constructor. 511 """ 512 PARENT = None 513 514 ADDITIONAL_CONFIG = BackendConfig() 515 """Optional additional Values for backends, appended to parent CONFIG 516 517 Values must be weboob.tools.value.Value objects. 518 """ 519 520 @classmethod 521 def _resolve_abstract(cls, weboob, name): 522 """ Replace AbstractModule parent with the real base class """ 523 if cls.PARENT is None: 524 raise AbstractModuleMissingParentError("PARENT is not defined for module %s" % cls.__name__) 525 526 try: 527 parent = weboob.load_or_install_module(cls.PARENT).klass 528 except ModuleInstallError as err: 529 raise ModuleInstallError('The module %s depends on %s module but %s\'s installation failed with: %s' % (name, cls.PARENT, cls.PARENT, err)) 530 531 # Parent may have defined an ADDITIONAL_CONFIG 532 if getattr(parent, 'ADDITIONAL_CONFIG', None): 533 cls.ADDITIONAL_CONFIG = BackendConfig(*(list(parent.ADDITIONAL_CONFIG.values()) + list(cls.ADDITIONAL_CONFIG.values()))) 534 535 # Parent may be an AbstractModule as well 536 if hasattr(parent, '_resolve_abstract'): 537 parent._resolve_abstract(weboob, name) 538 539 parent_caps = parent.iter_caps() 540 cls.__bases__ = tuple([parent] + [cap for cap in cls.iter_caps() if cap not in parent_caps]) 541 return parent 542 543 def __new__(cls, weboob, name, config=None, storage=None, logger=None, nofail=False): 544 parent = cls._resolve_abstract(weboob=weboob, name=name) 545 546 # fake backend config inheritance, override existing Values 547 # do not use CONFIG to allow the children to overwrite completely the parent CONFIG. 548 if getattr(cls, 'ADDITIONAL_CONFIG', None): 549 cls.CONFIG = BackendConfig(*(list(parent.CONFIG.values()) + list(cls.ADDITIONAL_CONFIG.values()))) 550 551 return Module.__new__(cls, weboob, name, config, storage, logger, nofail) 552