# piker: trading gear for hackers # Copyright (C) 2018-present Tyler Goodlet (in stewardship of piker0) # This program 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. # This program 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 this program. If not, see . """ Broker configuration mgmt. """ import platform import sys import os from os.path import dirname import shutil from typing import Optional from bidict import bidict import toml from .log import get_logger log = get_logger('broker-config') # taken from ``click`` since apparently they have some # super weirdness with sigint and sudo..no clue def get_app_dir(app_name, roaming=True, force_posix=False): r"""Returns the config folder for the application. The default behavior is to return whatever is most appropriate for the operating system. To give you an idea, for an app called ``"Foo Bar"``, something like the following folders could be returned: Mac OS X: ``~/Library/Application Support/Foo Bar`` Mac OS X (POSIX): ``~/.foo-bar`` Unix: ``~/.config/foo-bar`` Unix (POSIX): ``~/.foo-bar`` Win XP (roaming): ``C:\Documents and Settings\\Local Settings\Application Data\Foo Bar`` Win XP (not roaming): ``C:\Documents and Settings\\Application Data\Foo Bar`` Win 7 (roaming): ``C:\Users\\AppData\Roaming\Foo Bar`` Win 7 (not roaming): ``C:\Users\\AppData\Local\Foo Bar`` .. versionadded:: 2.0 :param app_name: the application name. This should be properly capitalized and can contain whitespace. :param roaming: controls if the folder should be roaming or not on Windows. Has no affect otherwise. :param force_posix: if this is set to `True` then on any POSIX system the folder will be stored in the home folder with a leading dot instead of the XDG config home or darwin's application support folder. """ def _posixify(name): return "-".join(name.split()).lower() # if WIN: if platform.system() == 'Windows': key = "APPDATA" if roaming else "LOCALAPPDATA" folder = os.environ.get(key) if folder is None: folder = os.path.expanduser("~") return os.path.join(folder, app_name) if force_posix: return os.path.join(os.path.expanduser("~/.{}".format(_posixify(app_name)))) if sys.platform == "darwin": return os.path.join( os.path.expanduser("~/Library/Application Support"), app_name ) return os.path.join( os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), _posixify(app_name), ) _config_dir = _click_config_dir = get_app_dir('piker') _parent_user = os.environ.get('SUDO_USER') if _parent_user: non_root_user_dir = os.path.expanduser( f'~{_parent_user}' ) root = 'root' _config_dir = ( non_root_user_dir + _click_config_dir[ _click_config_dir.rfind(root) + len(root): ] ) _file_name = 'brokers.toml' _watchlists_data_path = os.path.join(_config_dir, 'watchlists.json') _context_defaults = dict( default_map={ # Questrade specific quote poll rates 'monitor': { 'rate': 3, }, 'optschain': { 'rate': 1, }, } ) def _override_config_dir( path: str ) -> None: global _config_dir _config_dir = path def get_broker_conf_path(): """Return the default config path normally under ``~/.config/piker`` on linux. Contains files such as: - brokers.toml - watchlists.toml - signals.toml - strats.toml """ return os.path.join(_config_dir, _file_name) def repodir(): """Return the abspath to the repo directory. """ dirpath = os.path.abspath( # we're 3 levels down in **this** module file dirname(dirname(os.path.realpath(__file__))) ) return dirpath def load( path: str = None ) -> (dict, str): """Load broker config. """ path = path or get_broker_conf_path() if not os.path.isfile(path): shutil.copyfile( os.path.join(repodir(), 'config', 'brokers.toml'), path, ) config = toml.load(path) log.debug(f"Read config file {path}") return config, path def write( config: dict, # toml config as dict path: str = None, ) -> None: """Write broker config to disk. Create a ``brokers.ini`` file if one does not exist. """ path = path or get_broker_conf_path() dirname = os.path.dirname(path) if not os.path.isdir(dirname): log.debug(f"Creating config dir {_config_dir}") os.makedirs(dirname) if not config: raise ValueError( "Watch out you're trying to write a blank config!") log.debug(f"Writing config file {path}") with open(path, 'w') as cf: return toml.dump(config, cf) def load_accounts( providers: Optional[list[str]] = None ) -> bidict[str, Optional[str]]: conf, path = load() accounts = bidict() for provider_name, section in conf.items(): accounts_section = section.get('accounts') if ( providers is None or providers and provider_name in providers ): if accounts_section is None: log.warning(f'No accounts named for {provider_name}?') continue else: for label, value in accounts_section.items(): accounts[ f'{provider_name}.{label}' ] = value # our default paper engine entry accounts['paper'] = None return accounts