pympacds 0.1.0__tar.gz
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.
- pympacds-0.1.0/PKG-INFO +187 -0
- pympacds-0.1.0/README.md +154 -0
- pympacds-0.1.0/pyproject.toml +56 -0
- pympacds-0.1.0/setup.cfg +4 -0
- pympacds-0.1.0/src/pympacds/__init__.py +133 -0
- pympacds-0.1.0/src/pympacds/builtin_contracts.py +129 -0
- pympacds-0.1.0/src/pympacds/cli.py +671 -0
- pympacds-0.1.0/src/pympacds/config.py +120 -0
- pympacds-0.1.0/src/pympacds/contracts.py +126 -0
- pympacds-0.1.0/src/pympacds/dbus.py +272 -0
- pympacds-0.1.0/src/pympacds/middleware.py +147 -0
- pympacds-0.1.0/src/pympacds/process.py +601 -0
- pympacds-0.1.0/src/pympacds/service_discovery.py +49 -0
- pympacds-0.1.0/src/pympacds/utils.py +31 -0
- pympacds-0.1.0/src/pympacds.egg-info/PKG-INFO +187 -0
- pympacds-0.1.0/src/pympacds.egg-info/SOURCES.txt +33 -0
- pympacds-0.1.0/src/pympacds.egg-info/dependency_links.txt +1 -0
- pympacds-0.1.0/src/pympacds.egg-info/entry_points.txt +5 -0
- pympacds-0.1.0/src/pympacds.egg-info/requires.txt +17 -0
- pympacds-0.1.0/src/pympacds.egg-info/top_level.txt +1 -0
- pympacds-0.1.0/tests/test_cli.py +185 -0
- pympacds-0.1.0/tests/test_cli_install.py +106 -0
- pympacds-0.1.0/tests/test_cli_integration.py +280 -0
- pympacds-0.1.0/tests/test_config.py +267 -0
- pympacds-0.1.0/tests/test_contracts.py +211 -0
- pympacds-0.1.0/tests/test_coverage.py +447 -0
- pympacds-0.1.0/tests/test_dbus.py +106 -0
- pympacds-0.1.0/tests/test_dbus_integration.py +115 -0
- pympacds-0.1.0/tests/test_examples.py +123 -0
- pympacds-0.1.0/tests/test_import.py +141 -0
- pympacds-0.1.0/tests/test_middleware.py +163 -0
- pympacds-0.1.0/tests/test_process.py +555 -0
- pympacds-0.1.0/tests/test_process_supplement.py +124 -0
- pympacds-0.1.0/tests/test_service_discovery.py +87 -0
- pympacds-0.1.0/tests/test_utils.py +23 -0
pympacds-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pympacds
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python Multi Process Asyncio-based Cooperative Discoverable Services Framework
|
|
5
|
+
Author-email: Oscar Diaz <odiaz@ieee.org>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: dbus,asyncio,daemon,framework,embedded,ipc
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Provides-Extra: dbus-fast
|
|
21
|
+
Requires-Dist: dbus-fast>=4.0; extra == "dbus-fast"
|
|
22
|
+
Provides-Extra: dbus-next
|
|
23
|
+
Requires-Dist: dbus-next>=0.2; extra == "dbus-next"
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
27
|
+
Requires-Dist: mypy; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff; extra == "dev"
|
|
29
|
+
Provides-Extra: docs
|
|
30
|
+
Requires-Dist: sphinx>=7.0; extra == "docs"
|
|
31
|
+
Requires-Dist: myst-parser>=2.0; extra == "docs"
|
|
32
|
+
Requires-Dist: furo>=2023.9.10; extra == "docs"
|
|
33
|
+
|
|
34
|
+
# pympacds
|
|
35
|
+
|
|
36
|
+
**Py**thon **M**ulti **P**rocess **A**sync-based **C**ooperative **D**iscoverable **S**ervices Framework
|
|
37
|
+
|
|
38
|
+
A lightweight, D-Bus-native application framework for building modular, multi-process, asyncio-based daemons on Linux — particularly suited for embedded and IoT systems.
|
|
39
|
+
|
|
40
|
+
## Overview
|
|
41
|
+
|
|
42
|
+
pympacds provides a common foundation for building systems composed of multiple cooperating daemons that communicate via D-Bus. Each daemon runs as an independent process (managed by systemd as a service), uses Python's `asyncio` for internal concurrency, and automatically discovers peer services on the D-Bus system bus.
|
|
43
|
+
|
|
44
|
+
The framework is designed for any domain — IoT gateways, industrial control, home automation, automotive, robotics, or any Linux-based distributed system.
|
|
45
|
+
|
|
46
|
+
## Architecture
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
50
|
+
│ D-Bus System Bus │
|
|
51
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
52
|
+
│ │ Service A │ │ Service B │ │ Service C │ │ Service D │ │
|
|
53
|
+
│ │ (sensor) │ │ (storage) │ │ (comm) │ │ (ui) │ │
|
|
54
|
+
│ │ ProcessBase│ │ ProcessBase│ │ ProcessBase│ │ ProcessBase│ │
|
|
55
|
+
│ │ DBusManager│ │ DBusManager│ │ DBusManager│ │ DBusManager│ │
|
|
56
|
+
│ │ task1 │ │ task1 │ │ task1 │ │ task1 │ │
|
|
57
|
+
│ │ task2 │ │ task2 │ │ │ │ │ │
|
|
58
|
+
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
|
59
|
+
│ │ │ │ │ │
|
|
60
|
+
│ discover ←────────→ discover ←────→ discover ←──→ discover │
|
|
61
|
+
│ (auto peer detection via NameOwnerChanged) │
|
|
62
|
+
└─────────────────────────────────────────────────────────────┘
|
|
63
|
+
│
|
|
64
|
+
┌─────────┴──────────┐
|
|
65
|
+
│ systemd (PID 1) │
|
|
66
|
+
│ service-a.service │
|
|
67
|
+
│ service-b.service │
|
|
68
|
+
│ service-c.service │
|
|
69
|
+
└────────────────────┘
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Features
|
|
73
|
+
|
|
74
|
+
pympacds provides:
|
|
75
|
+
|
|
76
|
+
- **Process lifecycle** — `ProcessBase` with setup/init/main/close, structured logging, signal handling, and named task management.
|
|
77
|
+
- **D-Bus transport** — `DBusManager` over `dbus-fast`, configurable namespace prefix, and graceful disconnect.
|
|
78
|
+
- **Service discovery** — automatic peer detection and dynamic subscribe/unsubscribe between services.
|
|
79
|
+
- **Interface contracts** — validated D-Bus interface definitions, with built-in `Health`, `Lifecycle`, `Config`, and `Metrics` contracts.
|
|
80
|
+
- **Middleware** — optional lifecycle extensions (e.g. HTTP config provisioning).
|
|
81
|
+
|
|
82
|
+
See [Features and Philosophy](doc/features.md) for the full details.
|
|
83
|
+
|
|
84
|
+
## Example: Minimal Service
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import asyncio
|
|
88
|
+
import time
|
|
89
|
+
from pympacds.process import ProcessBase
|
|
90
|
+
from pympacds.dbus import DBusManager
|
|
91
|
+
from pympacds.contracts import ServiceContract, dbus_method, dbus_signal
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class MyContract(ServiceContract):
|
|
95
|
+
def __init__(self, ifname, base):
|
|
96
|
+
super().__init__(ifname, base)
|
|
97
|
+
self._require("dbus_my_method")
|
|
98
|
+
|
|
99
|
+
@dbus_method()
|
|
100
|
+
def my_method(self, arg: "s") -> "s":
|
|
101
|
+
return self.base.dbus_my_method(arg)
|
|
102
|
+
|
|
103
|
+
@dbus_signal()
|
|
104
|
+
def heartbeat(self, uptime: "i") -> "i":
|
|
105
|
+
return [uptime]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class MyService(ProcessBase):
|
|
109
|
+
def __init__(self):
|
|
110
|
+
super().__init__(
|
|
111
|
+
name="myservice",
|
|
112
|
+
version="1.0.0",
|
|
113
|
+
description="A minimal pympacds service",
|
|
114
|
+
bus_prefix="com.example.mysystem",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
async def start_dbus(self):
|
|
118
|
+
self.bus = DBusManager(
|
|
119
|
+
logger=self.logger,
|
|
120
|
+
busname="myservice",
|
|
121
|
+
bus_prefix=self.bus_prefix,
|
|
122
|
+
)
|
|
123
|
+
self.iface = MyContract("com.example.MyInterface", self)
|
|
124
|
+
self.bus.add_interface("MyService", self.iface)
|
|
125
|
+
await self.bus.start()
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
def update_tasks(self):
|
|
129
|
+
if "periodic" not in self.tasklist:
|
|
130
|
+
self.tasklist["periodic"] = asyncio.create_task(
|
|
131
|
+
self._periodic(), name="periodic"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def dbus_my_method(self, arg: str) -> str:
|
|
135
|
+
return f"Got: {arg}"
|
|
136
|
+
|
|
137
|
+
async def _periodic(self):
|
|
138
|
+
try:
|
|
139
|
+
while not self.exitevent.is_set():
|
|
140
|
+
await self.do_waitexit(timeout=5)
|
|
141
|
+
self.iface.heartbeat(int(time.time()))
|
|
142
|
+
except asyncio.CancelledError:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
MyService().start()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Installation
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install pympacds
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
For minimal embedded deployments without the optional Cython extension:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
SKIP_CYTHON=1 pip install --no-binary dbus-fast pympacds
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Documentation
|
|
162
|
+
|
|
163
|
+
Detailed documentation lives in the [`doc/`](doc/) directory:
|
|
164
|
+
|
|
165
|
+
- [Features and Philosophy](doc/features.md)
|
|
166
|
+
- [Configuration](doc/config.md)
|
|
167
|
+
- [Built-in Contracts](doc/contracts.md)
|
|
168
|
+
- [Middleware](doc/middleware.md)
|
|
169
|
+
|
|
170
|
+
## Key Concepts
|
|
171
|
+
|
|
172
|
+
| Concept | Description |
|
|
173
|
+
|---------|-------------|
|
|
174
|
+
| **Service** | A single-purpose daemon that subclasses `ProcessBase` and exports a D-Bus interface |
|
|
175
|
+
| **Bus** | D-Bus system bus used as the IPC backbone between services |
|
|
176
|
+
| **Discovery** | Automatic peer detection via bus name conventions and `NameOwnerChanged` signals |
|
|
177
|
+
| **Interface Contract** | A standardized D-Bus interface template that enforces required callbacks at construction time |
|
|
178
|
+
| **Task** | An asyncio coroutine managed inside a service's event loop |
|
|
179
|
+
| **Lifecycle** | `setup → init_loop → main_loop → close_loop` pattern inherited from `ProcessBase` |
|
|
180
|
+
|
|
181
|
+
## Related Work
|
|
182
|
+
|
|
183
|
+
- [dbus-fast](https://github.com/Bluetooth-Devices/dbus-fast) — underlying D-Bus library
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
pympacds-0.1.0/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# pympacds
|
|
2
|
+
|
|
3
|
+
**Py**thon **M**ulti **P**rocess **A**sync-based **C**ooperative **D**iscoverable **S**ervices Framework
|
|
4
|
+
|
|
5
|
+
A lightweight, D-Bus-native application framework for building modular, multi-process, asyncio-based daemons on Linux — particularly suited for embedded and IoT systems.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
pympacds provides a common foundation for building systems composed of multiple cooperating daemons that communicate via D-Bus. Each daemon runs as an independent process (managed by systemd as a service), uses Python's `asyncio` for internal concurrency, and automatically discovers peer services on the D-Bus system bus.
|
|
10
|
+
|
|
11
|
+
The framework is designed for any domain — IoT gateways, industrial control, home automation, automotive, robotics, or any Linux-based distributed system.
|
|
12
|
+
|
|
13
|
+
## Architecture
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
17
|
+
│ D-Bus System Bus │
|
|
18
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
19
|
+
│ │ Service A │ │ Service B │ │ Service C │ │ Service D │ │
|
|
20
|
+
│ │ (sensor) │ │ (storage) │ │ (comm) │ │ (ui) │ │
|
|
21
|
+
│ │ ProcessBase│ │ ProcessBase│ │ ProcessBase│ │ ProcessBase│ │
|
|
22
|
+
│ │ DBusManager│ │ DBusManager│ │ DBusManager│ │ DBusManager│ │
|
|
23
|
+
│ │ task1 │ │ task1 │ │ task1 │ │ task1 │ │
|
|
24
|
+
│ │ task2 │ │ task2 │ │ │ │ │ │
|
|
25
|
+
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
|
26
|
+
│ │ │ │ │ │
|
|
27
|
+
│ discover ←────────→ discover ←────→ discover ←──→ discover │
|
|
28
|
+
│ (auto peer detection via NameOwnerChanged) │
|
|
29
|
+
└─────────────────────────────────────────────────────────────┘
|
|
30
|
+
│
|
|
31
|
+
┌─────────┴──────────┐
|
|
32
|
+
│ systemd (PID 1) │
|
|
33
|
+
│ service-a.service │
|
|
34
|
+
│ service-b.service │
|
|
35
|
+
│ service-c.service │
|
|
36
|
+
└────────────────────┘
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
pympacds provides:
|
|
42
|
+
|
|
43
|
+
- **Process lifecycle** — `ProcessBase` with setup/init/main/close, structured logging, signal handling, and named task management.
|
|
44
|
+
- **D-Bus transport** — `DBusManager` over `dbus-fast`, configurable namespace prefix, and graceful disconnect.
|
|
45
|
+
- **Service discovery** — automatic peer detection and dynamic subscribe/unsubscribe between services.
|
|
46
|
+
- **Interface contracts** — validated D-Bus interface definitions, with built-in `Health`, `Lifecycle`, `Config`, and `Metrics` contracts.
|
|
47
|
+
- **Middleware** — optional lifecycle extensions (e.g. HTTP config provisioning).
|
|
48
|
+
|
|
49
|
+
See [Features and Philosophy](doc/features.md) for the full details.
|
|
50
|
+
|
|
51
|
+
## Example: Minimal Service
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import asyncio
|
|
55
|
+
import time
|
|
56
|
+
from pympacds.process import ProcessBase
|
|
57
|
+
from pympacds.dbus import DBusManager
|
|
58
|
+
from pympacds.contracts import ServiceContract, dbus_method, dbus_signal
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class MyContract(ServiceContract):
|
|
62
|
+
def __init__(self, ifname, base):
|
|
63
|
+
super().__init__(ifname, base)
|
|
64
|
+
self._require("dbus_my_method")
|
|
65
|
+
|
|
66
|
+
@dbus_method()
|
|
67
|
+
def my_method(self, arg: "s") -> "s":
|
|
68
|
+
return self.base.dbus_my_method(arg)
|
|
69
|
+
|
|
70
|
+
@dbus_signal()
|
|
71
|
+
def heartbeat(self, uptime: "i") -> "i":
|
|
72
|
+
return [uptime]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class MyService(ProcessBase):
|
|
76
|
+
def __init__(self):
|
|
77
|
+
super().__init__(
|
|
78
|
+
name="myservice",
|
|
79
|
+
version="1.0.0",
|
|
80
|
+
description="A minimal pympacds service",
|
|
81
|
+
bus_prefix="com.example.mysystem",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
async def start_dbus(self):
|
|
85
|
+
self.bus = DBusManager(
|
|
86
|
+
logger=self.logger,
|
|
87
|
+
busname="myservice",
|
|
88
|
+
bus_prefix=self.bus_prefix,
|
|
89
|
+
)
|
|
90
|
+
self.iface = MyContract("com.example.MyInterface", self)
|
|
91
|
+
self.bus.add_interface("MyService", self.iface)
|
|
92
|
+
await self.bus.start()
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
def update_tasks(self):
|
|
96
|
+
if "periodic" not in self.tasklist:
|
|
97
|
+
self.tasklist["periodic"] = asyncio.create_task(
|
|
98
|
+
self._periodic(), name="periodic"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def dbus_my_method(self, arg: str) -> str:
|
|
102
|
+
return f"Got: {arg}"
|
|
103
|
+
|
|
104
|
+
async def _periodic(self):
|
|
105
|
+
try:
|
|
106
|
+
while not self.exitevent.is_set():
|
|
107
|
+
await self.do_waitexit(timeout=5)
|
|
108
|
+
self.iface.heartbeat(int(time.time()))
|
|
109
|
+
except asyncio.CancelledError:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
MyService().start()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Installation
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pip install pympacds
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
For minimal embedded deployments without the optional Cython extension:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
SKIP_CYTHON=1 pip install --no-binary dbus-fast pympacds
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Documentation
|
|
129
|
+
|
|
130
|
+
Detailed documentation lives in the [`doc/`](doc/) directory:
|
|
131
|
+
|
|
132
|
+
- [Features and Philosophy](doc/features.md)
|
|
133
|
+
- [Configuration](doc/config.md)
|
|
134
|
+
- [Built-in Contracts](doc/contracts.md)
|
|
135
|
+
- [Middleware](doc/middleware.md)
|
|
136
|
+
|
|
137
|
+
## Key Concepts
|
|
138
|
+
|
|
139
|
+
| Concept | Description |
|
|
140
|
+
|---------|-------------|
|
|
141
|
+
| **Service** | A single-purpose daemon that subclasses `ProcessBase` and exports a D-Bus interface |
|
|
142
|
+
| **Bus** | D-Bus system bus used as the IPC backbone between services |
|
|
143
|
+
| **Discovery** | Automatic peer detection via bus name conventions and `NameOwnerChanged` signals |
|
|
144
|
+
| **Interface Contract** | A standardized D-Bus interface template that enforces required callbacks at construction time |
|
|
145
|
+
| **Task** | An asyncio coroutine managed inside a service's event loop |
|
|
146
|
+
| **Lifecycle** | `setup → init_loop → main_loop → close_loop` pattern inherited from `ProcessBase` |
|
|
147
|
+
|
|
148
|
+
## Related Work
|
|
149
|
+
|
|
150
|
+
- [dbus-fast](https://github.com/Bluetooth-Devices/dbus-fast) — underlying D-Bus library
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pympacds"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python Multi Process Asyncio-based Cooperative Discoverable Services Framework"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{name="Oscar Diaz", email="odiaz@ieee.org"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["dbus", "asyncio", "daemon", "framework", "embedded", "ipc"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
pympacds-admin = "pympacds.cli:main"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dbus-fast = ["dbus-fast>=4.0"]
|
|
34
|
+
dbus-next = ["dbus-next>=0.2"]
|
|
35
|
+
dev = ["pytest", "pytest-asyncio", "mypy", "ruff"]
|
|
36
|
+
docs = ["sphinx>=7.0", "myst-parser>=2.0", "furo>=2023.9.10"]
|
|
37
|
+
|
|
38
|
+
[project.entry-points."pympacds.middleware"]
|
|
39
|
+
httpconfprov = "pympacds.middleware:HttpConfigMiddleware"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
asyncio_mode = "auto"
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
pythonpath = ["src"]
|
|
48
|
+
addopts = [
|
|
49
|
+
"-v",
|
|
50
|
+
"--tb=short",
|
|
51
|
+
]
|
|
52
|
+
filterwarnings = [
|
|
53
|
+
# Third-party deprecation from PyGObject (triggered by the D-Bus library's
|
|
54
|
+
# GLib backend import); pympacds uses asyncio and does not use GLib.
|
|
55
|
+
"ignore:GLib.unix_signal_add_full is deprecated.*:gi.PyGIDeprecationWarning",
|
|
56
|
+
]
|
pympacds-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pympacds — Python Multi Process Asyncio-based Cooperative Discoverable Services Framework.
|
|
3
|
+
|
|
4
|
+
A lightweight, D-Bus-native application framework for building modular,
|
|
5
|
+
multi-process, asyncio-based daemons on Linux.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
_logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
_DBUS_BACKEND: str | None = None
|
|
14
|
+
_dbus_lib: object | None = None
|
|
15
|
+
_dbus_aio: object | None = None
|
|
16
|
+
_dbus_service: object | None = None
|
|
17
|
+
|
|
18
|
+
_import_error_msg: str = "pympacds requires a D-Bus library. "\
|
|
19
|
+
"Install either 'dbus-fast' (recommended) or 'dbus-next':\n"\
|
|
20
|
+
" pip install dbus-fast\n"\
|
|
21
|
+
" pip install dbus-next"
|
|
22
|
+
|
|
23
|
+
def _import_dbus() -> tuple[str, object, object, object]:
|
|
24
|
+
"""Import the D-Bus library in priority order.
|
|
25
|
+
|
|
26
|
+
Tries ``dbus-fast`` first, then ``dbus-next``. Set the environment
|
|
27
|
+
variable ``PYMPACDS_DBUS_BACKEND`` to ``dbus-fast`` or ``dbus-next``
|
|
28
|
+
to force a specific backend.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Tuple of (backend_name, dbus_lib_module, dbus_aio_module, dbus_service_module).
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ImportError: If neither library is installed.
|
|
35
|
+
"""
|
|
36
|
+
global _DBUS_BACKEND, _dbus_lib, _dbus_aio, _dbus_service
|
|
37
|
+
|
|
38
|
+
if _DBUS_BACKEND is not None:
|
|
39
|
+
return _DBUS_BACKEND, _dbus_lib, _dbus_aio, _dbus_service
|
|
40
|
+
|
|
41
|
+
if os.environ.get("PYMPACDS_DBUS_BACKEND", "") == "dbus-fast":
|
|
42
|
+
# force dbus-fast
|
|
43
|
+
_try_dbus_fast_import(True)
|
|
44
|
+
elif os.environ.get("PYMPACDS_DBUS_BACKEND", "") == "dbus-next":
|
|
45
|
+
# force dbus-next
|
|
46
|
+
_try_dbus_next_import(True)
|
|
47
|
+
else:
|
|
48
|
+
# try first dbus-fast
|
|
49
|
+
if not _try_dbus_fast_import(False):
|
|
50
|
+
# then dbus-next
|
|
51
|
+
if not _try_dbus_next_import(False):
|
|
52
|
+
# neither, raise error
|
|
53
|
+
raise ImportError(_import_error_msg)
|
|
54
|
+
|
|
55
|
+
_logger.info("D-Bus backend: %s", _DBUS_BACKEND)
|
|
56
|
+
return _DBUS_BACKEND, _dbus_lib, _dbus_aio, _dbus_service
|
|
57
|
+
|
|
58
|
+
def _try_dbus_fast_import(doraise: bool) -> bool:
|
|
59
|
+
"""Attempt to import ``dbus-fast``.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
doraise: If True, raise ImportError instead of returning False.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if the import succeeded.
|
|
66
|
+
"""
|
|
67
|
+
global _DBUS_BACKEND, _dbus_lib, _dbus_aio, _dbus_service
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
import dbus_fast as _dbus_lib
|
|
71
|
+
import dbus_fast.aio.message_bus as _dbus_aio
|
|
72
|
+
import dbus_fast.service as _dbus_service
|
|
73
|
+
|
|
74
|
+
_DBUS_BACKEND = "dbus-fast"
|
|
75
|
+
except ImportError:
|
|
76
|
+
if doraise:
|
|
77
|
+
raise ImportError(_import_error_msg)
|
|
78
|
+
return False
|
|
79
|
+
else:
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _try_dbus_next_import(doraise: bool) -> bool:
|
|
84
|
+
"""Attempt to import ``dbus-next``.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
doraise: If True, raise ImportError instead of returning False.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True if the import succeeded.
|
|
91
|
+
"""
|
|
92
|
+
global _DBUS_BACKEND, _dbus_lib, _dbus_aio, _dbus_service
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
import dbus_next as _dbus_lib
|
|
96
|
+
import dbus_next.aio.message_bus as _dbus_aio
|
|
97
|
+
import dbus_next.service as _dbus_service
|
|
98
|
+
|
|
99
|
+
_DBUS_BACKEND = "dbus-next"
|
|
100
|
+
except ImportError:
|
|
101
|
+
if doraise:
|
|
102
|
+
raise ImportError(_import_error_msg)
|
|
103
|
+
return False
|
|
104
|
+
else:
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_dbus_backend() -> str:
|
|
109
|
+
"""Return the active D-Bus library name (``"dbus-fast"`` or ``"dbus-next"``)."""
|
|
110
|
+
if _DBUS_BACKEND is None:
|
|
111
|
+
_import_dbus()
|
|
112
|
+
return _DBUS_BACKEND
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_dbus_lib() -> object:
|
|
116
|
+
"""Return the active D-Bus library root module."""
|
|
117
|
+
if _dbus_lib is None:
|
|
118
|
+
_import_dbus()
|
|
119
|
+
return _dbus_lib
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_dbus_aio() -> object:
|
|
123
|
+
"""Return the active D-Bus asyncio module."""
|
|
124
|
+
if _dbus_aio is None:
|
|
125
|
+
_import_dbus()
|
|
126
|
+
return _dbus_aio
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_dbus_service() -> object:
|
|
130
|
+
"""Return the active D-Bus service module."""
|
|
131
|
+
if _dbus_service is None:
|
|
132
|
+
_import_dbus()
|
|
133
|
+
return _dbus_service
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Built-in D-Bus interface contracts.
|
|
2
|
+
|
|
3
|
+
- HealthContract (enabled by default)
|
|
4
|
+
- LifecycleContract, ConfigContract, MetricsContract (opt-in via [dbus] INI)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .contracts import ServiceContract, dbus_method, dbus_signal, dbus_property
|
|
8
|
+
from . import get_dbus_lib
|
|
9
|
+
|
|
10
|
+
_PropertyAccess = get_dbus_lib().PropertyAccess
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HealthContract(ServiceContract):
|
|
14
|
+
"""Service health and diagnostics.
|
|
15
|
+
|
|
16
|
+
Exported at the "health" relative object path automatically
|
|
17
|
+
when [dbus] contract_health = true (default).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
iface_name = "Health"
|
|
21
|
+
iface_version = "1.0.0"
|
|
22
|
+
contract_type = "health"
|
|
23
|
+
|
|
24
|
+
def __init__(self, ifname: str, base):
|
|
25
|
+
super().__init__(ifname, base)
|
|
26
|
+
self._require(
|
|
27
|
+
"dbus_health_ping",
|
|
28
|
+
"dbus_health_status",
|
|
29
|
+
"dbus_health_get_uptime",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@dbus_method()
|
|
33
|
+
def ping(self) -> "b":
|
|
34
|
+
return self.base.dbus_health_ping()
|
|
35
|
+
|
|
36
|
+
@dbus_method()
|
|
37
|
+
def status(self) -> "s":
|
|
38
|
+
return self.base.dbus_health_status()
|
|
39
|
+
|
|
40
|
+
@dbus_signal()
|
|
41
|
+
def heartbeat(self, uptime: "i") -> "i":
|
|
42
|
+
return [uptime]
|
|
43
|
+
|
|
44
|
+
@dbus_property(access=_PropertyAccess.READ)
|
|
45
|
+
def uptime(self) -> "u":
|
|
46
|
+
return self.base.dbus_health_get_uptime()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class LifecycleContract(ServiceContract):
|
|
50
|
+
"""Remote restart and shutdown.
|
|
51
|
+
|
|
52
|
+
Exported when [dbus] contract_lifecycle = true.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
iface_name = "Lifecycle"
|
|
56
|
+
iface_version = "1.0.0"
|
|
57
|
+
contract_type = "lifecycle"
|
|
58
|
+
|
|
59
|
+
def __init__(self, ifname: str, base):
|
|
60
|
+
super().__init__(ifname, base)
|
|
61
|
+
self._require(
|
|
62
|
+
"dbus_lifecycle_restart",
|
|
63
|
+
"dbus_lifecycle_shutdown",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@dbus_method()
|
|
67
|
+
def restart(self) -> "b":
|
|
68
|
+
return self.base.dbus_lifecycle_restart()
|
|
69
|
+
|
|
70
|
+
@dbus_method()
|
|
71
|
+
def shutdown(self) -> "b":
|
|
72
|
+
return self.base.dbus_lifecycle_shutdown()
|
|
73
|
+
|
|
74
|
+
@dbus_signal()
|
|
75
|
+
def state_changed(self, new_state: "s") -> "s":
|
|
76
|
+
return [new_state]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ConfigContract(ServiceContract):
|
|
80
|
+
"""Runtime configuration management.
|
|
81
|
+
|
|
82
|
+
Exported when [dbus] contract_config = true.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
iface_name = "Config"
|
|
86
|
+
iface_version = "1.0.0"
|
|
87
|
+
contract_type = "config"
|
|
88
|
+
|
|
89
|
+
def __init__(self, ifname: str, base):
|
|
90
|
+
super().__init__(ifname, base)
|
|
91
|
+
self._require(
|
|
92
|
+
"dbus_config_get",
|
|
93
|
+
"dbus_config_set",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@dbus_method()
|
|
97
|
+
def get_config(self, section: "s", key: "s") -> "s":
|
|
98
|
+
return self.base.dbus_config_get(section, key)
|
|
99
|
+
|
|
100
|
+
@dbus_method()
|
|
101
|
+
def set_config(self, section: "s", key: "s", value: "s") -> "b":
|
|
102
|
+
return self.base.dbus_config_set(section, key, value)
|
|
103
|
+
|
|
104
|
+
@dbus_signal()
|
|
105
|
+
def config_changed(self, section: "s", key: "s", value: "s") -> "sss":
|
|
106
|
+
return [section, key, value]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class MetricsContract(ServiceContract):
|
|
110
|
+
"""Operational metrics exposition.
|
|
111
|
+
|
|
112
|
+
Exported when [dbus] contract_metrics = true.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
iface_name = "Metrics"
|
|
116
|
+
iface_version = "1.0.0"
|
|
117
|
+
contract_type = "metrics"
|
|
118
|
+
|
|
119
|
+
def __init__(self, ifname: str, base):
|
|
120
|
+
super().__init__(ifname, base)
|
|
121
|
+
self._require("dbus_metrics_get")
|
|
122
|
+
|
|
123
|
+
@dbus_method()
|
|
124
|
+
def get_metrics(self) -> "s":
|
|
125
|
+
return self.base.dbus_metrics_get()
|
|
126
|
+
|
|
127
|
+
@dbus_signal()
|
|
128
|
+
def metric_update(self, name: "s", value: "v") -> "sv":
|
|
129
|
+
return [name, value]
|