Passed
Push — main ( 3fbf5b...7fa90f )
by Jochen
02:02
created

weitersager.config   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 175
Duplicated Lines 0 %

Test Coverage

Coverage 95.7%

Importance

Changes 0
Metric Value
wmc 15
eloc 105
dl 0
loc 175
ccs 89
cts 93
cp 0.957
rs 10
c 0
b 0
f 0

7 Functions

Rating   Name   Duplication   Size   Complexity  
A _get_log_level() 0 7 2
A load_config() 0 12 1
A _get_irc_channels() 0 5 2
A _get_irc_server() 0 17 4
A _get_irc_config() 0 15 1
A _get_http_config() 0 9 1
A _get_channel_tokens_to_channel_names() 0 19 4
1
"""
2
weitersager.config
3
~~~~~~~~~~~~~~~~~~
4
5
Configuration loading
6
7
:Copyright: 2007-2024 Jochen Kupperschmidt
8
:License: MIT, see LICENSE for details.
9
"""
10
11 1
from __future__ import annotations
12 1
from dataclasses import dataclass
13 1
from pathlib import Path
14 1
from typing import Any, Iterator, Optional
15
16 1
import rtoml
17
18
19 1
DEFAULT_HTTP_HOST = '127.0.0.1'
20 1
DEFAULT_HTTP_PORT = 8080
21 1
DEFAULT_IRC_SERVER_PORT = 6667
22 1
DEFAULT_IRC_REALNAME = 'Weitersager'
23
24
25 1
class ConfigurationError(Exception):
26
    """Indicates a configuration error."""
27
28
29 1
@dataclass(frozen=True)
30 1
class Config:
31 1
    log_level: str
32 1
    http: HttpConfig
33 1
    irc: IrcConfig
34
35
36 1
@dataclass(frozen=True)
37 1
class HttpConfig:
38
    """An HTTP receiver configuration."""
39
40 1
    host: str
41 1
    port: int
42 1
    api_tokens: set[str]
43 1
    channel_tokens_to_channel_names: dict[str, str]
44
45
46 1
@dataclass(frozen=True)
47 1
class IrcServer:
48
    """An IRC server."""
49
50 1
    host: str
51 1
    port: int = DEFAULT_IRC_SERVER_PORT
52 1
    ssl: bool = False
53 1
    password: Optional[str] = None
54 1
    rate_limit: Optional[float] = None
55
56
57 1
@dataclass(frozen=True, order=True)
58 1
class IrcChannel:
59
    """An IRC channel."""
60
61 1
    name: str
62 1
    password: Optional[str] = None
63
64
65 1
@dataclass(frozen=True)
66 1
class IrcConfig:
67
    """An IRC bot configuration."""
68
69 1
    server: Optional[IrcServer]
70 1
    nickname: str
71 1
    realname: str
72 1
    commands: list[str]
73 1
    channels: set[IrcChannel]
74
75
76 1
def load_config(path: Path) -> Config:
77
    """Load configuration from file."""
78 1
    data = rtoml.load(path)
79
80 1
    log_level = _get_log_level(data)
81 1
    http_config = _get_http_config(data)
82 1
    irc_config = _get_irc_config(data)
83
84 1
    return Config(
85
        log_level=log_level,
86
        http=http_config,
87
        irc=irc_config,
88
    )
89
90
91 1
def _get_log_level(data: dict[str, Any]) -> str:
92 1
    level = data.get('log_level', 'debug').upper()
93
94 1
    if level not in {'CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'}:
95
        raise ConfigurationError(f'Unknown log level "{level}"')
96
97 1
    return level
98
99
100 1
def _get_http_config(data: dict[str, Any]) -> HttpConfig:
101 1
    data_http = data.get('http', {})
102
103 1
    host = data_http.get('host', DEFAULT_HTTP_HOST)
104 1
    port = int(data_http.get('port', DEFAULT_HTTP_PORT))
105 1
    api_tokens = set(data_http.get('api_tokens', []))
106 1
    channel_tokens_to_channel_names = _get_channel_tokens_to_channel_names(data)
107
108 1
    return HttpConfig(host, port, api_tokens, channel_tokens_to_channel_names)
109
110
111 1
def _get_channel_tokens_to_channel_names(
112
    data: dict[str, Any],
113
) -> dict[str, str]:
114 1
    channel_tokens_to_channel_names = {}
115
116 1
    for channel in data['irc'].get('channels', []):
117 1
        channel_name = channel['name']
118
119 1
        tokens = set(channel.get('tokens', []))
120 1
        for token in tokens:
121
            if token in channel_tokens_to_channel_names:
122
                raise ConfigurationError(
123
                    f'A channel token for channel "{channel_name}" '
124
                    'is already configured somewhere else.'
125
                )
126
127
            channel_tokens_to_channel_names[token] = channel_name
128
129 1
    return channel_tokens_to_channel_names
130
131
132 1
def _get_irc_config(data: dict[str, Any]) -> IrcConfig:
133 1
    data_irc = data['irc']
134
135 1
    server = _get_irc_server(data_irc)
136 1
    nickname = data_irc['bot']['nickname']
137 1
    realname = data_irc['bot'].get('realname', DEFAULT_IRC_REALNAME)
138 1
    commands = data_irc.get('commands', [])
139 1
    channels = set(_get_irc_channels(data_irc))
140
141 1
    return IrcConfig(
142
        server=server,
143
        nickname=nickname,
144
        realname=realname,
145
        commands=commands,
146
        channels=channels,
147
    )
148
149
150 1
def _get_irc_server(data_irc: Any) -> Optional[IrcServer]:
151 1
    data_server = data_irc.get('server')
152 1
    if data_server is None:
153 1
        return None
154
155 1
    host = data_server.get('host')
156 1
    if not host:
157 1
        return None
158
159 1
    port = int(data_server.get('port', DEFAULT_IRC_SERVER_PORT))
160 1
    ssl = data_server.get('ssl', False)
161 1
    password = data_server.get('password')
162 1
    rate_limit_str = data_server.get('rate_limit')
163 1
    rate_limit = float(rate_limit_str) if rate_limit_str else None
164
165 1
    return IrcServer(
166
        host=host, port=port, ssl=ssl, password=password, rate_limit=rate_limit
167
    )
168
169
170 1
def _get_irc_channels(data_irc: Any) -> Iterator[IrcChannel]:
171 1
    for channel in data_irc.get('channels', []):
172 1
        name = channel['name']
173 1
        password = channel.get('password')
174
        yield IrcChannel(name, password)
175