enapter 0.10.1__py3-none-any.whl → 0.11.3__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.

Potentially problematic release.


This version of enapter might be problematic. Click here for more details.

enapter/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.10.1"
1
+ __version__ = "0.11.3"
2
2
 
3
3
  from . import async_, log, mdns, mqtt, types, vucm
4
4
 
@@ -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
@@ -5,20 +5,20 @@ import contextlib
5
5
 
6
6
  class Routine(abc.ABC):
7
7
  @abc.abstractmethod
8
- async def _run(self):
8
+ async def _run(self) -> None:
9
9
  raise NotImplementedError # pragma: no cover
10
10
 
11
11
  async def __aenter__(self):
12
12
  await self.start()
13
13
  return self
14
14
 
15
- async def __aexit__(self, *_):
15
+ async def __aexit__(self, *_) -> None:
16
16
  await self.stop()
17
17
 
18
- def task(self):
18
+ def task(self) -> asyncio.Task:
19
19
  return self._task
20
20
 
21
- async def start(self, cancel_parent_task_on_exception=True):
21
+ async def start(self, cancel_parent_task_on_exception: bool = True) -> None:
22
22
  self._started = asyncio.Event()
23
23
  self._stack = contextlib.AsyncExitStack()
24
24
 
@@ -43,26 +43,27 @@ class Routine(abc.ABC):
43
43
  if self._task in done:
44
44
  self._task.result()
45
45
 
46
- async def stop(self):
46
+ async def stop(self) -> None:
47
47
  self.cancel()
48
48
  await self.join()
49
49
 
50
- def cancel(self):
50
+ def cancel(self) -> None:
51
51
  self._task.cancel()
52
52
 
53
- async def join(self):
53
+ async def join(self) -> None:
54
54
  if self._task.done():
55
55
  self._task.result()
56
56
  else:
57
57
  await self._task
58
58
 
59
- async def __run(self):
59
+ async def __run(self) -> None:
60
60
  try:
61
61
  await self._run()
62
62
  except asyncio.CancelledError:
63
63
  pass
64
64
  except:
65
65
  if self._started.is_set() and self._cancel_parent_task_on_exception:
66
+ assert self._parent_task is not None
66
67
  self._parent_task.cancel()
67
68
  raise
68
69
  finally:
@@ -1,10 +1,22 @@
1
1
  import datetime
