cozyweboob/cozyweboob.py

240 lines
7.9 KiB
Python
Executable File

#!/usr/bin/env python2
"""
Wrapper script around Weboob to be able to use it in combination with Cozy +
Konnectors easily.
Part of this code comes from [Kresus](https://github.com/bnjbvr/kresus/)
written by bnjbvr and released under MIT.
"""
from __future__ import print_function
import collections
import importlib
import json
import logging
import sys
from getpass import getpass
from requests.utils import dict_from_cookiejar
from weboob.core import Weboob
from weboob.exceptions import ModuleInstallError
from tools.env import is_in_debug_mode
from tools.jsonwriter import pretty_json
from tools.progress import DummyProgress
import tools.weboob_tools as weboob_tools
# Dynamically load capabilities conversion modules
# Dynamic loading is required to be able to call them programatically.
CAPABILITIES_CONVERSION_MODULES = importlib.import_module("capabilities")
# Module specific logger
logger = logging.getLogger(__name__)
class WeboobProxy(object):
"""
Connector is a tool that connects to common websites like bank website,
phone operator website... and that grabs personal data from there.
Credentials are required to make this operation.
Technically, connectors are weboob backend wrappers.
"""
@staticmethod
def version():
"""
Get Weboob version.
Returns:
the version of installed Weboob.
"""
return Weboob.VERSION
def __init__(self):
"""
Create a Weboob handle.
"""
# Get a weboob instance
self.weboob = Weboob()
self.backend = None
def install_modules(self, capability=None, name=None):
"""
Ensure latest version of modules is installed.
Args:
capability: Restrict the modules to install to a given capability.
name: Only install the specified module.
Returns: A map between name and infos for all installed modules.
"""
repositories = self.weboob.repositories
# Update modules list
repositories.update_repositories(DummyProgress())
# Get module infos
if name:
modules = {name: repositories.get_module_info(name)}
else:
modules = repositories.get_all_modules_info(capability)
# Install modules if required
for infos in modules.values():
if infos is not None and (
not infos.is_installed() or
not infos.is_local()
):
try:
repositories.install(infos, progress=DummyProgress())
except ModuleInstallError as e:
logger.info(str(e))
return {
module_name: dict(infos.dump())
for module_name, infos in modules.items()
if infos.is_installed()
}
def list_modules(self, capability=None, name=None):
"""
Ensure latest version of modules is installed.
Args:
capability: Restrict the modules to install to a given capability.
name: Only install the specified module.
Returns: The list of installed module infos.
"""
# Update modules and get the latest up to date list
installed_modules = self.install_modules(
capability=capability,
name=name
)
# For each module, get back its config options and website base url
for module_name in installed_modules:
module = self.weboob.modules_loader.get_or_load_module(module_name)
installed_modules[module_name]["config"] = (
weboob_tools.dictify_config_desc(module.config)
)
installed_modules[module_name]["website"] = module.website
return installed_modules
def init_backend(self, modulename, parameters):
"""
Backend initialization.
Returns:
the built backend.
"""
# Ensure module is installed
self.install_modules(name=modulename)
# Build backend
self.backend = self.weboob.build_backend(modulename, parameters)
return self.backend
def main_fetch(used_modules):
"""
Main fetching code
Args:
used_modules: A list of modules description dicts.
Returns: A dict of all the results, ready to be JSON serialized.
"""
# Fetch data for the specified modules
fetched_data = collections.defaultdict(dict)
logger.info("Start fetching from konnectors.")
for module in used_modules:
try:
weboob_proxy = WeboobProxy()
logger.info("Fetching data from module %s.", module["id"])
# Get associated backend for this module
backend = weboob_proxy.init_backend(
module["name"],
module["parameters"]
)
for capability in backend.iter_caps(): # Supported capabilities
# Get capability class name for dynamic import of converter
capability = capability.__name__
try:
fetching_function = (
getattr(
getattr(
CAPABILITIES_CONVERSION_MODULES,
capability
),
"to_cozy"
)
)
logger.info("Fetching capability %s.", capability)
# Fetch data and merge them with the ones from other
# capabilities
fetched_data[module["id"]].update(
fetching_function(backend)
)
except AttributeError:
# In case the converter does not exist on our side
logger.error("%s capability is not implemented.",
capability)
continue
# Store session cookie of this module, to fetch files afterwards
try:
fetched_data[module["id"]]["cookies"] = dict_from_cookiejar(
backend.browser.session.cookies
)
except AttributeError:
# Avoid an AttributeError if no session is used for this module
fetched_data[module["id"]]["cookies"] = None
except Exception as e:
# Store any error happening in a dedicated field
fetched_data[module["id"]]["error"] = e
if is_in_debug_mode():
# Reraise if in debug
raise
else:
# Skip any errored module when not in debug
continue
logger.info("Done fetching from konnectors.")
return fetched_data
def main(json_params):
"""
Main code
Args:
json_params: A JSON string representing the params to use.
Returns: A JSON string of the results.
"""
try:
# Fetch konnectors JSON description from stdin
konnectors = json.loads(json_params)
# Debug only: Handle missing passwords using getpass
if is_in_debug_mode():
for module in konnectors:
for param in module["parameters"]:
if not module["parameters"][param]:
module["parameters"][param] = getpass(
"Password for module %s? " % (
module["id"],
)
)
except ValueError:
logger.error("Invalid JSON input.")
sys.exit(-1)
# Output the JSON formatted results on stdout
return pretty_json(
main_fetch(konnectors)
)
if __name__ == '__main__':
try:
# Debug only: Set logging level and format
if is_in_debug_mode():
logging.basicConfig(
format='%(levelname)s: %(message)s',
level=logging.INFO
)
print(main(sys.stdin.read()))
except KeyboardInterrupt:
pass