Coverage for aiocoap / config.py: 94%

93 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-17 12:28 +0000

1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors 

2# 

3# SPDX-License-Identifier: MIT 

4 

5"""Configurable behavior of aiocoap. 

6 

7Classes of this module can be used to customize aspects of aiocoap operation 

8that are generally not part of a written application, such as which addresses 

9to bind to or which CoAP transports to make available. 

10 

11The two main places to enter them are: 

12 

13* Passing :class:`TransportParameters` as an argument to 

14 :meth:`Context.create_client_context() <aiocoap.protocol.Context.create_client_context>` 

15 / :meth:`create_server_context() <aiocoap.protocol.Context.create_server_context>`. 

16* A :class:`Config` being processed by many :doc:`/tools` in their ``--server-config`` argument. 

17 

18There are two main ways to create them: 

19 

20* Programmatically by constructing the classes. 

21 

22 All these classes are standard Python dataclasses, and can be populated in a 

23 constructor or be built up after constructing them with default values. 

24 

25 This is practical when the application has its own configuration format that 

26 is exposed to the user, or when in ad-hoc scripts where the configuration is 

27 hard-coded. 

28 

29* Loading from JSON, TOML, or (provided dependencies are present) YAML, CBOR or CBOR Diagnostic Notation (EDN) files. 

30 

31 All types in here are instances of 

32 :class:`LoadStoreClass <aiocoap.util.dataclass_data.LoadStoreClass>`, and 

33 thus have a :meth:`.load_from_file() 

34 <aiocoap.util.dataclass_data.LoadStoreClass.load_from_file>` method by which 

35 they can be loaded. 

36 

37 For most data types, the common information model has a trivial representation -- 

38 for example, a full config that contains a transport details for UDP can be:: 

39 

40 [transport.udp6] 

41 bind = ["[::]:5683", "[::]:61616"] 

42 

43 Some types in the items receive special handling to make them easier to use: 

44 For example, any ``Path`` items are, when relative, resolved based on the 

45 loaded file's location. 

46 

47* There exists the hybrid option of creating the deserialized version of such a 

48 configuration file in Python, and passing it to :meth:`.load() 

49 <aiocoap.util.dataclass_data.LoadStoreClass.load>`:: 

50 

51 >>> config = Config.load({"transport": {"udp6": {"bind": ["[::]:5683", "[::]:61616"]}}}) 

52 >>> config.transport.udp6 # doctest: +ELLIPSIS 

53 Udp6Parameters(bind=['[::]:5683', '[::]:61616'], ...) 

54 

55.. 

56 Grouped to the front as they are most imnportant; code can be restructured 

57 when all supported Python versions have lazy annotation loading. 

58 

59Stability 

60--------- 

61 

62This module is regarded as stable; breaking changes will be pointed out through 

63semver releases. 

64 

65Main classes 

66------------ 

67 

68.. autoclass:: Config 

69 :members: 

70.. autoclass:: TransportParameters 

71 :members: 

72 

73Transport specifics 

74------------------- 

75""" 

76 

77from dataclasses import dataclass, field 

78from pathlib import Path 

79from typing import Optional, Self 

80 

81from .util.dataclass_data import LoadStoreClass 

82 

83# The concrete per-transport data classes are *not* part of the 

84# aiocoap/transport/ files to avoid eagerly loading them. (And let's see, maybe 

85# they rackup so many commonalities that it doesn't make sense to have them per 

86# tranport anyway). 

87 

88 

89@dataclass 

90class Udp6Parameters(LoadStoreClass): 

91 """Parameters for setting up a :mod:`udp6 <aiocoap.transports.udp6>` transport.""" 

92 

93 bind: Optional[list[str]] = None 

94 """Address and port to bind to. 

95 

96 Addresses without a port are bound to the default port (5683). Binding to an 

97 address at the unspecified port is possible by explicitly giving the port 

98 as ``:0``; the choice of a port of an ephemeral port will be left to the 

99 operating system. 

100 

101 The default value when nothing is given explicitly depends on whether 

102 a server is run (then it's ``["[::]"]`` implying port 5683) or not (then 

103 it's effectively ``["[::]:0"]``, although the ``bind`` syscall may be elided 

104 in that case). The default is altered when the to-be-deprecated ``bind`` 

105 argument is passed at server creation. 

106 

107 Multiple explicit ports can be bound to, both by listing them explicitly, 

108 and by giving names that are resolved (at startup) to multiple 

109 addresses. 

110 

111 Currently, only the first address (first resolved value of the first 

112 address) is used for outgoing requests (more precisley, for outgoing 

113 requests with an ``UnspecifiedRemote`` remote; outgoing requests from role 

114 reversal always stick with their addresses). That means that unless the 

115 first item is a ``[::]`` unspecified address, only that addresse's IP version 

116 will work for outgoing requests. A convenient workaround is to bind to 

117 ``[::]:0`` first, which sends all such outgoing requests through a random 

118 port chosen by the OS at startup, and then list the concrete addresses to 

119 bind to.""" 

