"""Configuring cryptoassets.core for your project.
Setup SQLAlchemy, backends, etc. based on individual dictionaries or YAML syntax configuration file.
"""
import io
import inspect
import logging
import logging.config
import yaml
from zope.dottedname.resolve import resolve
from sqlalchemy import engine_from_config
from .coin.defaults import COIN_MODEL_DEFAULTS
from .coin.registry import Coin
from .coin.registry import CoinRegistry
from .backend.base import CoinBackend
from .coin import registry as coin_registry
from .event.registry import EventHandlerRegistry
from .event.base import EventHandler
from .service import status
from .app import Subsystem
from .utils.dictutil import merge_dict
#: XXX: logger cannot be used in this module due to order of logger initialization?
logger = None
[docs]class ConfigurationError(Exception):
"""ConfigurationError is thrown when the Configurator thinks somethink cannot make sense with the config data."""
[docs]class Configurator:
"""Read configuration data and set up Cryptoassets library.
Reads Python or YAML format config data and then setss :py:class:`cryptoassets.core.app.CryptoassetsApp` up and running accordingly.
"""
def __init__(self, app, service=None):
"""
:param app: :py:class:`cryptoassets.core.app.CryptoassetsApp` instance
:param service: :py:class:`cryptoassets.core.service.main.Service` instance (optional)
"""
self.app = app
self.service = service
#: Store full parsed configuration as Python dict for later consumption
self.config = None
[docs] def setup_engine(self, configuration):
"""Setup database engine.
See ``sqlalchemy.engine_from_config`` for details.
TODO: Move engine to its own module?
:param dict configuration: ``engine`` configuration section
"""
# Do not enable database
if not self.app.is_enabled(Subsystem.database):
return
transaction_retries = configuration.pop("transaction_retries", 3)
self.app.transaction_retries = transaction_retries
echo = configuration.get("echo") in (True, "true")
engine = engine_from_config(configuration, prefix="", echo=echo, isolation_level="SERIALIZABLE")
return engine
[docs] def setup_backend(self, coin, data):
"""Setup backends.
:param data: dictionary of backend configuration entries
"""
# Do not enable
if not self.app.is_enabled(Subsystem.backend):
return
if not data:
raise ConfigurationError("backends section missing in config")
data = data.copy() # No mutate in place
klass = data.pop("class")
data["coin"] = coin
provider = resolve(klass)
max_tracked_incoming_confirmations = data.pop("max_tracked_incoming_confirmations", 15)
# Pass given configuration options to the backend as is
try:
instance = provider(**data)
except TypeError as te:
# TODO: Here we reflect potential passwords from the configuration file
# back to the terminal
# TypeError: __init__() got an unexpected keyword argument 'network'
raise ConfigurationError("Could not initialize backend {} with options {}".format(klass, data)) from te
assert isinstance(instance, CoinBackend)
return instance
[docs] def setup_model(self, module):
"""Setup SQLAlchemy models.
:param module: Python module defining SQLAlchemy models for a cryptocurrency
:return: :py:class:`cryptoassets.core.coin.registry.CoinModelDescription` instance
"""
_engine = None
result = resolve(module) # Imports module, making SQLAlchemy aware of it
if not result:
raise ConfigurationError("Could not resolve {}".format(module))
coin_description = getattr(result, "coin_description", None)
if not coin_description:
raise ConfigurationError("Module does not export coin_description attribute: {}".format(module))
return coin_description
def setup_coins(self, coins):
coin_registry = CoinRegistry()
if not coins:
raise ConfigurationError("No cryptocurrencies given in the config.")
for name, data in coins.items():
default_models_module = COIN_MODEL_DEFAULTS.get(name)
models_module = data.get("models", default_models_module)
if not models_module:
raise ConfigurationError("Don't know which SQLAlchemy model to use for coin {}.".format(name))
coin_description = self.setup_model(models_module)
backend_config = data.get("backend")
if not backend_config:
raise ConfigurationError("No backend config given for {}".format(name))
max_confirmation_count = int(data.get("max_confirmation_count", 15))
testnet = data.get("testnet") in ("true", True)
coin = Coin(coin_description, max_confirmation_count=max_confirmation_count, testnet=testnet)
backend = self.setup_backend(coin, data.get("backend"))
coin.backend = backend
coin_registry.register(name, coin)
return coin_registry
[docs] def setup_event_handlers(self, event_handler_registry):
"""Read notification settings.
Example notifier format::
{
"shell": {
"class": "cryptoassets.core.event_handler_registry.shell.ShellNotifier",
"script": "/usr/bin/local/new-payment.sh"
}
}
"""
# Do not enable event_handler_registry
if not self.app.is_enabled(Subsystem.event_handler_registry):
return
notifier_registry = EventHandlerRegistry()
if not event_handler_registry:
# event_handler_registry not configured
return
for name, data in event_handler_registry.items():
data = data.copy() # No mutate in place
klass = data.pop("class")
provider = resolve(klass)
# Pass given configuration options to the backend as is
try:
instance = provider(**data)
except TypeError as te:
# TODO: Here we reflect potential passwords from the configuration file
# back to the terminal
# TypeError: __init__() got an unexpected keyword argument 'network'
raise ConfigurationError("Could not initialize notifier {} with options {}".format(klass, data)) from te
assert isinstance(instance, EventHandler)
notifier_registry.register(name, instance)
return notifier_registry
[docs] def setup_status_server(self, config):
"""Prepare status server instance for the cryptoassets helper service.
"""
if not config:
return
# Do not enable status server
if not self.app.is_enabled(Subsystem.status_server):
return
ip = config.get("ip", "127.0.0.1")
port = int(config.get("port", "18881"))
server = status.StatusHTTPServer(ip, port)
return server
[docs] def setup_service(self, config):
"""Configure cryptoassets service helper process."""
assert self.service
# Nothing given, use defaults
if not config:
return
if "broadcast_period" in config:
self.service.broadcast_period = int(config["broadcast_period"])
[docs] def load_from_dict(self, config):
""" Load configuration from Python dictionary.
Populates ``app`` with instances required to run ``cryptocurrency.core`` framework.
"""
self.app.engine = self.setup_engine(config.get("database"))
self.app.coins = self.setup_coins(config.get("coins"))
# XXX: Backwards compatibility ... drop in some point
self.app.status_server = self.setup_status_server(config.get("status_server") or config.get("status-server"))
self.app.event_handler_registry = self.setup_event_handlers(config.get("events"))
if self.service:
self.setup_service(config.get("service"))
self.config = config
@classmethod
[docs] def setup_service_logging(cls, config):
"""Setup Python loggers for the helper service process.
:param config: service -> logging configure section.
"""
if not config:
# Go with the stderr
logging.basicConfig()
else:
config["version"] = 1
logging.config.dictConfig(config)
@classmethod
[docs] def setup_startup(cls, config):
"""Service helper process specific setup when launched from command line.
Reads configuration ``service`` section, ATM only interested in ``logging`` subsection.
This is run before the actual Cryptoassets application initialization. We need logging initialized beforehand so that we can print out nice ``$VERSIONNUMBER is starting`` message.
"""
service = config.get("service", {})
logging = service.get("logging", None)
cls.setup_service_logging(logging)
return config
@staticmethod
[docs] def prepare_yaml_file(fname):
"""Extract config dictionary from a YAML file."""
stream = io.open(fname, "rt")
config = yaml.safe_load(stream)
stream.close()
if not type(config) == dict:
raise ConfigurationError("YAML configuration file must be mapping like")
return config
[docs] def load_yaml_file(self, fname, overrides={}):
"""Load config from a YAML file.
:param fname: Path to the YAML file
:param overrides: Python nested dicts for specific setting overrides
"""
config = self.prepare_yaml_file(fname)
merge_dict(config, overrides)
self.load_from_dict(config)