piker/piker/config.py

285 lines
7.1 KiB
Python
Raw Normal View History

2020-11-06 17:23:14 +00:00
# piker: trading gear for hackers
# Copyright (C) 2018-present Tyler Goodlet (in stewardship for pikers)
2020-11-06 17:23:14 +00:00
# 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 <https://www.gnu.org/licenses/>.
"""
Broker configuration mgmt.
"""
import platform
import sys
2019-02-26 00:29:54 +00:00
import os
from os import path
from os.path import dirname
import shutil
2021-08-23 18:42:26 +00:00
from typing import Optional
2021-09-08 18:01:54 +00:00
from bidict import bidict
import toml
2021-09-07 01:26:28 +00:00
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\<user>\Local Settings\Application Data\Foo``
Win XP (not roaming):
``C:\Documents and Settings\<user>\Application Data\Foo Bar``
Win 7 (roaming):
``C:\Users\<user>\AppData\Roaming\Foo Bar``
Win 7 (not roaming):
``C:\Users\<user>\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):
]
)
_conf_names: set[str] = {
'brokers',
'pps',
'trades',
'watchlists',
}
_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,
},
}
)
2019-02-26 00:29:54 +00:00
def _override_config_dir(
path: str
) -> None:
global _config_dir
_config_dir = path
def _conf_fn_w_ext(
name: str,
) -> str:
# change this if we ever change the config file format.
return f'{name}.toml'
def get_conf_path(
conf_name: str = 'brokers',
) -> str:
'''
Return the top-level default config path normally under
``~/.config/piker`` on linux for a given ``conf_name``, the config
name.
2021-03-19 17:23:33 +00:00
Contains files such as:
- brokers.toml
- pp.toml
2021-03-19 17:23:33 +00:00
- watchlists.toml
# maybe coming soon ;)
2021-03-19 17:23:33 +00:00
- signals.toml
- strats.toml
'''
assert conf_name in _conf_names
fn = _conf_fn_w_ext(conf_name)
return os.path.join(
_config_dir,
fn,
)
2019-02-26 00:29:54 +00:00
def repodir():
'''
Return the abspath to the repo directory.
'''
dirpath = path.abspath(
# we're 3 levels down in **this** module file
dirname(dirname(os.path.realpath(__file__)))
)
return dirpath
2019-02-26 00:29:54 +00:00
def load(
conf_name: str = 'brokers',
2022-06-18 19:54:16 +00:00
path: str = None,
**tomlkws,
) -> (dict, str):
'''
Load config file by name.
'''
path = path or get_conf_path(conf_name)
2022-11-11 22:39:46 +00:00
if not os.path.isdir(_config_dir):
os.mkdir(_config_dir)
if not os.path.isfile(path):
fn = _conf_fn_w_ext(conf_name)
template = os.path.join(
repodir(),
'config',
fn
)
# try to copy in a template config to the user's directory
# if one exists.
if os.path.isfile(template):
shutil.copyfile(template, path)
2022-11-11 22:39:46 +00:00
else:
with open(path, 'r'):
pass # touch it
2022-06-18 19:54:16 +00:00
config = toml.load(path, **tomlkws)
log.debug(f"Read config file {path}")
return config, path
2019-02-26 00:29:54 +00:00
def write(
config: dict, # toml config as dict
name: str = 'brokers',
2019-02-26 00:29:54 +00:00
path: str = None,
**toml_kwargs,
2019-02-26 00:29:54 +00:00
) -> None:
''''
Write broker config to disk.
Create a ``brokers.ini`` file if one does not exist.
'''
path = path or get_conf_path(name)
2019-02-26 00:29:54 +00:00
dirname = os.path.dirname(path)
if not os.path.isdir(dirname):
log.debug(f"Creating config dir {_config_dir}")
2019-02-26 00:29:54 +00:00
os.makedirs(dirname)
if not config:
raise ValueError(
"Watch out you're trying to write a blank config!")
log.debug(
f"Writing config `{name}` file to:\n"
f"{path}"
)
2019-02-26 00:29:54 +00:00
with open(path, 'w') as cf:
return toml.dump(
config,
cf,
**toml_kwargs,
)
2021-08-23 18:42:26 +00:00
2021-09-08 18:01:54 +00:00
def load_accounts(
2021-09-10 15:35:30 +00:00
providers: Optional[list[str]] = None
2021-09-08 18:01:54 +00:00
) -> bidict[str, Optional[str]]:
2021-08-23 18:42:26 +00:00
conf, path = load()
2021-09-10 15:35:30 +00:00
accounts = bidict()
for provider_name, section in conf.items():
accounts_section = section.get('accounts')
if (
2021-09-10 15:35:30 +00:00
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
2021-08-23 18:42:26 +00:00
2021-09-10 15:35:30 +00:00
# our default paper engine entry
accounts['paper'] = None
2021-08-23 18:42:26 +00:00
return accounts