enapter 0.6.3__py3-none-any.whl → 0.12.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. enapter/__init__.py +4 -4
  2. enapter/async_/__init__.py +1 -4
  3. enapter/async_/generator.py +5 -2
  4. enapter/async_/routine.py +21 -53
  5. enapter/http/__init__.py +3 -0
  6. enapter/http/api/__init__.py +5 -0
  7. enapter/http/api/client.py +31 -0
  8. enapter/http/api/config.py +23 -0
  9. enapter/http/api/devices/__init__.py +21 -0
  10. enapter/http/api/devices/authorized_role.py +11 -0
  11. enapter/http/api/devices/client.py +25 -0
  12. enapter/http/api/devices/communication_config.py +45 -0
  13. enapter/http/api/devices/device.py +32 -0
  14. enapter/http/api/devices/device_type.py +10 -0
  15. enapter/http/api/devices/mqtt_credentials.py +13 -0
  16. enapter/http/api/devices/mqtt_protocol.py +7 -0
  17. enapter/http/api/devices/mqtts_credentials.py +18 -0
  18. enapter/http/api/devices/time_sync_protocol.py +6 -0
  19. enapter/log/json_formatter.py +14 -4
  20. enapter/mdns/resolver.py +14 -11
  21. enapter/mqtt/__init__.py +5 -12
  22. enapter/mqtt/api/__init__.py +6 -0
  23. enapter/mqtt/api/client.py +62 -0
  24. enapter/mqtt/api/config.py +77 -0
  25. enapter/mqtt/api/device/__init__.py +21 -0
  26. enapter/mqtt/api/device/channel.py +56 -0
  27. enapter/mqtt/api/device/command_request.py +38 -0
  28. enapter/mqtt/api/device/command_response.py +28 -0
  29. enapter/mqtt/api/device/command_state.py +8 -0
  30. enapter/mqtt/api/device/log.py +31 -0
  31. enapter/mqtt/api/device/log_severity.py +9 -0
  32. enapter/mqtt/api/device/message.py +24 -0
  33. enapter/mqtt/api/device/properties.py +24 -0
  34. enapter/mqtt/api/device/telemetry.py +28 -0
  35. enapter/mqtt/client.py +88 -119
  36. enapter/mqtt/errors.py +3 -0
  37. enapter/mqtt/message.py +3 -0
  38. enapter/standalone/__init__.py +25 -0
  39. enapter/standalone/config.py +218 -0
  40. enapter/standalone/device.py +59 -0
  41. enapter/standalone/device_protocol.py +33 -0
  42. enapter/standalone/logger.py +21 -0
  43. enapter/standalone/mqtt_adapter.py +134 -0
  44. enapter/standalone/run.py +39 -0
  45. enapter/standalone/ucm.py +28 -0
  46. enapter-0.12.1.dist-info/METADATA +74 -0
  47. enapter-0.12.1.dist-info/RECORD +52 -0
  48. {enapter-0.6.3.dist-info → enapter-0.12.1.dist-info}/WHEEL +1 -1
  49. {enapter-0.6.3.dist-info → enapter-0.12.1.dist-info}/top_level.txt +0 -1
  50. enapter/mqtt/command.py +0 -45
  51. enapter/mqtt/config.py +0 -48
  52. enapter/mqtt/device_channel.py +0 -83
  53. enapter/types.py +0 -3
  54. enapter/vucm/__init__.py +0 -12
  55. enapter/vucm/app.py +0 -48
  56. enapter/vucm/config.py +0 -59
  57. enapter/vucm/device.py +0 -92
  58. enapter/vucm/logger.py +0 -39
  59. enapter/vucm/ucm.py +0 -30
  60. enapter-0.6.3.dist-info/METADATA +0 -111
  61. enapter-0.6.3.dist-info/RECORD +0 -27
  62. tests/test_async.py +0 -152
  63. tests/test_log.py +0 -69
  64. /tests/__init__.py → /enapter/py.typed +0 -0
