Skip to content

Commit 74a21b8

Browse files
committed
feat(config): env var override
1 parent 21cbc31 commit 74a21b8

File tree

2 files changed

+99
-20
lines changed

2 files changed

+99
-20
lines changed

src/jmclient/configure.py

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,10 @@ def jm_single() -> AttributeDict:
9191
'POLICY': ['absurd_fee_per_kb', 'taker_utxo_retries',
9292
'taker_utxo_age', 'taker_utxo_amtpercent']}
9393

94-
_DEFAULT_INTEREST_RATE = "0.015"
94+
_ENV_VAR_PREFIX = "JM_"
95+
_SECTIONS_WITH_SUBSECTIONS = {"MESSAGING"}
9596

97+
_DEFAULT_INTEREST_RATE = "0.015"
9698
_DEFAULT_BONDLESS_MAKERS_ALLOWANCE = "0.125"
9799

98100
defaultconfig = \
@@ -673,9 +675,8 @@ def _remove_unwanted_default_settings(config: ConfigParser) -> None:
673675
if section.startswith('MESSAGING:'):
674676
config.remove_section(section)
675677

676-
def load_program_config(config_path: str = "", bs: Optional[str] = None,
677-
plugin_services: List[JMPluginService] = []) -> None:
678-
global_singleton.config.read_file(io.StringIO(defaultconfig))
678+
679+
def set_paths(config_path: str = "") -> None:
679680
if not config_path:
680681
config_path = lookup_appdata_folder(global_singleton.APPNAME)
681682
# we set the global home directory, but keep the config_path variable
@@ -692,29 +693,70 @@ def load_program_config(config_path: str = "", bs: Optional[str] = None,
692693
if not os.path.exists(os.path.join(global_singleton.datadir, "cmtdata")):
693694
os.makedirs(os.path.join(global_singleton.datadir, "cmtdata"))
694695
global_singleton.config_location = os.path.join(
695-
global_singleton.datadir, global_singleton.config_location)
696+
global_singleton.datadir, global_singleton.config_location
697+
)
696698

697-
_remove_unwanted_default_settings(global_singleton.config)
699+
700+
def read_config_file() -> bool:
698701
try:
699-
loadedFiles = global_singleton.config.read(
700-
[global_singleton.config_location])
702+
loaded = global_singleton.config.read([global_singleton.config_location])
701703
except UnicodeDecodeError:
702-
jmprint("Error loading `joinmarket.cfg`, invalid file format.",
703-
"info")
704+
jmprint("Error loading `joinmarket.cfg`, invalid file format.", "info")
704705
sys.exit(EXIT_FAILURE)
706+
return len(loaded) == 1
707+
705708

709+
def write_config_file(config: str = defaultconfig) -> bool:
710+
with open(global_singleton.config_location, "w") as configfile:
711+
configfile.write(config)
712+
jmprint(
713+
"Created a new `joinmarket.cfg`. Please review and adopt the "
714+
"settings and restart joinmarket.",
715+
"info",
716+
)
717+
sys.exit(EXIT_FAILURE)
718+
719+
720+
def override(config: ConfigParser, bs: Optional[str] = None) -> None:
721+
_remove_unwanted_default_settings(config)
706722
# Hack required for bitcoin-rpc-no-history and probably others
707723
# (historicaly electrum); must be able to enforce a different blockchain
708724
# interface even in default/new load.
709725
if bs:
710-
global_singleton.config.set("BLOCKCHAIN", "blockchain_source", bs)
711-
# Create default config file if not found
712-
if len(loadedFiles) != 1:
713-
with open(global_singleton.config_location, "w") as configfile:
714-
configfile.write(defaultconfig)
715-
jmprint("Created a new `joinmarket.cfg`. Please review and adopt the "
716-
"settings and restart joinmarket.", "info")
717-
sys.exit(EXIT_FAILURE)
726+
config.set("BLOCKCHAIN", "blockchain_source", bs)
727+
728+
729+
def override_from_environment(config: ConfigParser) -> ConfigParser:
730+
for key, value in os.environ.items():
731+
if key.startswith(_ENV_VAR_PREFIX):
732+
key = key.removeprefix(_ENV_VAR_PREFIX)
733+
section, key = key.split("_", 1)
734+
if section in _SECTIONS_WITH_SUBSECTIONS:
735+
sub, key = key.split("_", 1)
736+
section = f"{section}:{sub.lower()}"
737+
key = key.lower()
738+
if not config.has_section(section):
739+
config.add_section(section)
740+
log.info(f"Overriding [{section}] {key}={value}")
741+
config.set(section, key, value)
742+
return config
743+
744+
745+
def load_program_config(
746+
config_path: str = "",
747+
bs: Optional[str] = None,
748+
plugin_services: List[JMPluginService] = [],
749+
from_environment: bool = False,
750+
) -> None:
751+
set_paths(config_path)
752+
global_singleton.config.read_string(defaultconfig)
753+
if from_environment:
754+
override_from_environment(global_singleton.config)
755+
else:
756+
override(global_singleton.config, bs)
757+
# Create default config file if not found
758+
if not read_config_file():
759+
write_config_file()
718760

719761
loglevel = global_singleton.config.get("LOGGING", "console_log_level")
720762
try:

test/jmclient/test_configure.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
'''test configure module.'''
22

3+
import os
4+
from configparser import ConfigParser
5+
36
import pytest
4-
from jmclient import load_test_config, jm_single
5-
from jmclient.configure import get_blockchain_interface_instance
7+
8+
from jmclient import jm_single, load_test_config
9+
from jmclient.configure import (
10+
get_blockchain_interface_instance,
11+
override_from_environment,
12+
)
613

714
pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind")
815

@@ -35,3 +42,33 @@ def test_blockchain_sources():
3542
else:
3643
get_blockchain_interface_instance(jm_single().config)
3744
load_test_config()
45+
46+
47+
def test_override_from_environment():
48+
config = ConfigParser()
49+
config.add_section("BLOCKCHAIN")
50+
config.add_section("POLICY")
51+
52+
overrides = {
53+
"JM_BLOCKCHAIN_NETWORK": "testnet",
54+
"JM_POLICY_TX_FEES": "1000",
55+
"JM_MESSAGING_ONION_TYPE": "onion",
56+
"JM_DAEMON_DAEMON_PORT": "27184",
57+
}
58+
for key, value in overrides.items():
59+
os.environ[key] = value
60+
61+
try:
62+
# Override config from environment
63+
override_from_environment(config)
64+
65+
# Verify overrides were applied
66+
assert config.get("BLOCKCHAIN", "network") == "testnet"
67+
assert config.get("POLICY", "tx_fees") == "1000"
68+
assert config.get("MESSAGING:onion", "type") == "onion"
69+
assert config.get("DAEMON", "daemon_port") == "27184"
70+
finally:
71+
# Clean up environment variables
72+
for key in overrides.keys():
73+
if key in os.environ:
74+
del os.environ[key]

0 commit comments

Comments
 (0)