pympacds 0.1.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.
- pympacds/__init__.py +133 -0
- pympacds/builtin_contracts.py +129 -0
- pympacds/cli.py +671 -0
- pympacds/config.py +120 -0
- pympacds/contracts.py +126 -0
- pympacds/dbus.py +272 -0
- pympacds/middleware.py +147 -0
- pympacds/process.py +601 -0
- pympacds/service_discovery.py +49 -0
- pympacds/utils.py +31 -0
- pympacds-0.1.0.dist-info/METADATA +187 -0
- pympacds-0.1.0.dist-info/RECORD +15 -0
- pympacds-0.1.0.dist-info/WHEEL +5 -0
- pympacds-0.1.0.dist-info/entry_points.txt +5 -0
- pympacds-0.1.0.dist-info/top_level.txt +1 -0
pympacds/__init__.py
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]
|