Passed
Branch main (668435)
by Jochen
03:33 queued 01:35
created

_get_channel_tokens_to_channel_names()   A

Complexity

Conditions 4

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.432

Importance

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