enapter 0.9.1__py3-none-any.whl → 0.10.0__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.9.1"
1
+ __version__ = "0.10.0"
2
2
 
3
3
  from . import async_, log, mdns, mqtt, types, vucm
4
4
 
@@ -6,7 +6,7 @@ import json_log_formatter
6
6
  class JSONFormatter(json_log_formatter.JSONFormatter):
7
7
  def json_record(self, message, extra, record):
8
8
  json_record = {
9
- "time": datetime.datetime.utcnow().isoformat(),
9
+ "time": datetime.datetime.now(datetime.UTC).isoformat(),
10
10
  "level": record.levelname[:4],
11
11
  "name": record.name,
12
12
  **extra,
enapter/types.py CHANGED
@@ -1,3 +1,3 @@
1
- from typing import Any, Dict
1
+ from typing import Dict, List, Union
2
2
 
3
- JSON = Dict[str, Any]
3
+ JSON = Union[str, int, float, None, bool, List["JSON"], Dict[str, "JSON"]]
enapter/vucm/__init__.py CHANGED
@@ -1,12 +1,14 @@
1
1
  from .app import App, run
2
2
  from .config import Config
3
- from .device import Device
3
+ from .device import Device, device_command, device_task
4
4
  from .ucm import UCM
5
5
 
6
6
  __all__ = [
7
7
  "App",
8
8
  "Config",
9
9
  "Device",
10
+ "device_command",
11
+ "device_task",
10
12
  "UCM",
11
13
  "run",
12
14
  ]
enapter/vucm/app.py CHANGED
@@ -6,10 +6,10 @@ from .config import Config
6
6
  from .ucm import UCM
7
7
 
8
8
 
9
- async def run(device_factory):
9
+ async def run(device_factory, config_prefix=None):
10
10
  enapter.log.configure(level=enapter.log.LEVEL or "info")
11
11
 
12
- config = Config.from_env()
12
+ config = Config.from_env(prefix=config_prefix)
13
13
 
14
14
  async with App(config=config, device_factory=device_factory) as app:
15
15
  await app.join()
enapter/vucm/config.py CHANGED
@@ -7,7 +7,9 @@ import enapter
7
7
 
8
8
  class Config:
9
9
  @classmethod
10
- def from_env(cls, prefix="ENAPTER_VUCM_", env=os.environ):
10
+ def from_env(cls, prefix=None, env=os.environ):
11
+ if prefix is None:
12
+ prefix = "ENAPTER_VUCM_"
11
13
  try:
12
14
  blob = os.environ[prefix + "BLOB"]
13
15
  except KeyError:
enapter/vucm/device.py CHANGED
@@ -2,25 +2,57 @@ import asyncio
2
2
  import concurrent
3
3
  import functools
4
4
  import traceback
5
- from typing import Optional, Set
5
+ from typing import Any, Callable, Coroutine, Optional, Set
6
6
 
7
7
  import enapter
8
8
 
9
9
  from .logger import Logger
10
10
 
11
+ DEVICE_TASK_MARK = "_enapter_vucm_device_task"
12
+ DEVICE_COMMAND_MARK = "_enapter_vucm_device_command"
13
+
14
+ DeviceTaskFunc = Callable[[Any], Coroutine]
15
+ DeviceCommandFunc = Callable[..., Coroutine]
16
+
17
+
18
+ def device_task(func: DeviceTaskFunc) -> DeviceTaskFunc:
19
+ setattr(func, DEVICE_TASK_MARK, True)
20
+ return func
21
+
22
+
23
+ def device_command(func: DeviceCommandFunc) -> DeviceTaskFunc:
24
+ setattr(func, DEVICE_COMMAND_MARK, True)
25
+ return func
26
+
27
+
28
+ def is_device_task(func: DeviceTaskFunc) -> bool:
29
+ return getattr(func, DEVICE_TASK_MARK, False) is True
30
+
31
+
32
+ def is_device_command(func: DeviceCommandFunc) -> bool:
33
+ return getattr(func, DEVICE_COMMAND_MARK, False) is True
34
+
11
35
 
12
36
  class Device(enapter.async_.Routine):
13
37
  def __init__(
14
38
  self,
15
39
  channel,
16
40
  cmd_prefix="cmd_",
17
- task_prefix="task_",
18
41
  thread_pool_executor=None,
19
42
  ) -> None:
20
43
  self.__channel = channel
21
44
 
22
- self.__cmd_prefix = cmd_prefix
23
- self.__task_prefix = task_prefix
45
+ self.__tasks = {}
46
+ for name in dir(self):
47
+ obj = getattr(self, name)
48
+ if is_device_task(obj):
49
+ self.__tasks[name] = obj
50
+
51
+ self.__commands = {}
52
+ for name in dir(self):
53
+ obj = getattr(self, name)
54
+ if is_device_command(obj):
55
+ self.__commands[name] = obj
24
56
 
25
57
  if thread_pool_executor is None:
26
58
  thread_pool_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
@@ -62,11 +94,8 @@ class Device(enapter.async_.Routine):
62
94
 
63
95
  tasks = set()
64
96
 
65
- for name in dir(self):
66
- if name.startswith(self.__task_prefix):
67
- task_func = getattr(self, name)
68
- name_without_prefix = name[len(self.__task_prefix) :]
69
- tasks.add(asyncio.create_task(task_func(), name=name_without_prefix))
97
+ for name, func in self.__tasks.items():
98
+ tasks.add(asyncio.create_task(func(), name=name))
70
99
 
71
100
  tasks.add(
72
101
  asyncio.create_task(
@@ -107,8 +136,8 @@ class Device(enapter.async_.Routine):
107
136
 
108
137
  async def __execute_command(self, req):
109
138
  try:
110
- cmd = getattr(self, self.__cmd_prefix + req.name)
111
- except AttributeError:
139
+ cmd = self._commands[req.name]
140
+ except KeyError:
112
141
  return enapter.mqtt.api.CommandState.ERROR, {"reason": "unknown command"}
113
142
 
114
143
  try:
enapter/vucm/ucm.py CHANGED
@@ -2,7 +2,7 @@ import asyncio
2
2
 
3
3
  import enapter
4
4
 
5
- from .device import Device
5
+ from .device import Device, device_command, device_task
6
6
 
7
7
 
8
8
  class UCM(Device):
@@ -13,20 +13,24 @@ class UCM(Device):
13
13
  )
14
14
  )
15
15
 
16
- async def cmd_reboot(self):
16
+ @device_command
17
+ async def reboot(self):
17
18
  await asyncio.sleep(0)
18
19
  raise NotImplementedError
19
20
 
20
- async def cmd_upload_lua_script(self, url, sha1, payload=None):
21
+ @device_command
22
+ async def upload_lua_script(self, url, sha1, payload=None):
21
23
  await asyncio.sleep(0)
22
24
  raise NotImplementedError
23
25
 
24
- async def task_telemetry_publisher(self):
26
+ @device_task
27
+ async def telemetry_publisher(self) -> None:
25
28
  while True:
26
29
  await self.send_telemetry()
27
30
  await asyncio.sleep(1)
28
31
 
29
- async def task_properties_publisher(self):
32
+ @device_task
33
+ async def properties_publisher(self):
30
34
  while True:
31
35
  await self.send_properties({"virtual": True, "lua_api_ver": 1})
32
36
  await asyncio.sleep(10)
@@ -1,14 +1,21 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: enapter
3
- Version: 0.9.1
3
+ Version: 0.10.0
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.2.*
11
- Requires-Dist: json-log-formatter ==0.5.*
9
+ Requires-Dist: aiomqtt==1.0.*
10
+ Requires-Dist: dnspython==2.6.*
11
+ Requires-Dist: json-log-formatter==0.5.*
12
+ Dynamic: author
13
+ Dynamic: author-email
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: home-page
17
+ Dynamic: requires-dist
18
+ Dynamic: summary
12
19
 
13
20
  # Enapter Python SDK
14
21
 
@@ -42,12 +49,13 @@ Checkout [examples](examples).
42
49
 
43
50
  ### Device Telemetry and Properties
44
51
 
45
- Every method of `enapter.vucm.Device` subclass with a name that starts with
46
- `task_` prefix is considered a _device task_. When such a device is started,
47
- all of its tasks are started as well. Device tasks are started in random order
48
- and are being executed concurrently in the background. If a device task returns
49
- or raises an exception, device routine is terminated. A typical use of the task
50
- is to run a periodic job to send device telemetry and properties.
52
+ Every method of `enapter.vucm.Device` subclass decorated with
53
+ `enapter.vucm.device_task` decorator is considered a _device task_. When such a
54
+ device is started, all of its tasks are started as well. Device tasks are
55
+ started in random order and are being executed concurrently in the background.
56
+ If a device task returns or raises an exception, device routine is terminated.
57
+ A typical use of the task is to run a periodic job to send device telemetry and
58
+ properties.
51
59
 
52
60
  In order to send telemetry and properties define two corresponding device
53
61
  tasks. It is advised (but is not obligatory) to send telemetry every **1
@@ -57,12 +65,12 @@ Examples:
57
65
 
58
66
  - [wttr-in](examples/vucm/wttr-in)
59
67
 
60
- ### Device Commands
68
+ ### Device Command Handlers
61
69
 
62
- Every method of `enapter.vucm.Device` subclass with a name that starts with
63
- `cmd_` prefix is considered a _device command handler_. Device command handlers
64
- receive the same arguments as described in device Blueprint manifest and can
65
- optionally return a payload as `dict`.
70
+ Every method of `enapter.vucm.Device` subclass decorated with
71
+ `enapter.vucm.device_command` is considered a _device command handler_. Device
72
+ command handlers receive the same arguments as described in device Blueprint
73
+ manifest and can optionally return a payload as `enapter.types.JSON`.
66
74
 
67
75
  In order to handle device commands define corresponding device command
68
76
  handlers.
@@ -1,10 +1,10 @@
1
- enapter/__init__.py,sha256=1BOEXXQg9nxO7F5cVELgi7Va_DQVxnn-FF8Ib7toMPw,182
2
- enapter/types.py,sha256=2366bx_VI7bIBNnIaG5sol7G4k0PqaQ6oS74dzM4l1M,52
1
+ enapter/__init__.py,sha256=No4wlAO2UL9UZK-GmXjX2uitUxjgply-xv38DCZb4_w,183
2
+ enapter/types.py,sha256=J_rFCW79cloh2FF_49Oab5kaEGdZohymkCJU7vfko-8,113
3
3
  enapter/async_/__init__.py,sha256=JuiRI2bN2AgB-HLfAUoSsZpEziwFRftNNEp59Evnd0M,109
4
4
  enapter/async_/generator.py,sha256=qBhnt36Gl2166sJFnZHsREbZu8l43M4DfxybUMIv6W4,300
5
5
  enapter/async_/routine.py,sha256=A5fG4XnCEQT0Qa_JNh1N43Fv5lnLaCoGF4xt6pOAdNs,1770
6
6
  enapter/log/__init__.py,sha256=n1sWMDKJSs_ebZzjbTrVdfg-oi0V1tvliTxgIV-msJ0,600
7
- enapter/log/json_formatter.py,sha256=P46zEdU5-dr3mDMkUjBy7z_FSMPi61RUgEYyBEfqVNQ,705
7
+ enapter/log/json_formatter.py,sha256=mkcWW2VmksCmRVPU7ayxbNp5-eRetznSjZlA-DUloOY,714
8
8
  enapter/mdns/__init__.py,sha256=uwsg8wJ0Lcsr9oOEW1PkEV3jVgWzgA7RG2ur_MRLtM0,55
9
9
  enapter/mdns/resolver.py,sha256=FFQuZiaKOaNtfSgI2YOaSdG-BuZwOKmYVy06Sc735Zo,1297
10
10
  enapter/mqtt/__init__.py,sha256=aKJklTmR8OEnwnQXN1MtrvJimC9EfxcTOqhhBsPcb84,126
@@ -14,12 +14,12 @@ enapter/mqtt/api/__init__.py,sha256=M1g2891bSLCnDbZOuLElEPPlN6pJk0J1w_1Fi8x5xJU,
14
14
  enapter/mqtt/api/command.py,sha256=ozhDTjRrdCWv_bzPTjVFpL8tx7nhirm3JtQaD45wTdo,1092
15
15
  enapter/mqtt/api/device_channel.py,sha256=fma_R59kxMoH6S4DwDBcSqgTsr8KNJkEov-c3poNsD0,2371
16
16
  enapter/mqtt/api/log_severity.py,sha256=ZmHXMc8d1e2ZnsXDWwl3S3noAEJjILYab_qjqk374Qw,126
17
- enapter/vucm/__init__.py,sha256=OhUYlVNO1RereHiZEyeJPdpEcy2QEqDBg4xBk_-A7-c,177
18
- enapter/vucm/app.py,sha256=NLA4z32rykoxa9I3YDYh0TbT-25fx0gBpR6oKgaJzSs,1384
19
- enapter/vucm/config.py,sha256=fYvHk7vDWYf0w5OGQX7f_-Y4RmrrK2U1WY3DiOBEKcg,1660
20
- enapter/vucm/device.py,sha256=nRV83ZYmaWwwma8mPTlxym57LYB5vZYUr4yB0tb6YzQ,3620
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
21
  enapter/vucm/logger.py,sha256=a_ZtuIinATbEtiP5avV8JGAKFKG0-RZhF6SozaF-_PU,1445
22
- enapter/vucm/ucm.py,sha256=zRU4hCuF04LgV65zxJqiy5EYHU_gHRmLyZFmmD26Uhw,864
22
+ enapter/vucm/ucm.py,sha256=r1q1rSYlT7GWZ24JLmTbzkpMDUUHcvCNp9t5xHTEyzY,957
23
23
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  tests/conftest.py,sha256=qEZwCEK8F6hJZ5sSHK6hUmUYKaWfY77UuNOHntfNS3c,397
25
25
  tests/fake_data_generator.py,sha256=lpVgazRRXAP07SeiTeY3Fe1LzDdM44lS50QTlwBdMYg,587
@@ -28,11 +28,11 @@ tests/integration/conftest.py,sha256=TpbUrExImSsLp77FIp1O-6wJrxgTmyxCgmmATIDEmL4
28
28
  tests/integration/test_mqtt.py,sha256=Hm165Sxysrc5ZatJjiMKITv0-78N0NbGiTI5QrJ_aSU,3891
29
29
  tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  tests/unit/test_async.py,sha256=KwhwKBSeSb7Qyaf50Ca2LGB7gm3m5j5wgGgWnvYY98k,4208
31
- tests/unit/test_log.py,sha256=DQD1OCVkYYzy-Kq-jH_xsaRDtiwIqULuTgMitjSeiuo,1876
32
- tests/unit/test_vucm.py,sha256=h3sY9GXxNizHyjx0lxM5yErHUI4VzzJj_O1MbIu6OGM,685
31
+ tests/unit/test_log.py,sha256=Q-ZelqGfladBCaw-BQrwRrxbxMK1VZxgY7HBsBb1GHw,1875
32
+ tests/unit/test_vucm.py,sha256=a3euiqV9etsfrWFDUtHCNqKU3irx-GEZMczBcmddhek,3626
33
33
  tests/unit/test_mqtt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  tests/unit/test_mqtt/test_api.py,sha256=ObKCHB-KDOYQLFrdzjTmLfjdWXXX0oanGKpX49P0qMI,2670
35
- enapter-0.9.1.dist-info/METADATA,sha256=6UxjnzUYfnrXljArhwjl5QgE9jAE-o9ydcUoBmeK8aI,3153
36
- enapter-0.9.1.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
37
- enapter-0.9.1.dist-info/top_level.txt,sha256=DsMzVradd7z3A0fm7zmn9oh08ijO41RtzglrnPlx54w,14
38
- enapter-0.9.1.dist-info/RECORD,,
35
+ enapter-0.10.0.dist-info/METADATA,sha256=354uZuKv0_6QgPow7MPuP6VT6GNMNKYLfhSnSXNo0Fw,3335
36
+ enapter-0.10.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
+ enapter-0.10.0.dist-info/top_level.txt,sha256=DsMzVradd7z3A0fm7zmn9oh08ijO41RtzglrnPlx54w,14
38
+ enapter-0.10.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
tests/unit/test_log.py CHANGED
@@ -26,11 +26,9 @@ class TestJSONFormat:
26
26
  time = record.pop("time")
27
27
  assert re.match(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+", time) is not None
28
28
 
29
- assert record == {
30
- "level": "INFO",
31
- "name": "enapter",
32
- "message": "hello",
33
- }
29
+ assert record["level"] == "INFO"
30
+ assert record["name"] == "enapter"
31
+ assert record["message"] == "hello"
34
32
 
35
33
  def test_exc_info(self):
36
34
  buf = io.StringIO()
tests/unit/test_vucm.py CHANGED
@@ -11,6 +11,101 @@ class TestDevice:
11
11
  assert await device.run_in_thread(lambda: 42) == 42
12
12
  assert device._Device__thread_pool_executor._shutdown
13
13
 
14
+ async def test_task_marks(self, fake):
15
+ class MyDevice(enapter.vucm.Device):
16
+ async def task_foo(self):
17
+ pass
18
+
19
+ @enapter.vucm.device_task
20
+ async def task_bar(self):
21
+ pass
22
+
23
+ @enapter.vucm.device_task
24
+ async def baz(self):
25
+ pass
26
+
27
+ @enapter.vucm.device_task
28
+ async def goo(self):
29
+ pass
30
+
31
+ async with MyDevice(channel=MockChannel(fake)) as device:
32
+ tasks = device._Device__tasks
33
+ assert len(tasks) == 3
34
+ assert "task_foo" not in tasks
35
+ assert "task_bar" in tasks
36
+ assert "baz" in tasks
37
+ assert "goo" in tasks
38
+
39
+ async def test_command_marks(self, fake):
40
+ class MyDevice(enapter.vucm.Device):
41
+ async def cmd_foo(self):
42
+ pass
43
+
44
+ async def cmd_foo2(self, a, b, c):
45
+ pass
46
+
47
+ @enapter.vucm.device_command
48
+ async def cmd_bar(self):
49
+ pass
50
+
51
+ @enapter.vucm.device_command
52
+ async def cmd_bar2(self, a, b, c):
53
+ pass
54
+
55
+ @enapter.vucm.device_command
56
+ async def baz(self):
57
+ pass
58
+
59
+ @enapter.vucm.device_command
60
+ async def baz2(self, a, b, c):
61
+ pass
62
+
63
+ @enapter.vucm.device_command
64
+ async def goo(self):
65
+ pass
66
+
67
+ async with MyDevice(channel=MockChannel(fake)) as device:
68
+ commands = device._Device__commands
69
+ assert len(commands) == 5
70
+ assert "cmd_foo" not in commands
71
+ assert "cmd_foo2" not in commands
72
+ assert "cmd_bar" in commands
73
+ assert "cmd_bar2" in commands
74
+ assert "baz" in commands
75
+ assert "baz2" in commands
76
+ assert "goo" in commands
77
+
78
+ async def test_task_and_commands_marks(self, fake):
79
+ class MyDevice(enapter.vucm.Device):
80
+ @enapter.vucm.device_task
81
+ async def foo_task(self):
82
+ pass
83
+
84
+ @enapter.vucm.device_task
85
+ async def bar_task(self):
86
+ pass
87
+
88
+ @enapter.vucm.device_command
89
+ async def foo_command(self):
90
+ pass
91
+
92
+ @enapter.vucm.device_command
93
+ async def bar_command(self):
94
+ pass
95
+
96
+ async with MyDevice(channel=MockChannel(fake)) as device:
97
+ tasks = device._Device__tasks
98
+ assert len(tasks) == 2
99
+ assert "foo_task" in tasks
100
+ assert "bar_task" in tasks
101
+ assert "foo_command" not in tasks
102
+ assert "bar_command" not in tasks
103
+ commands = device._Device__commands
104
+ assert "foo_task" not in commands
105
+ assert "bar_task" not in commands
106
+ assert "foo_command" in commands
107
+ assert "bar_command" in commands
108
+
14
109
 
15
110
  class MockChannel:
16
111
  def __init__(self, fake):