2
+ import logging
3
+ from typing import Any, Dict
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
+ def json_record(
10
+ self,
11
+ message: str,
12
+ extra: Dict[str, Any],
13
+ record: logging.LogRecord,
14
+ ) -> Dict[str, Any]:
15
+ try:
16
+ del extra["taskName"]
17
+ except KeyError:
18
+ pass
19
+
8
20
  json_record = {
9
21
  "time": datetime.datetime.now(datetime.timezone.utc).isoformat(),
10
22
  "level": record.levelname[:4],
@@ -22,5 +34,5 @@ class JSONFormatter(json_log_formatter.JSONFormatter):
22
34
 
23
35
  return json_record
24
36
 
25
- def mutate_json_record(self, json_record):
37
+ def mutate_json_record(self, json_record: Dict[str, Any]) -> Dict[str, Any]:
26
38
  return json_record
enapter/mdns/resolver.py CHANGED
@@ -1,17 +1,17 @@
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
+ def __init__(self) -> None:
10
10
  self._logger = LOGGER
11
11
  self._dns_resolver = self._new_dns_resolver()
12
12
  self._mdns_resolver = self._new_mdns_resolver()
13
13
 
14
- async def resolve(self, host):
14
+ async def resolve(self, host: str) -> str:
15
15
  # TODO: Resolve concurrently.
16
16
  try:
17
17
  ip = await self._resolve(self._dns_resolver, host)
@@ -25,17 +25,17 @@ class Resolver:
25
25
  self._logger.info("%r resolved using mDNS: %r", host, ip)
26
26
  return ip
27
27
 
28
- async def _resolve(self, resolver, host):
28
+ async def _resolve(self, resolver: dns.asyncresolver.Resolver, host: str) -> str:
29
29
  answer = await resolver.resolve(host, "A")
30
30
  if not answer:
31
31
  raise ValueError(f"empty answer received: {host}")
32
32
 
33
33
  return answer[0].address
34
34
 
35
- def _new_dns_resolver(self):
35
+ def _new_dns_resolver(self) -> dns.asyncresolver.Resolver:
36
36
  return dns.asyncresolver.Resolver(configure=True)
37
37
 
38
- def _new_mdns_resolver(self):
38
+ def _new_mdns_resolver(self) -> dns.asyncresolver.Resolver:
39
39
  r = dns.asyncresolver.Resolver(configure=False)
40
40
  r.nameservers = ["224.0.0.251"]
41
41
  r.port = 5353
enapter/mqtt/__init__.py CHANGED
@@ -1,9 +1,10 @@
1
1
  from . import api
2
2
  from .client import Client
3
- from .config import Config
3
+ from .config import Config, TLSConfig
4
4
 
5
5
  __all__ = [
6
- "api",
7
6
  "Client",
8
7
  "Config",
8
+ "TLSConfig",
9
+ "api",
9
10
  ]
@@ -1,5 +1,6 @@
1
1
  import enum
2
2
  import json
3
+ from typing import Any, Dict, Optional, Union
3
4
 
4
5
 
5
6
  class CommandState(enum.Enum):
@@ -9,11 +10,11 @@ class CommandState(enum.Enum):
9
10
 
10
11
  class CommandRequest:
11
12
  @classmethod
12
- def unmarshal_json(cls, data):
13
+ def unmarshal_json(cls, data: Union[str, bytes]) -> "CommandRequest":
13
14
  req = json.loads(data)
14
15
  return cls(id_=req["id"], name=req["name"], args=req.get("arguments"))
15
16
 
16
- def __init__(self, id_, name, args=None):
17
+ def __init__(self, id_: str, name: str, args: Optional[Dict[str, Any]] = None):
17
18
  self.id = id_
18
19
  self.name = name
19
20
 
@@ -21,12 +22,17 @@ class CommandRequest:
21
22
  args = {}
22
23
  self.args = args
23
24
 
24
- def new_response(self, *args, **kwargs):
25
+ def new_response(self, *args, **kwargs) -> "CommandResponse":
25
26
  return CommandResponse(self.id, *args, **kwargs)
26
27
 
27
28
 
28
29
  class CommandResponse:
29
- def __init__(self, id_, state, payload=None):
30
+ def __init__(
31
+ self,
32
+ id_: str,
33
+ state: Union[str, CommandState],
34
+ payload: Optional[Union[Dict[str, Any], str]] = None,
35
+ ) -> None:
30
36
  self.id = id_
31
37
 
32
38
  if not isinstance(state, CommandState):
@@ -37,8 +43,8 @@ class CommandResponse:
37
43
  payload = {"message": payload}
38
44
  self.payload = payload
39
45
 
40
- def json(self):
41
- json_object = {"id": self.id, "state": self.state.value}
46
+ def json(self) -> Dict[str, Any]:
47
+ json_object: Dict[str, Any] = {"id": self.id, "state": self.state.value}
42
48
  if self.payload is not None:
43
49
  json_object["payload"] = self.payload
44
50
 
@@ -1,72 +1,82 @@
1
1
  import json
2
2
  import logging
3
3
  import time
4
+ from typing import Any, AsyncContextManager, AsyncGenerator, Dict
5
+
6
+ import aiomqtt # type: ignore
4
7
 
5
8
  import enapter
6
9
 
7
- from .command import CommandRequest
10
+ from ..client import Client
11
+ from .command import CommandRequest, CommandResponse
12
+ from .log_severity import LogSeverity
8
13
 
9
14
  LOGGER = logging.getLogger(__name__)
10
15
 
11
16
 
12
17
  class DeviceChannel:
13
- def __init__(self, client, hardware_id, channel_id):
18
+ def __init__(self, client: Client, hardware_id: str, channel_id: str) -> None:
14
19
  self._client = client
15
20
  self._logger = self._new_logger(hardware_id, channel_id)
16
21
  self._hardware_id = hardware_id
17
22
  self._channel_id = channel_id
18
23
 
19
24
  @property
20
- def hardware_id(self):
25
+ def hardware_id(self) -> str:
21
26
  return self._hardware_id
22
27
 
23
28
  @property
24
- def channel_id(self):
29
+ def channel_id(self) -> str:
25
30
  return self._channel_id
26
31
 
27
32
  @staticmethod
28
- def _new_logger(hardware_id, channel_id):
33
+ def _new_logger(hardware_id, channel_id) -> logging.LoggerAdapter:
29
34
  extra = {"hardware_id": hardware_id, "channel_id": channel_id}
30
35
  return logging.LoggerAdapter(LOGGER, extra=extra)
31
36
 
32
37
  @enapter.async_.generator
33
- async def subscribe_to_command_requests(self):
38
+ async def subscribe_to_command_requests(
39
+ self,
40
+ ) -> AsyncGenerator[CommandRequest, None]:
34
41
  async with self._subscribe("v1/command/requests") as messages:
35
42
  async for msg in messages:
43
+ assert isinstance(msg.payload, str) or isinstance(msg.payload, bytes)
36
44
  yield CommandRequest.unmarshal_json(msg.payload)
37
45
 
38
- async def publish_command_response(self, resp):
46
+ async def publish_command_response(self, resp: CommandResponse) -> None:
39
47
  await self._publish_json("v1/command/responses", resp.json())
40
48
 
41
- async def publish_telemetry(self, telemetry, **kwargs):
49
+ async def publish_telemetry(self, telemetry: Dict[str, Any], **kwargs) -> None:
42
50
  await self._publish_json("v1/telemetry", telemetry, **kwargs)
43
51
 
44
- async def publish_properties(self, properties, **kwargs):
52
+ async def publish_properties(self, properties: Dict[str, Any], **kwargs) -> None:
45
53
  await self._publish_json("v1/register", properties, **kwargs)
46
54
 
47
- async def publish_logs(self, msg, severity, persist=False, **kwargs):
55
+ async def publish_logs(
56
+ self, msg: str, severity: LogSeverity, persist: bool = False, **kwargs
57
+ ) -> None:
48
58
  logs = {
49
59
  "message": msg,
50
60
  "severity": severity.value,
61
+ "persist": persist,
51
62
  }
52
- if persist:
53
- logs["persist"] = True
54
-
55
63
  await self._publish_json("v3/logs", logs, **kwargs)
56
64
 
57
- def _subscribe(self, path):
65
+ def _subscribe(
66
+ self, path: str
67
+ ) -> AsyncContextManager[AsyncGenerator[aiomqtt.Message, None]]:
58
68
  topic = f"v1/to/{self._hardware_id}/{self._channel_id}/{path}"
59
69
  return self._client.subscribe(topic)
60
70
 
61
- async def _publish_json(self, path, json_object, **kwargs):
71
+ async def _publish_json(
72
+ self, path: str, json_object: Dict[str, Any], **kwargs
73
+ ) -> None:
62
74
  if "timestamp" not in json_object:
63
75
  json_object["timestamp"] = int(time.time())
64
-
65
76
  payload = json.dumps(json_object)
66
-
67
77
  await self._publish(path, payload, **kwargs)
68
78
 
69
- async def _publish(self, path, payload, **kwargs):
79
+ async def _publish(self, path: str, payload: str, **kwargs) -> None:
70
80
  topic = f"v1/from/{self._hardware_id}/{self._channel_id}/{path}"
71
81
  try:
72
82
  await self._client.publish(topic, payload, **kwargs)
enapter/mqtt/client.py CHANGED
@@ -1,145 +1,114 @@
1
1
  import asyncio
2
- import collections
3
2
  import contextlib
4
3
  import logging
5
4
  import ssl
6
5
  import tempfile
6
+ from typing import AsyncGenerator, Optional
7
7
 
8
- import aiomqtt
8
+ import aiomqtt # type: ignore
9
9
 
10
10
  import enapter
11
11
 
12
+ from .config import Config
13
+
12
14
  LOGGER = logging.getLogger(__name__)
13
15
 
14
16
 
15
17
  class Client(enapter.async_.Routine):
16
- def __init__(self, config):
18
+ def __init__(self, config: Config) -> None:
17
19
  self._logger = self._new_logger(config)
18
20
  self._config = config
19
21
  self._mdns_resolver = enapter.mdns.Resolver()
20
22
  self._tls_context = self._new_tls_context(config)
21
- self._client = None
22
- self._client_ready = asyncio.Event()
23
- self._subscribers = collections.defaultdict(int)
23
+ self._publisher: Optional[aiomqtt.Client] = None
24
+ self._publisher_connected = asyncio.Event()
24
25
 
25
26
  @staticmethod
26
- def _new_logger(config):
27
+ def _new_logger(config: Config) -> logging.LoggerAdapter:
27
28
  extra = {"host": config.host, "port": config.port}
28
29
  return logging.LoggerAdapter(LOGGER, extra=extra)
29
30
 
30
- def config(self):
31
+ def config(self) -> Config:
31
32
  return self._config
32
33
 
33
- async def publish(self, *args, **kwargs):
34
- client = await self._wait_client()
35
- await client.publish(*args, **kwargs)
34
+ async def publish(self, *args, **kwargs) -> None:
35
+ await self._publisher_connected.wait()
36
+ assert self._publisher is not None
37
+ await self._publisher.publish(*args, **kwargs)
36
38
 
37
39
  @enapter.async_.generator
38
- async def subscribe(self, topic):
40
+ async def subscribe(self, *topics: str) -> AsyncGenerator[aiomqtt.Message, None]:
39
41
  while True:
40
- client = await self._wait_client()
41
-
42
42
  try:
43
- async with client.messages() as messages:
44
- async with self._subscribe(client, topic):
45
- async for msg in messages:
46
- if msg.topic.matches(topic):
47
- yield msg
48
-
43
+ async with self._connect() as subscriber:
44
+ for topic in topics:
45
+ await subscriber.subscribe(topic)
46
+ self._logger.info("subscriber [%s] connected", ",".join(topics))
47
+ async for msg in subscriber.messages:
48
+ yield msg
49
49
  except aiomqtt.MqttError as e:
50
50
  self._logger.error(e)
51
51
  retry_interval = 5
52
52
  await asyncio.sleep(retry_interval)
53
53
 
54
- @contextlib.asynccontextmanager
55
- async def _subscribe(self, client, topic):
56
- first_subscriber = not self._subscribers[topic]
57
- self._subscribers[topic] += 1
58
- try:
59
- if first_subscriber:
60
- await client.subscribe(topic)
61
- yield
62
- finally:
63
- self._subscribers[topic] -= 1
64
- assert not self._subscribers[topic] < 0
65
- last_unsubscriber = not self._subscribers[topic]
66
- if last_unsubscriber:
67
- del self._subscribers[topic]
68
- await client.unsubscribe(topic)
69
-
70
- async def _wait_client(self):
71
- await self._client_ready.wait()
72
- assert self._client_ready.is_set()
73
- return self._client
74
-
75
- async def _run(self):
54
+ async def _run(self) -> None:
76
55
  self._logger.info("starting")
77
-
78
56
  self._started.set()
79
-
80
57
  while True:
81
58
  try:
82
- async with self._connect() as client:
83
- self._client = client
84
- self._client_ready.set()
85
- self._logger.info("client ready")
86
-
87
- # tracking disconnect
88
- async with client.messages() as messages:
89
- async for msg in messages:
90
- pass
59
+ async with self._connect() as publisher:
60
+ self._logger.info("publisher connected")
61
+ self._publisher = publisher
62
+ self._publisher_connected.set()
63
+ async for msg in publisher.messages:
64
+ pass
91
65
  except aiomqtt.MqttError as e:
92
66
  self._logger.error(e)
93
67
  retry_interval = 5
94
68
  await asyncio.sleep(retry_interval)
95
69
  finally:
96
- self._client_ready.clear()
97
- self._client = None
98
- self._logger.info("client not ready")
70
+ self._publisher_connected.clear()
71
+ self._publisher = None
72
+ self._logger.info("publisher disconnected")
99
73
 
100
74
  @contextlib.asynccontextmanager
101
- async def _connect(self):
75
+ async def _connect(self) -> AsyncGenerator[aiomqtt.Client, None]:
102
76
  host = await self._maybe_resolve_mdns(self._config.host)
103
-
104
- try:
105
- async with aiomqtt.Client(
106
- hostname=host,
107
- port=self._config.port,
108
- username=self._config.user,
109
- password=self._config.password,
110
- logger=self._logger,
111
- tls_context=self._tls_context,
112
- ) as client:
113
- yield client
114
- except asyncio.CancelledError:
115
- # FIXME: A cancelled `aiomqtt.Client.connect` leaks resources.
116
- raise
77
+ async with aiomqtt.Client(
78
+ hostname=host,
79
+ port=self._config.port,
80
+ username=self._config.user,
81
+ password=self._config.password,
82
+ logger=LOGGER,
83
+ tls_context=self._tls_context,
84
+ ) as client:
85
+ yield client
117
86
 
118
87
  @staticmethod
119
- def _new_tls_context(config):
120
- if not config.tls_enabled:
88
+ def _new_tls_context(config: Config) -> Optional[ssl.SSLContext]:
89
+ if config.tls is None:
121
90
  return None
122
91
 
123
92
  ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
124
93
 
125
94
  ctx.verify_mode = ssl.CERT_REQUIRED
126
95
  ctx.check_hostname = False
127
- ctx.load_verify_locations(None, None, config.tls_ca_cert)
96
+ ctx.load_verify_locations(None, None, config.tls.ca_cert)
128
97
 
129
98
  with contextlib.ExitStack() as stack:
130
99
  certfile = stack.enter_context(tempfile.NamedTemporaryFile())
131
- certfile.write(config.tls_cert.encode())
100
+ certfile.write(config.tls.cert.encode())
132
101
  certfile.flush()
133
102
 
134
103
  keyfile = stack.enter_context(tempfile.NamedTemporaryFile())
135
- keyfile.write(config.tls_secret_key.encode())
104
+ keyfile.write(config.tls.secret_key.encode())
136
105
  keyfile.flush()
137
106
 
138
107
  ctx.load_cert_chain(certfile.name, keyfile=keyfile.name)
139
108
 
140
109
  return ctx
141
110
 
142
- async def _maybe_resolve_mdns(self, host):
111
+ async def _maybe_resolve_mdns(self, host: str) -> str:
143
112
  if not host.endswith(".local"):
144
113
  return host
145
114
 
enapter/mqtt/config.py CHANGED
@@ -1,48 +1,69 @@
1
1
  import os
2
+ from typing import MutableMapping, Optional
2
3
 
3
4
 
4
- class Config:
5
+ class TLSConfig:
6
+
5
7
  @classmethod
6
- def from_env(cls, prefix="ENAPTER_", env=os.environ):
7
- def pem(value):
8
- if value is None:
9
- return value
8
+ def from_env(
9
+ cls, prefix: str = "ENAPTER_", env: MutableMapping[str, str] = os.environ
10
+ ) -> Optional["TLSConfig"]:
11
+ secret_key = env.get(prefix + "MQTT_TLS_SECRET_KEY")
12
+ cert = env.get(prefix + "MQTT_TLS_CERT")
13
+ ca_cert = env.get(prefix + "MQTT_TLS_CA_CERT")
14
+
15
+ nothing_defined = {secret_key, cert, ca_cert} == {None}
16
+ if nothing_defined:
17
+ return None
18
+
19
+ if secret_key is None:
20
+ raise KeyError(prefix + "MQTT_TLS_SECRET_KEY")
21
+ if cert is None:
22
+ raise KeyError(prefix + "MQTT_TLS_CERT")
23
+ if ca_cert is None:
24
+ raise KeyError(prefix + "MQTT_TLS_CA_CERT")
25
+
26
+ def pem(value: str) -> str:
10
27
  return value.replace("\\n", "\n")
11
28
 
29
+ return cls(secret_key=pem(secret_key), cert=pem(cert), ca_cert=pem(ca_cert))
30
+
31
+ def __init__(self, secret_key: str, cert: str, ca_cert: str) -> None:
32
+ self.secret_key = secret_key
33
+ self.cert = cert
34
+ self.ca_cert = ca_cert
35
+
36
+
37
+ class Config:
38
+ @classmethod
39
+ def from_env(
40
+ cls, prefix: str = "ENAPTER_", env: MutableMapping[str, str] = os.environ
41
+ ) -> "Config":
12
42
  return cls(
13
43
  host=env[prefix + "MQTT_HOST"],
14
44
  port=int(env[prefix + "MQTT_PORT"]),
15
45
  user=env.get(prefix + "MQTT_USER", default=None),
16
46
  password=env.get(prefix + "MQTT_PASSWORD", default=None),
17
- tls_secret_key=pem(env.get(prefix + "MQTT_TLS_SECRET_KEY", default=None)),
18
- tls_cert=pem(env.get(prefix + "MQTT_TLS_CERT", default=None)),
19
- tls_ca_cert=pem(env.get(prefix + "MQTT_TLS_CA_CERT", default=None)),
47
+ tls=TLSConfig.from_env(prefix=prefix, env=env),
20
48
  )
21
49
 
22
50
  def __init__(
23
51
  self,
24
- host,
25
- port,
26
- user=None,
27
- password=None,
28
- tls_secret_key=None,
29
- tls_cert=None,
30
- tls_ca_cert=None,
31
- ):
52
+ host: str,
53
+ port: int,
54
+ user: Optional[str] = None,
55
+ password: Optional[str] = None,
56
+ tls: Optional[TLSConfig] = None,
57
+ ) -> None:
32
58
  self.host = host