enapter/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
- __version__ = "0.6.3"
1
+ __version__ = "0.12.1"
2
2
 
3
- from . import async_, log, mdns, mqtt, types, vucm
3
+ from . import async_, log, mdns, mqtt, http, standalone # isort: skip
4
4
 
5
5
  __all__ = [
6
6
  "__version__",
@@ -8,6 +8,6 @@ __all__ = [
8
8
  "log",
9
9
  "mdns",
10
10
  "mqtt",
11
- "types",
12
- "vucm",
11
+ "http",
12
+ "standalone",
13
13
  ]
@@ -1,7 +1,4 @@
1
1
  from .generator import generator
2
2
  from .routine import Routine
3
3
 
4
- __all__ = [
5
- "generator",
6
- "Routine",
7
- ]
4
+ __all__ = ["generator", "Routine"]
@@ -1,11 +1,14 @@
1
1
  import contextlib
2
2
  import functools
3
+ from typing import AsyncContextManager, AsyncGenerator, Callable
3
4
 
4
5
 
5
- def generator(func):
6
+ def generator(
7
+ func: Callable[..., AsyncGenerator],
8
+ ) -> Callable[..., AsyncContextManager[AsyncGenerator]]:
6
9
  @functools.wraps(func)
7
10
  @contextlib.asynccontextmanager
8
- async def wrapper(*args, **kwargs):
11
+ async def wrapper(*args, **kwargs) -> AsyncGenerator[AsyncGenerator, None]:
9
12
  gen = func(*args, **kwargs)
10
13
  try:
11
14
  yield gen
enapter/async_/routine.py CHANGED
@@ -1,69 +1,37 @@
1
1
  import abc
2
2
  import asyncio
3
3
  import contextlib
4
+ from typing import Self
4
5
 
5
6
 
6
7
  class Routine(abc.ABC):
8
+
9
+ def __init__(self, task_group: asyncio.TaskGroup | None = None) -> None:
10
+ self._task_group = task_group
11
+ self._task: asyncio.Task | None = None
12
+
7
13
  @abc.abstractmethod
8
- async def _run(self):
9
- raise NotImplementedError # pragma: no cover
14
+ async def _run(self) -> None:
15
+ pass # pragma: no cover
10
16
 
11
- async def __aenter__(self):
17
+ async def __aenter__(self) -> Self:
12
18
  await self.start()
13
19
  return self
14
20
 
15
- async def __aexit__(self, *_):
21
+ async def __aexit__(self, *_) -> None:
16
22
  await self.stop()
17
23
 
18
- def task(self):
19
- return self._task
20
-
21
- async def start(self, cancel_parent_task_on_exception=True):
22
- self._started = asyncio.Event()
23
- self._stack = contextlib.AsyncExitStack()
24
-
25
- self._parent_task = asyncio.current_task()
26
- self._cancel_parent_task_on_exception = cancel_parent_task_on_exception
27
-
28
- self._task = asyncio.create_task(self.__run())
29
- wait_started_task = asyncio.create_task(self._started.wait())
30
-
31
- done, _ = await asyncio.wait(
32
- {self._task, wait_started_task},
33
- return_when=asyncio.FIRST_COMPLETED,
34
- )
35
-
36
- if wait_started_task not in done:
37
- wait_started_task.cancel()
38
- try:
39
- await wait_started_task
40
- except asyncio.CancelledError:
41
- pass
42
-
43
- if self._task in done:
44
- self._task.result()
45
-
46
- async def stop(self):
47
- self.cancel()
48
- await self.join()
24
+ async def start(self) -> None:
25
+ if self._task is not None:
26
+ raise RuntimeError("already started")
27
+ if self._task_group is None:
28
+ self._task = asyncio.create_task(self._run())
29
+ else:
30
+ self._task = self._task_group.create_task(self._run())
49
31
 
50
- def cancel(self):
32
+ async def stop(self) -> None:
33
+ if self._task is None:
34
+ raise RuntimeError("not started yet")
51
35
  self._task.cancel()
52
-
53
- async def join(self):
54
- if self._task.done():
55
- self._task.result()
56
- else:
36
+ with contextlib.suppress(asyncio.CancelledError):
57
37
  await self._task
58
-
59
- async def __run(self):
60
- try:
61
- await self._run()
62
- except asyncio.CancelledError:
63
- pass
64
- except:
65
- if self._started.is_set() and self._cancel_parent_task_on_exception:
66
- self._parent_task.cancel()
67
- raise
68
- finally:
69
- await self._stack.aclose()
@@ -0,0 +1,3 @@
1
+ from . import api
2
+
3
+ __all__ = ["api"]
@@ -0,0 +1,5 @@
1
+ from . import devices
2
+ from .client import Client
3
+ from .config import Config
4
+
5
+ __all__ = ["Client", "Config", "devices"]
@@ -0,0 +1,31 @@
1
+ from typing import Self
2
+
3
+ import httpx
4
+
5
+ from enapter.http.api import devices
6
+
7
+ from .config import Config
8
+
9
+
10
+ class Client:
11
+
12
+ def __init__(self, config: Config) -> None:
13
+ self._config = config
14
+ self._client = self._new_client()
15
+
16
+ def _new_client(self) -> httpx.AsyncClient:
17
+ return httpx.AsyncClient(
18
+ headers={"X-Enapter-Auth-Token": self._config.token},
19
+ base_url=self._config.base_url,
20
+ )
21
+
22
+ async def __aenter__(self) -> Self:
23
+ await self._client.__aenter__()
24
+ return self
25
+
26
+ async def __aexit__(self, *exc) -> None:
27
+ await self._client.__aexit__(*exc)
28
+
29
+ @property
30
+ def devices(self) -> devices.Client:
31
+ return devices.Client(client=self._client)
@@ -0,0 +1,23 @@
1
+ import os
2
+ from typing import MutableMapping, Self
3
+
4
+
5
+ class Config:
6
+
7
+ @classmethod
8
+ def from_env(
9
+ cls, env: MutableMapping[str, str] = os.environ, namespace: str = "ENAPTER_"
10
+ ) -> Self:
11
+ prefix = namespace + "HTTP_API_"
12
+ return cls(
13
+ token=env[prefix + "TOKEN"],
14
+ base_url=env.get(prefix + "BASE_URL"),
15
+ )
16
+
17
+ def __init__(self, token: str, base_url: str | None = None) -> None:
18
+ if not token:
19
+ raise ValueError("token is missing")
20
+ self.token = token
21
+ if base_url is None:
22
+ base_url = "https://api.enapter.com"
23
+ self.base_url = base_url
@@ -0,0 +1,21 @@
1
+ from .authorized_role import AuthorizedRole
2
+ from .client import Client
3
+ from .communication_config import CommunicationConfig
4
+ from .device import Device
5
+ from .device_type import DeviceType
6
+ from .mqtt_credentials import MQTTCredentials
7
+ from .mqtt_protocol import MQTTProtocol
8
+ from .mqtts_credentials import MQTTSCredentials
9
+ from .time_sync_protocol import TimeSyncProtocol
10
+
11
+ __all__ = [
12
+ "AuthorizedRole",
13
+ "Client",
14
+ "CommunicationConfig",
15
+ "Device",
16
+ "DeviceType",
17
+ "MQTTCredentials",
18
+ "MQTTProtocol",
19
+ "MQTTSCredentials",
20
+ "TimeSyncProtocol",
21
+ ]
@@ -0,0 +1,11 @@
1
+ import enum
2
+
3
+
4
+ class AuthorizedRole(enum.Enum):
5
+
6
+ READONLY = "READONLY"
7
+ USER = "USER"
8
+ OWNER = "OWNER"
9
+ INSTALLER = "INSTALLER"
10
+ SYSTEM = "SYSTEM"
11
+ VENDOR = "VENDOR"
@@ -0,0 +1,25 @@
1
+ import httpx
2
+
3
+ from .communication_config import CommunicationConfig
4
+ from .device import Device
5
+ from .mqtt_protocol import MQTTProtocol
6
+
7
+
8
+ class Client:
9
+
10
+ def __init__(self, client: httpx.AsyncClient) -> None:
11
+ self._client = client
12
+
13
+ async def get(self, device_id: str) -> Device:
14
+ url = f"v3/devices/{device_id}"
15
+ response = await self._client.get(url)
16
+ response.raise_for_status()
17
+ return Device.from_dto(response.json()["device"])
18
+
19
+ async def generate_communication_config(
20
+ self, device_id: str, mqtt_protocol: MQTTProtocol
21
+ ) -> CommunicationConfig:
22
+ url = f"v3/devices/{device_id}/generate_config"
23
+ response = await self._client.post(url, json={"protocol": mqtt_protocol.value})
24
+ response.raise_for_status()
25
+ return CommunicationConfig.from_dto(response.json()["config"])
@@ -0,0 +1,45 @@
1
+ import dataclasses
2
+ from typing import Any, Self
3
+
4
+ from .mqtt_credentials import MQTTCredentials
5
+ from .mqtt_protocol import MQTTProtocol
6
+ from .mqtts_credentials import MQTTSCredentials
7
+ from .time_sync_protocol import TimeSyncProtocol
8
+
9
+
10
+ @dataclasses.dataclass
11
+ class CommunicationConfig:
12
+
13
+ mqtt_host: str
14
+ mqtt_port: int
15
+ mqtt_credentials: MQTTCredentials | MQTTSCredentials
16
+ mqtt_protocol: MQTTProtocol
17
+ time_sync_protocol: TimeSyncProtocol
18
+ time_sync_host: str
19
+ time_sync_port: int
20
+ hardware_id: str
21
+ channel_id: str
22
+
23
+ @classmethod
24
+ def from_dto(cls, dto: dict[str, Any]) -> Self:
25
+ mqtt_protocol = MQTTProtocol(dto["mqtt_protocol"])
26
+ mqtt_credentials: MQTTCredentials | MQTTSCredentials | None = None
27
+ match mqtt_protocol:
28
+ case MQTTProtocol.MQTT:
29
+ mqtt_credentials = MQTTCredentials.from_dto(dto["mqtt_credentials"])
30
+ case MQTTProtocol.MQTTS:
31
+ mqtt_credentials = MQTTSCredentials.from_dto(dto["mqtt_credentials"])
32
+ case _:
33
+ raise NotImplementedError(mqtt_protocol)
34
+ assert mqtt_credentials is not None
35
+ return cls(
36
+ mqtt_host=dto["mqtt_host"],
37
+ mqtt_port=int(dto["mqtt_port"]),
38
+ mqtt_credentials=mqtt_credentials,
39
+ mqtt_protocol=mqtt_protocol,
40
+ time_sync_protocol=TimeSyncProtocol(dto["time_sync_protocol"].upper()),
41
+ time_sync_host=dto["time_sync_host"],
42
+ time_sync_port=int(dto["time_sync_port"]),
43
+ hardware_id=dto["hardware_id"],
44
+ channel_id=dto["channel_id"],
45
+ )
@@ -0,0 +1,32 @@
1
+ import dataclasses
2
+ import datetime
3
+ from typing import Any, Self
4
+
5
+ from .authorized_role import AuthorizedRole
6
+ from .device_type import DeviceType
7
+
8
+
9
+ @dataclasses.dataclass
10
+ class Device:
11
+
12
+ id: str
13
+ blueprint_id: str
14
+ name: str
15
+ site_id: str
16
+ updated_at: datetime.datetime
17
+ slug: str
18
+ type: DeviceType
19
+ authorized_role: AuthorizedRole
20
+
21
+ @classmethod
22
+ def from_dto(cls, dto: dict[str, Any]) -> Self:
23
+ return cls(
24
+ id=dto["id"],
25
+ blueprint_id=dto["blueprint_id"],
26
+ name=dto["name"],
27
+ site_id=dto["site_id"],
28
+ updated_at=datetime.datetime.fromisoformat(dto["updated_at"]),
29
+ slug=dto["slug"],
30
+ type=DeviceType(dto["type"]),
31
+ authorized_role=AuthorizedRole(dto["authorized_role"]),
32
+ )
@@ -0,0 +1,10 @@
1
+ import enum
2
+
3
+
4
+ class DeviceType(enum.Enum):
5
+
6
+ LUA = "LUA"
7
+ VIRTUAL_UCM = "VIRTUAL_UCM"
8
+ HARDWARE_UCM = "HARDWARE_UCM"
9
+ STANDALONE = "STANDALONE"
10
+ GATEWAY = "GATEWAY"
@@ -0,0 +1,13 @@
1
+ import dataclasses
2
+ from typing import Any, Self
3
+
4
+
5
+ @dataclasses.dataclass
6
+ class MQTTCredentials:
7
+
8
+ username: str
9
+ password: str
10
+
11
+ @classmethod
12
+ def from_dto(cls, dto: dict[str, Any]) -> Self:
13
+ return cls(username=dto["username"], password=dto["password"])
@@ -0,0 +1,7 @@
1
+ import enum
2
+
3
+
4
+ class MQTTProtocol(enum.Enum):
5
+
6
+ MQTT = "MQTT"
7
+ MQTTS = "MQTTS"
@@ -0,0 +1,18 @@
1
+ import dataclasses
2
+ from typing import Any, Self
3
+
4
+
5
+ @dataclasses.dataclass
6
+ class MQTTSCredentials:
7
+
8
+ private_key: str
9
+ certificate: str
10
+ ca_chain: str
11
+
12
+ @classmethod
13
+ def from_dto(cls, dto: dict[str, Any]) -> Self:
14
+ return cls(
15
+ private_key=dto["private_key"],
16
+ certificate=dto["certificate"],
17
+ ca_chain=dto["ca_chain"],
18
+ )
@@ -0,0 +1,6 @@
1
+ import enum
2
+
3
+
4
+ class TimeSyncProtocol(enum.Enum):
5
+
6
+ NTP = "NTP"
@@ -1,12 +1,22 @@
1
1
  import datetime
2
+ import logging
3
+ from typing import Any
2
4
 
3
- import json_log_formatter
5
+ import json_log_formatter # type: ignore
4
6
 
5
7
 
6
8
  class JSONFormatter(json_log_formatter.JSONFormatter):
7
- def json_record(self, message, extra, record):
9
+
10
+ def json_record(
11
+ self, message: str, extra: dict[str, Any], record: logging.LogRecord
12
+ ) -> dict[str, Any]:
13
+ try:
14
+ del extra["taskName"]
15
+ except KeyError:
16
+ pass
17
+
8
18
  json_record = {
9
- "time": datetime.datetime.utcnow().isoformat(),
19
+ "time": datetime.datetime.now(datetime.timezone.utc).isoformat(),
10
20
  "level": record.levelname[:4],
11
21
  "name": record.name,
12
22
  **extra,
@@ -22,5 +32,5 @@ class JSONFormatter(json_log_formatter.JSONFormatter):
22
32
 
23
33
  return json_record
24
34
 
25
- def mutate_json_record(self, json_record):
35
+ def mutate_json_record(self, json_record: dict[str, Any]) -> dict[str, Any]:
26
36
  return json_record
enapter/mdns/resolver.py CHANGED
@@ -1,39 +1,42 @@
1
1
  import logging
2
2
 
3
- import dns.asyncresolver
3
+ import dns.asyncresolver # type: ignore
4
4
 
5
5
  LOGGER = logging.getLogger(__name__)
6
6
 
7
7
 
8
8
  class Resolver:
9
- def __init__(self):
9
+
10
+ def __init__(self) -> None:
10
11
  self._logger = LOGGER
11
12
  self._dns_resolver = self._new_dns_resolver()
12
13
  self._mdns_resolver = self._new_mdns_resolver()
13
14
 
14
- async def resolve(self, host):
15
+ async def resolve(self, host: str) -> str:
15
16
  # TODO: Resolve concurrently.
16
17
  try:
17
- return await self._resolve(self._dns_resolver, host)
18
+ ip = await self._resolve(self._dns_resolver, host)
19
+ self._logger.debug("%r resolved using DNS: %r", host, ip)
20
+ return ip
18
21
  except Exception as e:
19
22
  self._logger.debug(
20
- "failed to resolve %r using DNS resolver, switching to mDNS: %r",
21
- host,
22
- e,
23
+ "switching to mDNS: failed to resolve %r using DNS: %r", host, e
23
24
  )
24
- return await self._resolve(self._mdns_resolver, host)
25
+ ip = await self._resolve(self._mdns_resolver, host)
26
+ self._logger.info("%r resolved using mDNS: %r", host, ip)
27
+ return ip
25
28
 
26
- async def _resolve(self, resolver, host):
29
+ async def _resolve(self, resolver: dns.asyncresolver.Resolver, host: str) -> str:
27
30
  answer = await resolver.resolve(host, "A")
28
31
  if not answer:
29
32
  raise ValueError(f"empty answer received: {host}")
30
33
 
31
34
  return answer[0].address
32
35
 
33
- def _new_dns_resolver(self):
36
+ def _new_dns_resolver(self) -> dns.asyncresolver.Resolver:
34
37
  return dns.asyncresolver.Resolver(configure=True)
35
38
 
36
- def _new_mdns_resolver(self):
39
+ def _new_mdns_resolver(self) -> dns.asyncresolver.Resolver:
37
40
  r = dns.asyncresolver.Resolver(configure=False)
38
41
  r.nameservers = ["224.0.0.251"]
39
42
  r.port = 5353
enapter/mqtt/__init__.py CHANGED
@@ -1,14 +1,7 @@
1
1
  from .client import Client
2
- from .command import CommandRequest, CommandResponse, CommandState
3
- from .config import Config
4
- from .device_channel import DeviceChannel, DeviceLogSeverity
2
+ from .errors import Error
3
+ from .message import Message
5
4
 
6
- __all__ = [
7
- "Client",
8
- "CommandRequest",
9
- "CommandResponse",
10
- "CommandState",
11
- "Config",
12
- "DeviceChannel",
13
- "DeviceLogSeverity",
14
- ]
5
+ from . import api # isort: skip
6
+
7
+ __all__ = ["Client", "Error", "Message", "api"]
@@ -0,0 +1,6 @@
1
+ from .client import Client
2
+ from .config import Config, TLSConfig
3
+
4
+ from . import device # isort: skip
5
+
6
+ __all__ = ["Client", "Config", "TLSConfig", "device"]
@@ -0,0 +1,62 @@
1
+ import asyncio
2
+ import contextlib
3
+ import ssl
4
+ import tempfile
5
+ from typing import Self
6
+
7
+ from enapter import mqtt
8
+ from enapter.mqtt.api import device
9
+
10
+ from .config import Config
11
+
12
+
13
+ class Client:
14
+
15
+ def __init__(self, config: Config, task_group: asyncio.TaskGroup | None) -> None:
16
+ self._config = config
17
+ self._client = self._new_client(task_group=task_group)
18
+
19
+ async def __aenter__(self) -> Self:
20
+ await self._client.__aenter__()
21
+ return self
22
+
23
+ async def __aexit__(self, *exc) -> None:
24
+ await self._client.__aexit__(*exc)
25
+
26
+ def device_channel(self, hardware_id: str, channel_id: str) -> device.Channel:
27
+ return device.Channel(
28
+ client=self._client, hardware_id=hardware_id, channel_id=channel_id
29
+ )
30
+
31
+ def _new_client(self, task_group: asyncio.TaskGroup | None) -> mqtt.Client:
32
+ return mqtt.Client(
33
+ hostname=self._config.host,
34
+ port=self._config.port,
35
+ username=self._config.user,
36
+ password=self._config.password,
37
+ tls_context=self._new_tls_context(),
38
+ task_group=task_group,
39
+ )
40
+
41
+ def _new_tls_context(self) -> ssl.SSLContext | None:
42
+ if self._config.tls is None:
43
+ return None
44
+
45
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
46
+
47
+ ctx.verify_mode = ssl.CERT_REQUIRED
48
+ ctx.check_hostname = False
49
+ ctx.load_verify_locations(None, None, self._config.tls.ca_cert)
50
+
51
+ with contextlib.ExitStack() as stack:
52
+ certfile = stack.enter_context(tempfile.NamedTemporaryFile())
53
+ certfile.write(self._config.tls.cert.encode())
54
+ certfile.flush()
55
+
56
+ keyfile = stack.enter_context(tempfile.NamedTemporaryFile())
57
+ keyfile.write(self._config.tls.secret_key.encode())
58
+ keyfile.flush()
59
+
60
+ ctx.load_cert_chain(certfile.name, keyfile=keyfile.name)
61
+
62
+ return ctx
@@ -0,0 +1,77 @@
1
+ import os
2
+ from typing import MutableMapping, Self
3
+
4
+
5
+ class TLSConfig:
6
+
7
+ @classmethod
8
+ def from_env(
9
+ cls, env: MutableMapping[str, str] = os.environ, namespace: str = "ENAPTER_"
10
+ ) -> Self | None:
11
+ prefix = namespace + "MQTT_API_TLS_"
12
+
13
+ secret_key = env.get(prefix + "SECRET_KEY")
14
+ cert = env.get(prefix + "CERT")
15
+ ca_cert = env.get(prefix + "CA_CERT")
16
+
17
+ nothing_defined = {secret_key, cert, ca_cert} == {None}
18
+ if nothing_defined:
19
+ return None
20
+
21
+ if secret_key is None:
22
+ raise KeyError(prefix + "SECRET_KEY")
23
+ if cert is None:
24
+ raise KeyError(prefix + "CERT")
25
+ if ca_cert is None:
26
+ raise KeyError(prefix + "CA_CERT")
27
+
28
+ def pem(value: str) -> str:
29
+ return value.replace("\\n", "\n")
30
+
31
+ return cls(secret_key=pem(secret_key), cert=pem(cert), ca_cert=pem(ca_cert))
32
+
33
+ def __init__(self, secret_key: str, cert: str, ca_cert: str) -> None:
34
+ self.secret_key = secret_key
35
+ self.cert = cert
36
+ self.ca_cert = ca_cert
37
+
38
+
39
+ class Config:
40
+
41
+ @classmethod
42
+ def from_env(
43
+ cls, env: MutableMapping[str, str] = os.environ, namespace: str = "ENAPTER_"
44
+ ) -> Self:
45
+ prefix = namespace + "MQTT_API_"
46
+ return cls(
47
+ host=env[prefix + "HOST"],
48
+ port=int(env[prefix + "PORT"]),
49
+ user=env.get(prefix + "USER", default=None),
50
+ password=env.get(prefix + "PASSWORD", default=None),
51
+ tls_config=TLSConfig.from_env(env, namespace=namespace),
52
+ )
53
+
54
+ def __init__(
55
+ self,
56
+ host: str,
57
+ port: int,
58
+ user: str | None = None,
59
+ password: str | None = None,
60
+ tls_config: TLSConfig | None = None,
61
+ ) -> None:
62
+ self.host = host
63
+ self.port = port
64
+ self.user = user
65
+ self.password = password
66
+ self.tls_config = tls_config
67
+
68
+ @property
69
+ def tls(self) -> TLSConfig | None:
70
+ return self.tls_config
71
+
72
+ def __repr__(self) -> str:
73
+ return "mqtt.api.Config(host=%r, port=%r, tls=%r)" % (
74
+ self.host,
75
+ self.port,
76
+ self.tls is not None,
77
+ )