Skip to content

Commit 6ee87a0

Browse files
committed
✨ Add support for env variables
1 parent 3b3d747 commit 6ee87a0

File tree

4 files changed

+73
-10
lines changed

4 files changed

+73
-10
lines changed

main.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,20 @@ def check_module(module_name: str) -> bool:
1414
def main():
1515
bot = discord.Bot(intents=discord.Intents.default())
1616

17-
logger = get_logger(name="Main")
17+
logger = get_logger(name="main")
1818
logger.info("Starting bot")
1919

2020
for extension, its_config in config["extensions"].items():
2121
if its_config["enabled"]:
22+
_logger = get_logger(name=extension)
2223
if check_module(f"src.extensions.{extension}"):
2324
logger.info(f"Loading extension {extension}")
2425
module = importlib.import_module(f"src.extensions.{extension}")
25-
module.setup(bot, logger, its_config)
26+
module.setup(bot, _logger, its_config)
2627
elif check_module(extension):
2728
logger.info(f"Loading extension {extension}")
2829
module = importlib.import_module(extension)
29-
module.setup(bot, logger, its_config)
30+
module.setup(bot, _logger, its_config)
3031
else:
3132
logger.error(f"Extension {extension} not found")
3233

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ py-cord = "^2.5.0"
1313
aiohttp = "^3.9.5"
1414
pyyaml = "^6.0.1"
1515
python-dotenv = "^1.0.1"
16+
orjson = "^3.10.3"
1617

1718

1819
[tool.poetry.group.dev.dependencies]

readme.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ poetry install
3434

3535
## Setup
3636

37-
1. Set up the `config.yml` file with your bot token and desired extensions. Here's an example configuration:
37+
### Yaml Configuration
38+
You can set up the `config.yml` file with your bot token and desired extensions. Here's an example configuration:
3839

3940
```yaml
4041
extensions:
@@ -49,6 +50,15 @@ logging:
4950
level: INFO
5051
```
5152
53+
### Environment Variables
54+
Alternatively, you can set the bot token and other configuration options using environment variables. You can set any variable as you would in the `config.yml` file, but with the `BOTKIT__` prefix, and `__` to separate nested keys. To set lists, use regular json syntax.
55+
```env
56+
BOTKIT__bot__token=your_bot_token
57+
BOTKIT__extensions__topgg__enabled=false
58+
BOTKIT__extensions__ping__enabled=true
59+
BOTKIT__logging__level=INFO
60+
```
61+
5262
## Default Extensions
5363

5464
### Ping Extension
@@ -78,7 +88,7 @@ def setup(bot: discord.Bot, logger: logging.Logger, config: dict):
7888

7989
- `bot`: The Discord bot instance.
8090
- `logger`: A logger instance for logging messages.
81-
- `config`: The configuration dictionary for the extension from the `config.yml` file.
91+
- `config`: The configuration dictionary for the extension from the `config.yml` file. All config keys will always be lowercased for compatibility with environment variables.
8292

8393
## Contributing
8494

src/config/bot_config.py

+56-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,66 @@
11
import yaml
22
import os
3+
import orjson
34

5+
from dotenv import load_dotenv
46
from typing import Any
57

8+
load_dotenv()
9+
10+
SPLIT: str = "_"
11+
12+
13+
def load_from_env() -> dict[str, dict[str, Any]]:
14+
15+
_config = {}
16+
values = {k: v for k, v in os.environ.items() if k.startswith(f"BOTKIT{SPLIT}")}
17+
values = {k[len(f"BOTKIT{SPLIT}") :]: v for k, v in values.items()}
18+
for key, value in values.items():
19+
for i, part in enumerate(key.split(SPLIT)):
20+
part = part.lower()
21+
if i == 0:
22+
if part not in _config:
23+
_config[part] = {}
24+
current = _config[part]
25+
elif i == len(key.split(SPLIT)) - 1:
26+
current[part] = value
27+
else:
28+
if part not in current:
29+
current[part] = {}
30+
elif not isinstance(current[part], dict):
31+
raise ValueError(f"Key {key} in environment must be a leaf")
32+
current = current[part]
33+
34+
return load_json_recursive(_config)
35+
36+
37+
def load_json_recursive(data: dict[str, Any]) -> dict[str, Any]:
38+
for key, value in data.items():
39+
if isinstance(value, dict):
40+
data[key] = load_json_recursive(value)
41+
elif isinstance(value, str):
42+
if value.lower() == "true":
43+
data[key] = True
44+
elif value.lower() == "false":
45+
data[key] = False
46+
else:
47+
try:
48+
data[key] = orjson.loads(value)
49+
except orjson.JSONDecodeError:
50+
pass
51+
return data
52+
53+
54+
path = None
655
if os.path.exists("config.yaml"):
756
path = "config.yaml"
857
elif os.path.exists("config.yml"):
958
path = "config.yml"
10-
else:
11-
raise FileNotFoundError("Configuration file not found")
1259

13-
# noinspection PyArgumentEqualDefault
14-
with open(path, "r", encoding="utf-8") as f:
15-
config: dict[str, dict[str, Any]] = yaml.safe_load(f)
60+
61+
if path:
62+
# noinspection PyArgumentEqualDefault
63+
with open(path, "r", encoding="utf-8") as f:
64+
config: dict[str, dict[str, Any]] = yaml.safe_load(f)
65+
else:
66+
config: dict[str, dict[str, Any]] = load_from_env()

0 commit comments

Comments
 (0)