120 # FIXME: How do we assist users in taking SIGHUP (or whatever is a 

121 # convention) and re-evaluating at least the DNS names? 

122 # 

123 # Typing is authority-style string rather than (host, port) for ease of 

124 # configuration use, and as a soft hint that at some point we might be 

125 # doing SVCB resolution on the names and . 

126 

127 reuse_port: Optional[bool] = None 

128 """Use the SO_REUSEPORT option. 

129 

130 It has the effect that multiple Python processes can be bound to the same 

131 port to get some implicit load balancing. 

132 

133 The default is currently True if available on the platform, and also 

134 influenced by the (hereby deprecated) AIOCOAP_REUSE_PORT environment 

135 variable.""" 

136 # FIXME: Check if bind-after-shutdown is still an issue; if so, document 

137 # why this is useful. 

138 

139 

140@dataclass 

141class Simple6Parameters(LoadStoreClass): 

142 """Parameters for setting up a :mod:`simple6 <aiocoap.transports.simple6>` transport.""" 

143 

144 

145@dataclass 

146class SimpleSocketServerParameters(LoadStoreClass): 

147 """Parameters for setting up a :mod:`simplesocketserver <aiocoap.transports.simplesocketserver>` transport.""" 

148 

149 

150@dataclass 

151class TinyDTLSParameters(LoadStoreClass): 

152 """Parameters for setting up a :mod:`tinydtls <aiocoap.transports.tinydtls>` transport.""" 

153 

154 

155@dataclass 

156class TinyDTLSServerParameters(LoadStoreClass): 

157 """Parameters for setting up a :mod:`tinydtls_server <aiocoap.transports.tinydtls_server>` transport.""" 

158 

159 

160@dataclass 

161class TcpClientParameters(LoadStoreClass): 

162 """Parameters for setting up a :mod:`tcpclient <aiocoap.transports.tcpclient>` transport.""" 

163 

164 

165@dataclass 

166class TcpServerParameters(LoadStoreClass): 

167 """Parameters for setting up a :mod:`tcpserver <aiocoap.transports.tcpserver>` transport.""" 

168 

169 

170@dataclass 

171class TlsClientParameters(LoadStoreClass): 

172 """Parameters for setting up a :mod:`tlsclient <aiocoap.transports.tlsclient>` transport.""" 

173 

174 

175@dataclass 

176class TlsServerParameters(LoadStoreClass): 

177 """Parameters for setting up a :mod:`tlsserver <aiocoap.transports.tlsserver>` transport.""" 

178 

179 

180@dataclass 

181class WsParameters(LoadStoreClass): 

182 """Parameters for setting up a :mod:`ws <aiocoap.transports.ws>` transport.""" 

183 

184 

185@dataclass 

186class OscoreParameters(LoadStoreClass): 

187 """Parameters for setting up an :mod:`oscore <aiocoap.transports.oscore>` transport.""" 

188 

189 

190@dataclass 

191class SlipmuxDevice(LoadStoreClass): 

192 """Parameters for a single slipmux device. 

193 

194 By default, establishes a connection by looking up the name 

195 case-insensitively in ``/dev/`` (which works for UNIXes), falling back to 

196 opening the device by its name (which probably works on Windows)""" 

197 

198 device: Optional[Path] = None 

199 """Overrides the path at which the device file is expected. 

200 

201 This can be useful when catering for device path renames, or when devices 

202 contain characters that are not trivially encoded in the Hostname component 

203 of a URI.""" 

204 

205 unix_connect: Optional[Path] = None 

206 """If set, connection is not made through a serial port but rather by 

207 connecting to a UNIX socket at that file name.""" 

208 

209 unix_listen: Optional[Path] = None 

210 """If set, connection is not made through a serial port but rather by 

211 creating and listening at a UNIX socket at that file name.""" 

212 

213 def __post_init__(self): 

214 if ( 

215 sum( 

216 f is not None 

217 for f in (self.device, self.unix_connect, self.unix_listen) 

218 ) 

219 > 1 

220 ): 

221 raise ValueError( 

222 "Only one (or none) of the 'device', 'unix-connect' and 'unix-listen' fields can be set per device." 

223 ) 

224 

225 

226@dataclass 

227class SlipmuxParameters(LoadStoreClass): 

228 """Parameters for setting up a :mod:`slipmux <aiocoap.transports.slipmux>` transport.""" 

229 

230 devices: dict[str, SlipmuxDevice] = field(default_factory=dict) 

231 """Details of known slipmux devices. 

232 

233 The keys are the "devname" part of the ``coap://devname.dev.alt`` origins 

234 used with slimux. 

235 

236 Setting an item is done for two practical effects: 

237 * It allows overriding properties (see :class:`SlipmuxParameters`). 

238 * When configured as a server, these are the ports that get connected 

239 at startup. 

240 """ 

