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.
Files changed (35) hide show
  1. pympacds-0.1.0/PKG-INFO +187 -0
  2. pympacds-0.1.0/README.md +154 -0
  3. pympacds-0.1.0/pyproject.toml +56 -0
  4. pympacds-0.1.0/setup.cfg +4 -0
  5. pympacds-0.1.0/src/pympacds/__init__.py +133 -0
  6. pympacds-0.1.0/src/pympacds/builtin_contracts.py +129 -0
  7. pympacds-0.1.0/src/pympacds/cli.py +671 -0
  8. pympacds-0.1.0/src/pympacds/config.py +120 -0
  9. pympacds-0.1.0/src/pympacds/contracts.py +126 -0
  10. pympacds-0.1.0/src/pympacds/dbus.py +272 -0
  11. pympacds-0.1.0/src/pympacds/middleware.py +147 -0
  12. pympacds-0.1.0/src/pympacds/process.py +601 -0
  13. pympacds-0.1.0/src/pympacds/service_discovery.py +49 -0
  14. pympacds-0.1.0/src/pympacds/utils.py +31 -0
  15. pympacds-0.1.0/src/pympacds.egg-info/PKG-INFO +187 -0
  16. pympacds-0.1.0/src/pympacds.egg-info/SOURCES.txt +33 -0
  17. pympacds-0.1.0/src/pympacds.egg-info/dependency_links.txt +1 -0
  18. pympacds-0.1.0/src/pympacds.egg-info/entry_points.txt +5 -0
  19. pympacds-0.1.0/src/pympacds.egg-info/requires.txt +17 -0
  20. pympacds-0.1.0/src/pympacds.egg-info/top_level.txt +1 -0
  21. pympacds-0.1.0/tests/test_cli.py +185 -0
  22. pympacds-0.1.0/tests/test_cli_install.py +106 -0
  23. pympacds-0.1.0/tests/test_cli_integration.py +280 -0
  24. pympacds-0.1.0/tests/test_config.py +267 -0
  25. pympacds-0.1.0/tests/test_contracts.py +211 -0
  26. pympacds-0.1.0/tests/test_coverage.py +447 -0
  27. pympacds-0.1.0/tests/test_dbus.py +106 -0
  28. pympacds-0.1.0/tests/test_dbus_integration.py +115 -0
  29. pympacds-0.1.0/tests/test_examples.py +123 -0
  30. pympacds-0.1.0/tests/test_import.py +141 -0
  31. pympacds-0.1.0/tests/test_middleware.py +163 -0
  32. pympacds-0.1.0/tests/test_process.py +555 -0
  33. pympacds-0.1.0/tests/test_process_supplement.py +124 -0
  34. pympacds-0.1.0/tests/test_service_discovery.py +87 -0
  35. pympacds-0.1.0/tests/test_utils.py +23 -0
@@ -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
@@ -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
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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]