33
59
  self.port = port
34
60
  self.user = user
35
61
  self.password = password
62
+ self.tls = tls
36
63
 
37
- self.tls_secret_key = tls_secret_key
38
- self.tls_cert = tls_cert
39
- self.tls_ca_cert = tls_ca_cert
40
-
41
- self.tls_enabled = {tls_secret_key, tls_cert, tls_ca_cert} != {None}
42
-
43
- def __repr__(self):
44
- return "mqtt.Config(host=%r, port=%r, tls_enabled=%r)" % (
64
+ def __repr__(self) -> str:
65
+ return "mqtt.Config(host=%r, port=%r, tls=%r)" % (
45
66
  self.host,
46
67
  self.port,
47
- self.tls_enabled,
68
+ self.tls is not None,
48
69
  )
enapter/vucm/app.py CHANGED
@@ -1,12 +1,22 @@
1
1
  import asyncio
2
+ from typing import Optional, Protocol
2
3
 
3
4
  import enapter
4
5
 
5
6
  from .config import Config
7
+ from .device import Device
6
8
  from .ucm import UCM
7
9
 
8
10
 
9
- async def run(device_factory, config_prefix=None):
11
+ class DeviceFactory(Protocol):
12
+
13
+ def __call__(self, channel: enapter.mqtt.api.DeviceChannel, **kwargs) -> Device:
14
+ pass
15
+
16
+
17
+ async def run(
18
+ device_factory: DeviceFactory, config_prefix: Optional[str] = None
19
+ ) -> None:
10
20
  enapter.log.configure(level=enapter.log.LEVEL or "info")
11
21
 
12
22
  config = Config.from_env(prefix=config_prefix)
@@ -16,11 +26,11 @@ async def run(device_factory, config_prefix=None):
16
26
 
17
27
 
18
28
  class App(enapter.async_.Routine):
19
- def __init__(self, config, device_factory):
29
+ def __init__(self, config: Config, device_factory: DeviceFactory) -> None:
20
30
  self._config = config
21
31
  self._device_factory = device_factory
22
32
 
23
- async def _run(self):
33
+ async def _run(self) -> None:
24
34
  tasks = set()
25
35
 
26
36
  mqtt_client = await self._stack.enter_async_context(
enapter/vucm/config.py CHANGED
@@ -1,61 +1,72 @@
1
1
  import base64
2
2
  import json
3
3
  import os
4
+ from typing import MutableMapping, Optional
4
5
 
5
6
  import enapter
6
7
 
7
8
 
8
9
  class Config:
9
10
  @classmethod
10
- def from_env(cls, prefix=None, env=os.environ):
11
+ def from_env(
12
+ cls, prefix: Optional[str] = None, env: MutableMapping[str, str] = os.environ
13
+ ) -> "Config":
11
14
  if prefix is None:
12
15
  prefix = "ENAPTER_VUCM_"
13
16
  try:
14
- blob = os.environ[prefix + "BLOB"]
17
+ blob = env[prefix + "BLOB"]
15
18
  except KeyError:
16
19
  pass
17
20
  else:
18
21
  config = cls.from_blob(blob)
19
22
  try:
20
- config.channel_id = os.environ[prefix + "CHANNEL_ID"]
23
+ config.channel_id = env[prefix + "CHANNEL_ID"]
21
24
  except KeyError:
22
25
  pass
23
26
  return config
24
27
 
25
- hardware_id = os.environ[prefix + "HARDWARE_ID"]
26
- channel_id = os.environ[prefix + "CHANNEL_ID"]
28
+ hardware_id = env[prefix + "HARDWARE_ID"]
29
+ channel_id = env[prefix + "CHANNEL_ID"]
27
30
 
28
- mqtt_config = enapter.mqtt.Config.from_env(prefix=prefix, env=env)
31
+ mqtt = enapter.mqtt.Config.from_env(prefix=prefix, env=env)
29
32
 
30
- start_ucm = os.environ.get(prefix + "START_UCM", "1") != "0"
33
+ start_ucm = env.get(prefix + "START_UCM", "1") != "0"
31
34
 
32
35
  return cls(
33
36
  hardware_id=hardware_id,
34
37
  channel_id=channel_id,
35
- mqtt_config=mqtt_config,
38
+ mqtt=mqtt,
36
39
  start_ucm=start_ucm,
37
40
  )
38
41
 
39
42
  @classmethod
40
- def from_blob(cls, blob):
43
+ def from_blob(cls, blob: str) -> "Config":
41
44
  payload = json.loads(base64.b64decode(blob))
42
45
 
43
- mqtt_config = enapter.mqtt.Config(
46
+ mqtt = enapter.mqtt.Config(
44
47
  host=payload["mqtt_host"],
45
48
  port=int(payload["mqtt_port"]),
46
- tls_ca_cert=payload["mqtt_ca"],
47
- tls_cert=payload["mqtt_cert"],
48
- tls_secret_key=payload["mqtt_private_key"],
49
+ tls=enapter.mqtt.TLSConfig(
50
+ ca_cert=payload["mqtt_ca"],
51
+ cert=payload["mqtt_cert"],
52
+ secret_key=payload["mqtt_private_key"],
53
+ ),
49
54
  )
50
55
 
51
56
  return cls(
52
57
  hardware_id=payload["ucm_id"],
53
58
  channel_id=payload["channel_id"],
54
- mqtt_config=mqtt_config,
59
+ mqtt=mqtt,
55
60
  )
56
61
 
57
- def __init__(self, hardware_id, channel_id, mqtt_config, start_ucm=True):
62
+ def __init__(
63
+ self,
64
+ hardware_id: str,
65
+ channel_id: str,
66
+ mqtt: enapter.mqtt.Config,
67
+ start_ucm: bool = True,
68
+ ) -> None:
58
69
  self.hardware_id = hardware_id
59
70
  self.channel_id = channel_id
60
- self.mqtt = mqtt_config
71
+ self.mqtt = mqtt
61
72
  self.start_ucm = start_ucm
enapter/vucm/device.py CHANGED
@@ -2,7 +2,7 @@ import asyncio
2
2
  import concurrent
3
3
  import functools
4
4
  import traceback
5
- from typing import Any, Callable, Coroutine, Optional, Set
5
+ from typing import Any, Callable, Coroutine, Dict, Optional, Set, Tuple
6
6
 
7
7
  import enapter
8
8
 
@@ -35,10 +35,7 @@ def is_device_command(func: DeviceCommandFunc) -> bool:
35
35
 
36
36
  class Device(enapter.async_.Routine):
37
37
  def __init__(
38
- self,
39
- channel,
40
- cmd_prefix="cmd_",
41
- thread_pool_executor=None,
38
+ self, channel: enapter.mqtt.api.DeviceChannel, thread_pool_workers: int = 1
42
39
  ) -> None:
43
40
  self.__channel = channel
44
41
 
@@ -54,15 +51,15 @@ class Device(enapter.async_.Routine):
54
51
  if is_device_command(obj):
55
52
  self.__commands[name] = obj
56
53
 
57
- if thread_pool_executor is None:
58
- thread_pool_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
59
- self.__thread_pool_executor = thread_pool_executor
54
+ self.__thread_pool_executor = concurrent.futures.ThreadPoolExecutor(
55
+ max_workers=thread_pool_workers
56
+ )
60
57
 
61
58
  self.log = Logger(channel=channel)
62
59
  self.alerts: Set[str] = set()
63
60
 
64
61
  async def send_telemetry(
65
- self, telemetry: Optional[enapter.types.JSON] = None
62
+ self, telemetry: Optional[Dict[str, enapter.types.JSON]] = None
66
63
  ) -> None:
67
64
  if telemetry is None:
68
65
  telemetry = {}
@@ -74,7 +71,7 @@ class Device(enapter.async_.Routine):
74
71
  await self.__channel.publish_telemetry(telemetry)
75
72
 
76
73
  async def send_properties(
77
- self, properties: Optional[enapter.types.JSON] = None
74
+ self, properties: Optional[Dict[str, enapter.types.JSON]] = None
78
75
  ) -> None:
79
76
  if properties is None:
80
77
  properties = {}
@@ -83,13 +80,13 @@ class Device(enapter.async_.Routine):
83
80
 
84
81
  await self.__channel.publish_properties(properties)
85
82
 
86
- async def run_in_thread(self, func, *args, **kwargs):
83
+ async def run_in_thread(self, func, *args, **kwargs) -> Any:
87
84
  loop = asyncio.get_running_loop()
88
85
  return await loop.run_in_executor(
89
86
  self.__thread_pool_executor, functools.partial(func, *args, **kwargs)
90
87
  )
91
88
 
92
- async def _run(self):
89
+ async def _run(self) -> None:
93
90
  self._stack.enter_context(self.__thread_pool_executor)
94
91
 
95
92
  tasks = set()
@@ -115,7 +112,7 @@ class Device(enapter.async_.Routine):
115
112
  task.cancel()
116
113
  self._stack.push_async_callback(self.__wait_task, task)
117
114
 
118
- async def __wait_task(self, task):
115
+ async def __wait_task(self, task) -> None:
119
116
  try:
120
117
  await task
121
118
  except asyncio.CancelledError:
@@ -127,16 +124,18 @@ class Device(enapter.async_.Routine):
127
124
  pass
128
125
  raise
129
126
 
130
- async def __process_command_requests(self):
127
+ async def __process_command_requests(self) -> None:
131
128
  async with self.__channel.subscribe_to_command_requests() as reqs:
132
129
  async for req in reqs:
133
130
  state, payload = await self.__execute_command(req)
134
131
  resp = req.new_response(state, payload)
135
132
  await self.__channel.publish_command_response(resp)
136
133
 
137
- async def __execute_command(self, req):
134
+ async def __execute_command(
135
+ self, req
136
+ ) -> Tuple[enapter.mqtt.api.CommandState, enapter.types.JSON]:
138
137
  try:
139
- cmd = self._commands[req.name]
138
+ cmd = self.__commands[req.name]
140
139
  except KeyError:
141
140
  return enapter.mqtt.api.CommandState.ERROR, {"reason": "unknown command"}
142
141
 
enapter/vucm/logger.py CHANGED
@@ -6,32 +6,32 @@ LOGGER = logging.getLogger(__name__)
6
6
 
7
7
 
8
8
  class Logger:
9
- def __init__(self, channel):
9
+ def __init__(self, channel) -> None:
10
10
  self._channel = channel
11
11
  self._logger = self._new_logger(channel.hardware_id, channel.channel_id)
12
12
 
13
13
  @staticmethod
14
- def _new_logger(hardware_id, channel_id):
14
+ def _new_logger(hardware_id, channel_id) -> logging.LoggerAdapter:
15
15
  extra = {"hardware_id": hardware_id, "channel_id": channel_id}
16
16
  return logging.LoggerAdapter(LOGGER, extra=extra)
17
17
 
18
- async def debug(self, msg: str, persist: bool = False):
18
+ async def debug(self, msg: str, persist: bool = False) -> None:
19
19
  self._logger.debug(msg)
20
20
  await self.log(
21
21
  msg, severity=enapter.mqtt.api.LogSeverity.DEBUG, persist=persist
22
22
  )
23
23
 
24
- async def info(self, msg: str, persist: bool = False):
24
+ async def info(self, msg: str, persist: bool = False) -> None:
25
25
  self._logger.info(msg)
26
26
  await self.log(msg, severity=enapter.mqtt.api.LogSeverity.INFO, persist=persist)
27
27
 
28
- async def warning(self, msg: str, persist: bool = False):
28
+ async def warning(self, msg: str, persist: bool = False) -> None:
29
29
  self._logger.warning(msg)
30
30
  await self.log(
31
31
  msg, severity=enapter.mqtt.api.LogSeverity.WARNING, persist=persist
32
32
  )
33
33
 
34
- async def error(self, msg: str, persist: bool = False):
34
+ async def error(self, msg: str, persist: bool = False) -> None:
35
35
  self._logger.error(msg)
36
36
  await self.log(
37
37
  msg, severity=enapter.mqtt.api.LogSeverity.ERROR, persist=persist
@@ -39,7 +39,7 @@ class Logger:
39
39
 
40
40
  async def log(
41
41
  self, msg: str, severity: enapter.mqtt.api.LogSeverity, persist: bool = False
42
- ):
42
+ ) -> None:
43
43
  await self._channel.publish_logs(msg=msg, severity=severity, persist=persist)
44
44
 
45
45
  __call__ = log
enapter/vucm/ucm.py CHANGED
@@ -6,7 +6,7 @@ from .device import Device, device_command, device_task
6
6
 
7
7
 
8
8
  class UCM(Device):
9
- def __init__(self, mqtt_client, hardware_id):
9
+ def __init__(self, mqtt_client, hardware_id) -> None:
10
10
  super().__init__(
11
11
  channel=enapter.mqtt.api.DeviceChannel(
12
12
  client=mqtt_client, hardware_id=hardware_id, channel_id="ucm"
@@ -14,12 +14,12 @@ class UCM(Device):
14
14
  )
15
15
 
16
16
  @device_command
17
- async def reboot(self):
17
+ async def reboot(self) -> None:
18
18
  await asyncio.sleep(0)
19
19
  raise NotImplementedError
20
20
 
21
21
  @device_command
22
- async def upload_lua_script(self, url, sha1, payload=None):
22
+ async def upload_lua_script(self, url, sha1, payload=None) -> None:
23
23
  await asyncio.sleep(0)
24
24
  raise NotImplementedError
25
25
 
@@ -30,7 +30,7 @@ class UCM(Device):
30
30
  await asyncio.sleep(1)
31
31
 
32
32
  @device_task
33
- async def properties_publisher(self):
33
+ async def properties_publisher(self) -> None:
34
34
  while True:
35
35
  await self.send_properties({"virtual": True, "lua_api_ver": 1})
36
36
  await asyncio.sleep(10)
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: enapter
3
- Version: 0.10.1
3
+ Version: 0.11.3
4
4
  Summary: Enapter Python SDK
5
5
  Home-page: https://github.com/Enapter/python-sdk
6
6
  Author: Roman Novatorov
7
7
  Author-email: rnovatorov@enapter.com
8
8
  Description-Content-Type: text/markdown
9
- Requires-Dist: aiomqtt==1.0.*
10
- Requires-Dist: dnspython==2.6.*
11
- Requires-Dist: json-log-formatter==0.5.*
9
+ Requires-Dist: aiomqtt==2.4.*
10
+ Requires-Dist: dnspython==2.8.*
11
+ Requires-Dist: json-log-formatter==1.1.*
12
12
  Dynamic: author
13
13
  Dynamic: author-email
14
14
  Dynamic: description
@@ -25,20 +25,17 @@ Dynamic: summary
25
25
 
26
26
  Enapter software development kit for Python.
27
27
 
28
- :warning: **This project is work in progress. The API is not stable and may change at any time.** :warning:
29
-
30
28
  ## Installation
31
29
 
32
- Stable from PyPI:
30
+ This project uses [semantic versioning](https://semver.org/).
33
31
 
34
- ```bash
35
- pip install enapter
36
- ```
32
+ The API is still under development and may change at any time. It is
33
+ recommended to pin the version during installation.
37
34
 
38
- Latest for GitHub:
35
+ Latest from PyPI:
39
36
 
40
37
  ```bash
41
- pip install git+https://github.com/Enapter/python-sdk#egg=enapter
38
+ pip install enapter==0.11.3'
42
39
  ```
43
40
 
44
41
  ## Usage
@@ -0,0 +1,38 @@
1
+ enapter/__init__.py,sha256=awKW7jrI1c27sJl9KJ8WSFwREQ5me8hitA7HmBuhnJk,183
2
+ enapter/types.py,sha256=J_rFCW79cloh2FF_49Oab5kaEGdZohymkCJU7vfko-8,113
3
+ enapter/async_/__init__.py,sha256=JuiRI2bN2AgB-HLfAUoSsZpEziwFRftNNEp59Evnd0M,109
4
+ enapter/async_/generator.py,sha256=BQPPtPCYdsZIjgVy0UlC81DqId3yIDrpEyrmJ2cHdc8,497
5
+ enapter/async_/routine.py,sha256=zWAWWAEjwFoL8vmknC5d1oU7i2dr-HLUn-hbXEZuqHY,1903
6
+ enapter/log/__init__.py,sha256=n1sWMDKJSs_ebZzjbTrVdfg-oi0V1tvliTxgIV-msJ0,600
7
+ enapter/log/json_formatter.py,sha256=mNB9ORZ6To-Xv429dPyHiGrZcg9WadgYQN5ourib8Qw,1004
8
+ enapter/mdns/__init__.py,sha256=uwsg8wJ0Lcsr9oOEW1PkEV3jVgWzgA7RG2ur_MRLtM0,55
9
+ enapter/mdns/resolver.py,sha256=SyxA8LtoTjpKlr77HAnLZb7BH-6CPuFIxCjbsT0P1OY,1433
10
+ enapter/mqtt/__init__.py,sha256=TXdhxYwkzH-sqc84gO5tR1Jple29jCByW48C-Uyl-dU,154
11
+ enapter/mqtt/client.py,sha256=D_rZ_MEOPg2hGBTLRMOnDQulDrA1pfsba6k4abxHMCA,4212
12
+ enapter/mqtt/config.py,sha256=m3CoZf9EYqmjHWxefTH_dgDWNY2FGZfgjHXFw6RN9yc,2077
13
+ enapter/mqtt/api/__init__.py,sha256=M1g2891bSLCnDbZOuLElEPPlN6pJk0J1w_1Fi8x5xJU,267
14
+ enapter/mqtt/api/command.py,sha256=YEnKh2Uzv45GkQqmAhP9BOeyAiHDO1iLClZ_vyCkV9g,1388
15
+ enapter/mqtt/api/device_channel.py,sha256=h02nU8FhnSQ1H-BqDpsT93n79StIMFo8_58whBk8ku4,2995
16
+ enapter/mqtt/api/log_severity.py,sha256=ZmHXMc8d1e2ZnsXDWwl3S3noAEJjILYab_qjqk374Qw,126
17
+ enapter/vucm/__init__.py,sha256=40GAkJvpCl7oMuWDU09zpjP5rM4V-oKRbh_R1uhx4aE,247
18
+ enapter/vucm/app.py,sha256=53Z7x85GrRatEpvD7FZWy37ZchZ5TOAdlHMsd6DRFVw,1706
19
+ enapter/vucm/config.py,sha256=Cgx_-zW556opsNnSBhrLMHClvG5CwP1zva45dgCQyu4,1903
20
+ enapter/vucm/device.py,sha256=8_kuQ1PFYMrj-kZg2WLuDztcdf_dddlTdJM2BTtGupw,4427
21
+ enapter/vucm/logger.py,sha256=mClOnuNCR72W51BVH9uGuBsy109Ev4tBZxTTVHqmgAE,1518
22
+ enapter/vucm/ucm.py,sha256=Bc_sCo6EVjUprshf0AnNqKeShAmwdWOi5uZ4v_g_elc,989
23
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ tests/conftest.py,sha256=qEZwCEK8F6hJZ5sSHK6hUmUYKaWfY77UuNOHntfNS3c,397
25
+ tests/fake_data_generator.py,sha256=lpVgazRRXAP07SeiTeY3Fe1LzDdM44lS50QTlwBdMYg,587
26
+ tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ tests/integration/conftest.py,sha256=TpbUrExImSsLp77FIp1O-6wJrxgTmyxCgmmATIDEmL4,1617
28
+ tests/integration/test_mqtt.py,sha256=FDoRDZlmDM9j0ji04SG8410sR1egeSRMKv4ovdrRiaQ,3885
29
+ tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
+ tests/unit/test_async.py,sha256=KwhwKBSeSb7Qyaf50Ca2LGB7gm3m5j5wgGgWnvYY98k,4208
31
+ tests/unit/test_log.py,sha256=Q-ZelqGfladBCaw-BQrwRrxbxMK1VZxgY7HBsBb1GHw,1875
32
+ tests/unit/test_vucm.py,sha256=a3euiqV9etsfrWFDUtHCNqKU3irx-GEZMczBcmddhek,3626
33
+ tests/unit/test_mqtt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
+ tests/unit/test_mqtt/test_api.py,sha256=ObKCHB-KDOYQLFrdzjTmLfjdWXXX0oanGKpX49P0qMI,2670
35
+ enapter-0.11.3.dist-info/METADATA,sha256=vDupirPofGAFHYW-rpzjtY-di0ftkfu6L_1KbJ-RW54,3321
36
+ enapter-0.11.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
+ enapter-0.11.3.dist-info/top_level.txt,sha256=DsMzVradd7z3A0fm7zmn9oh08ijO41RtzglrnPlx54w,14
38
+ enapter-0.11.3.dist-info/RECORD,,
@@ -92,6 +92,6 @@ class HeartbitSender(enapter.async_.Routine):
92
92
  payload = str(int(time.time()))
93
93
  try:
94
94
  await self.enapter_mqtt_client.publish(self.topic, payload)
95
- except aiomqtt.error.MqttError as e:
95
+ except aiomqtt.MqttError as e:
96
96
  print(f"failed to publish heartbit: {e}")
97
97
  await asyncio.sleep(self.interval)
@@ -1,38 +0,0 @@
1
- enapter/__init__.py,sha256=4-pDIWcAIU8BKujgdIKwiiDBcHL9phC49rfiZW_2aGs,183
2
- enapter/types.py,sha256=J_rFCW79cloh2FF_49Oab5kaEGdZohymkCJU7vfko-8,113
3
- enapter/async_/__init__.py,sha256=JuiRI2bN2AgB-HLfAUoSsZpEziwFRftNNEp59Evnd0M,109
4
- enapter/async_/generator.py,sha256=qBhnt36Gl2166sJFnZHsREbZu8l43M4DfxybUMIv6W4,300
5
- enapter/async_/routine.py,sha256=A5fG4XnCEQT0Qa_JNh1N43Fv5lnLaCoGF4xt6pOAdNs,1770
6
- enapter/log/__init__.py,sha256=n1sWMDKJSs_ebZzjbTrVdfg-oi0V1tvliTxgIV-msJ0,600
7
- enapter/log/json_formatter.py,sha256=R5-nNEtFKdkv_SAi0gkGOaRrzsbxBjUmgZzzuJk2z7A,723
8
- enapter/mdns/__init__.py,sha256=uwsg8wJ0Lcsr9oOEW1PkEV3jVgWzgA7RG2ur_MRLtM0,55
9
- enapter/mdns/resolver.py,sha256=FFQuZiaKOaNtfSgI2YOaSdG-BuZwOKmYVy06Sc735Zo,1297
10
- enapter/mqtt/__init__.py,sha256=aKJklTmR8OEnwnQXN1MtrvJimC9EfxcTOqhhBsPcb84,126
11
- enapter/mqtt/client.py,sha256=2oExtmLOdrfo_s_wQuE7ECNK1kuFSoyhaFOkg3AP44w,4874
12
- enapter/mqtt/config.py,sha256=Bng9A271vXMg1cNZ1A0lWXVKQUzun8fQHLqqt8AMXNw,1399
13
- enapter/mqtt/api/__init__.py,sha256=M1g2891bSLCnDbZOuLElEPPlN6pJk0J1w_1Fi8x5xJU,267
14
- enapter/mqtt/api/command.py,sha256=ozhDTjRrdCWv_bzPTjVFpL8tx7nhirm3JtQaD45wTdo,1092
15
- enapter/mqtt/api/device_channel.py,sha256=fma_R59kxMoH6S4DwDBcSqgTsr8KNJkEov-c3poNsD0,2371
16
- enapter/mqtt/api/log_severity.py,sha256=ZmHXMc8d1e2ZnsXDWwl3S3noAEJjILYab_qjqk374Qw,126
17
- enapter/vucm/__init__.py,sha256=40GAkJvpCl7oMuWDU09zpjP5rM4V-oKRbh_R1uhx4aE,247
18
- enapter/vucm/app.py,sha256=zHdEEUnKrVHNvVGmw7Jrv5MFkVnbg5yiiped86jxbSU,1424
19
- enapter/vucm/config.py,sha256=Lj-YLQvoZ9ivXVBF0oKRvdupLRJ2l55EEzoxZ4HWWRE,1713
20
- enapter/vucm/device.py,sha256=FiJSJNgZkWGL-866IGY8bbqS75ZZigJZYyoH7kMW7Is,4352
21
- enapter/vucm/logger.py,sha256=a_ZtuIinATbEtiP5avV8JGAKFKG0-RZhF6SozaF-_PU,1445
22
- enapter/vucm/ucm.py,sha256=r1q1rSYlT7GWZ24JLmTbzkpMDUUHcvCNp9t5xHTEyzY,957
23
- tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- tests/conftest.py,sha256=qEZwCEK8F6hJZ5sSHK6hUmUYKaWfY77UuNOHntfNS3c,397
25
- tests/fake_data_generator.py,sha256=lpVgazRRXAP07SeiTeY3Fe1LzDdM44lS50QTlwBdMYg,587
26
- tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- tests/integration/conftest.py,sha256=TpbUrExImSsLp77FIp1O-6wJrxgTmyxCgmmATIDEmL4,1617
28
- tests/integration/test_mqtt.py,sha256=Hm165Sxysrc5ZatJjiMKITv0-78N0NbGiTI5QrJ_aSU,3891
29
- tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- tests/unit/test_async.py,sha256=KwhwKBSeSb7Qyaf50Ca2LGB7gm3m5j5wgGgWnvYY98k,4208
31
- tests/unit/test_log.py,sha256=Q-ZelqGfladBCaw-BQrwRrxbxMK1VZxgY7HBsBb1GHw,1875
32
- tests/unit/test_vucm.py,sha256=a3euiqV9etsfrWFDUtHCNqKU3irx-GEZMczBcmddhek,3626
33
- tests/unit/test_mqtt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- tests/unit/test_mqtt/test_api.py,sha256=ObKCHB-KDOYQLFrdzjTmLfjdWXXX0oanGKpX49P0qMI,2670
35
- enapter-0.10.1.dist-info/METADATA,sha256=I5JEyfIvlkzd3WUVUd9yOedrFdpBAgySiK_8HMJZMi0,3335
36
- enapter-0.10.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
- enapter-0.10.1.dist-info/top_level.txt,sha256=DsMzVradd7z3A0fm7zmn9oh08ijO41RtzglrnPlx54w,14
38
- enapter-0.10.1.dist-info/RECORD,,