241 

242 

243@dataclass 

244class TransportParameters(LoadStoreClass): 

245 """Parameters that guide which transports are selected and how they are 

246 configured. 

247 

248 :meta private: 

249 (not actually private, just hiding from automodule due to being 

250 explicitly called out. 

251 """ 

252 

253 @classmethod 

254 def _compat_create(cls, input: Self | None | dict | list[str]) -> Self: 

255 """Used to coerce transports= argument of 

256 ``create_{server,client}_context`` into this type. 

257 

258 It passes on any instance, loads from a JSON/CBOR/TOML style dict if 

259 present, selects the default transports when no data is given, and, in 

260 case of the legacy list-of-strings, sets them up as keys only 

261 (effectively choosing only those transports without any concrete 

262 configuration). 

263 

264 >>> TransportParameters._compat_create(None) == TransportParameters(default_transports=True) 

265 True 

266 """ 

267 

268 if isinstance(input, cls): 

269 return input 

270 elif input is None: 

271 return cls(default_transports=True) 

272 elif isinstance(input, dict): 

273 return cls.load(input) 

274 elif isinstance(input, list): 

275 return cls.load({k: {} for k in input}) 

276 else: 

277 raise ValueError( 

278 "Transports needs to bei either TransportParameters, or a dict that can be loaded as one, or None, or (deprecated) a list of transport names." 

279 ) 

280 

281 def _apply_defaults(self): 

282 """Modifies self to enable all transports from the 

283 :mod:`aiocoap.defaults` settings (which pulls in environment variables 

284 and installed modules). 

285 

286 This only applies any changes if ``.default_transports`` is present. It 

287 expects ``.is_server`` to be decided already.""" 

288 

289 from . import defaults 

290 

291 if not self.default_transports: 

292 return 

293 

294 if self.is_server: 

295 transports = defaults.get_default_servertransports() 

296 else: 

297 transports = defaults.get_default_clienttransports() 

298 

299 add_transports = [t for t in transports if getattr(self, t) is None] 

300 # We don't have good APIs to incrementally load, so we just create 

301 # something to splice into self 

302 empty_transports = self.load({k: {} for k in add_transports}) 

303 for t in add_transports: 

304 setattr(self, t, getattr(empty_transports, t)) 

305 

306 is_server: Optional[bool] = None 

307 """If True, in any place it applies, parameters for server operation are 

308 set. (For example, the UDP and TCP ports bind to the default port rather 

309 than an ephemeral port, and the default transports selection may be 

310 different). 

311 

312 Leaving this unset allows the parameters to be set when creating the 

313 context.""" 

314 

315 default_transports: bool = False 

316 """If True, all transports that are on by default (or selected by the 

317 environment) are enabled. 

318 

319 Note that this is False by default: If TransportParameters are given 

320 explicitly (by construction or by loading from JSON/CBOR/TOML style files), 

321 all transports are opt-in, and only when not specifying anything (or a 

322 legacy format) to the Context constructor, this gets set.""" 

323 

324 udp6: Udp6Parameters | None = None 

325 simple6: Simple6Parameters | None = None 

326 simplesocketserver: SimpleSocketServerParameters | None = None 

327 tinydtls: TinyDTLSParameters | None = None 

328 tinydtls_server: TinyDTLSServerParameters | None = None 

329 tcpclient: TcpClientParameters | None = None 

330 tcpserver: TcpServerParameters | None = None 

331 tlsclient: TlsClientParameters | None = None 

332 tlsserver: TlsServerParameters | None = None 

333 ws: WsParameters | None = None 

334 oscore: OscoreParameters | None = None 

335 slipmux: SlipmuxParameters | None = None 

336 

337 _legacy_bind = None 

338 """Stores the bind= argument of create_server_context, which will be 

339 deprecated in favor of setting up bindings per transport in this object.""" 

340 _legacy_multicast = None 

341 """Stores the multicast= argument of create_server_context, which will be 

342 deprecated in favor of setting those in the UDP transport in this 

343 object.""" 

344 

345 

346@dataclass 

347class Config(LoadStoreClass): 

348 """Configuration for aiocoap 

349 

350 An instance of this type covers aspects of aiocoap's behavior that are 

351 orthogonal to typical CoAP server or client applications, or for which an 

352 application would typically only forward configuration settings to. 

353 

354 :meta private: 

355 (not actually private, just hiding from automodule due to being 

356 explicitly called out. 

357 """ 

358 

359 transport: TransportParameters = field( 

360 default_factory=lambda: TransportParameters._compat_create(None) 

361 ) 

362 """Configuration for which transport protocols of CoAP are to be enabled, 

363 and how they are to be set up."""