Coverage for src/mafw/db/db_connection.py: 100%
97 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-26 09:13 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-26 09:13 +0000
1# Copyright 2026 European Union
2# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
3# SPDX-License-Identifier: EUPL-1.2
4"""
5Database connection configuration helpers.
7:Author: Bulgheroni Antonio
8:Description: Normalize database steering configuration into peewee connection arguments.
9"""
11from __future__ import annotations
13import os
14import warnings
15from collections.abc import Mapping
16from typing import Any
18from mafw.mafw_errors import SteeringFileDeprecation
19from mafw.tools.regexp import extract_protocol
21VALID_AUTH_METHODS: set[str] = {'env', 'inline', 'file'}
22"""Supported authentication methods for ``DBConfiguration.authentication``."""
24LEGACY_KEYS: set[str] = {'URL', 'pragmas'}
25"""Keys reserved by the legacy DBConfiguration schema."""
28def build_connection_parameters(config: Mapping[str, Any]) -> tuple[str, dict[str, Any]]:
29 """Normalize DBConfiguration mappings into peewee connection arguments.
31 The input can either be the full steering configuration dictionary or the
32 nested ``DBConfiguration`` table. The returned tuple provides the URL and
33 the keyword arguments for ``peewee.connect``.
35 :param config: Steering configuration or DBConfiguration table.
36 :type config: Mapping[str, Any]
37 :return: Tuple with database URL and connection keyword arguments.
38 :rtype: tuple[str, dict[str, Any]]
39 :raises ValueError: When required fields are missing or invalid.
40 """
42 conf = dict(config)
43 if 'DBConfiguration' in conf:
44 conf = dict(conf['DBConfiguration'])
46 url = conf.get('URL')
47 if not url:
48 raise ValueError('DBConfiguration must define a URL field.')
50 protocol = extract_protocol(str(url)) or ''
51 is_new_style = 'authentication' in conf or 'parameters' in conf
53 if not is_new_style:
54 warnings.warn(
55 'Legacy DBConfiguration schema detected. Please update to the new authentication/parameters layout.',
56 SteeringFileDeprecation,
57 stacklevel=2,
58 )
59 return str(url), _legacy_connection_parameters(protocol, conf)
61 connection_parameters: dict[str, Any] = {}
62 _apply_backend_parameters(protocol, conf, connection_parameters)
63 _apply_authentication(protocol, conf.get('authentication'), connection_parameters)
65 return str(url), connection_parameters
68def _legacy_connection_parameters(protocol: str, conf: Mapping[str, Any]) -> dict[str, Any]:
69 connection_parameters: dict[str, Any] = {}
70 if protocol == 'sqlite':
71 connection_parameters['pragmas'] = conf.get('pragmas', {})
72 for key, value in conf.items():
73 if key in LEGACY_KEYS:
74 continue
75 connection_parameters[key] = value
76 return connection_parameters
79def _apply_backend_parameters(protocol: str, conf: Mapping[str, Any], target: dict[str, Any]) -> None:
80 parameters = conf.get('parameters')
81 if not isinstance(parameters, Mapping):
82 return
84 backend_params = parameters.get(protocol)
85 if not isinstance(backend_params, Mapping):
86 return
88 backend_dict = dict(backend_params)
89 if protocol == 'sqlite':
90 pragmas = backend_dict.pop('pragmas', None)
91 if isinstance(pragmas, Mapping):
92 target['pragmas'] = dict(pragmas)
93 for key, value in backend_dict.items():
94 target[key] = value
97def _apply_authentication(protocol: str, auth: Any, target: dict[str, Any]) -> None:
98 if protocol == 'sqlite':
99 return
101 if auth is None or not isinstance(auth, Mapping):
102 raise ValueError('DBConfiguration.authentication must be defined for server-based databases.')
104 method = auth.get('method')
105 if method not in VALID_AUTH_METHODS:
106 raise ValueError(f'Unsupported authentication method: {method!r}.')
108 if method == 'env':
109 _apply_env_authentication(auth, target)
110 return
112 if method == 'inline':
113 warnings.warn(
114 'Inline database credentials are insecure. Prefer environment-based authentication.',
115 UserWarning,
116 stacklevel=2,
117 )
118 _apply_inline_authentication(auth, target)
119 return
121 if method == 'file':
122 _apply_file_authentication(protocol, auth, target)
123 return
126def _apply_env_authentication(auth: Mapping[str, Any], target: dict[str, Any]) -> None:
127 if 'username' in auth and auth['username'] is not None:
128 env_name = str(auth['username'])
129 value = os.getenv(env_name)
130 if value is None:
131 raise ValueError(f'Environment variable {env_name!r} for DB username is not set.')
132 target['user'] = value
133 if 'password' in auth and auth['password'] is not None:
134 env_name = str(auth['password'])
135 value = os.getenv(env_name)
136 if value is None:
137 raise ValueError(f'Environment variable {env_name!r} for DB password is not set.')
138 target['password'] = value
141def _apply_inline_authentication(auth: Mapping[str, Any], target: dict[str, Any]) -> None:
142 if 'username' in auth and auth['username'] is not None:
143 target['user'] = auth['username']
144 if 'password' in auth and auth['password'] is not None:
145 target['password'] = auth['password']
148def _apply_file_authentication(protocol: str, auth: Mapping[str, Any], target: dict[str, Any]) -> None:
149 if 'username' in auth and auth['username'] is not None:
150 target['user'] = auth['username']
152 passfile = auth.get('passfile')
153 if passfile is None:
154 return
156 if protocol == 'mysql':
157 target['read_default_file'] = passfile
158 elif protocol == 'postgresql':
159 target['passfile'] = passfile
160 else:
161 raise ValueError(f'Authentication method "file" is not supported for {protocol!r}.')