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 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]