Coverage for aiocoap / config.py: 94%
93 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 12:28 +0000
« 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
5"""Configurable behavior of aiocoap.
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.
11The two main places to enter them are:
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.
18There are two main ways to create them:
20* Programmatically by constructing the classes.
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.
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.
29* Loading from JSON, TOML, or (provided dependencies are present) YAML, CBOR or CBOR Diagnostic Notation (EDN) files.
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.
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::
40 [transport.udp6]
41 bind = ["[::]:5683", "[::]:61616"]
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.
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>`::
51 >>> config = Config.load({"transport": {"udp6": {"bind": ["[::]:5683", "[::]:61616"]}}})
52 >>> config.transport.udp6 # doctest: +ELLIPSIS
53 Udp6Parameters(bind=['[::]:5683', '[::]:61616'], ...)
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.
59Stability
60---------
62This module is regarded as stable; breaking changes will be pointed out through
63semver releases.
65Main classes
66------------
68.. autoclass:: Config
69 :members:
70.. autoclass:: TransportParameters
71 :members:
73Transport specifics
74-------------------
75"""
77from dataclasses import dataclass, field
78from pathlib import Path
79from typing import Optional, Self
81from .util.dataclass_data import LoadStoreClass
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).
89@dataclass
90class Udp6Parameters(LoadStoreClass):
91 """Parameters for setting up a :mod:`udp6 <aiocoap.transports.udp6>` transport."""
93 bind: Optional[list[str]] = None
94 """Address and port to bind to.
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.
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.
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.
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 .
127 reuse_port: Optional[bool] = None
128 """Use the SO_REUSEPORT option.
130 It has the effect that multiple Python processes can be bound to the same
131 port to get some implicit load balancing.
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.
140@dataclass
141class Simple6Parameters(LoadStoreClass):
142 """Parameters for setting up a :mod:`simple6 <aiocoap.transports.simple6>` transport."""
145@dataclass
146class SimpleSocketServerParameters(LoadStoreClass):
147 """Parameters for setting up a :mod:`simplesocketserver <aiocoap.transports.simplesocketserver>` transport."""
150@dataclass
151class TinyDTLSParameters(LoadStoreClass):
152 """Parameters for setting up a :mod:`tinydtls <aiocoap.transports.tinydtls>` transport."""
155@dataclass
156class TinyDTLSServerParameters(LoadStoreClass):
157 """Parameters for setting up a :mod:`tinydtls_server <aiocoap.transports.tinydtls_server>` transport."""
160@dataclass
161class TcpClientParameters(LoadStoreClass):
162 """Parameters for setting up a :mod:`tcpclient <aiocoap.transports.tcpclient>` transport."""
165@dataclass
166class TcpServerParameters(LoadStoreClass):
167 """Parameters for setting up a :mod:`tcpserver <aiocoap.transports.tcpserver>` transport."""
170@dataclass
171class TlsClientParameters(LoadStoreClass):
172 """Parameters for setting up a :mod:`tlsclient <aiocoap.transports.tlsclient>` transport."""
175@dataclass
176class TlsServerParameters(LoadStoreClass):
177 """Parameters for setting up a :mod:`tlsserver <aiocoap.transports.tlsserver>` transport."""
180@dataclass
181class WsParameters(LoadStoreClass):
182 """Parameters for setting up a :mod:`ws <aiocoap.transports.ws>` transport."""
185@dataclass
186class OscoreParameters(LoadStoreClass):
187 """Parameters for setting up an :mod:`oscore <aiocoap.transports.oscore>` transport."""
190@dataclass
191class SlipmuxDevice(LoadStoreClass):
192 """Parameters for a single slipmux device.
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)"""
198 device: Optional[Path] = None
199 """Overrides the path at which the device file is expected.
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."""
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."""
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."""
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 )
226@dataclass
227class SlipmuxParameters(LoadStoreClass):
228 """Parameters for setting up a :mod:`slipmux <aiocoap.transports.slipmux>` transport."""
230 devices: dict[str, SlipmuxDevice] = field(default_factory=dict)
231 """Details of known slipmux devices.
233 The keys are the "devname" part of the ``coap://devname.dev.alt`` origins
234 used with slimux.
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 """
243@dataclass
244class TransportParameters(LoadStoreClass):
245 """Parameters that guide which transports are selected and how they are
246 configured.
248 :meta private:
249 (not actually private, just hiding from automodule due to being
250 explicitly called out.
251 """
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.
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).
264 >>> TransportParameters._compat_create(None) == TransportParameters(default_transports=True)
265 True
266 """
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 )
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).
286 This only applies any changes if ``.default_transports`` is present. It
287 expects ``.is_server`` to be decided already."""
289 from . import defaults
291 if not self.default_transports:
292 return
294 if self.is_server:
295 transports = defaults.get_default_servertransports()
296 else:
297 transports = defaults.get_default_clienttransports()
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))
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).
312 Leaving this unset allows the parameters to be set when creating the
313 context."""
315 default_transports: bool = False
316 """If True, all transports that are on by default (or selected by the
317 environment) are enabled.
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."""
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
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."""
346@dataclass
347class Config(LoadStoreClass):
348 """Configuration for aiocoap
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.
354 :meta private:
355 (not actually private, just hiding from automodule due to being
356 explicitly called out.
357 """
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."""