pepeunit-client 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.
- pepeunit_client/__init__.py +11 -0
- pepeunit_client/abstract_clients.py +156 -0
- pepeunit_client/client.py +308 -0
- pepeunit_client/enums.py +56 -0
- pepeunit_client/file_manager.py +159 -0
- pepeunit_client/logger.py +92 -0
- pepeunit_client/pepeunit_mqtt_client.py +77 -0
- pepeunit_client/pepeunit_rest_client.py +82 -0
- pepeunit_client/schema_manager.py +81 -0
- pepeunit_client/settings.py +54 -0
- pepeunit_client-0.10.0.dist-info/METADATA +485 -0
- pepeunit_client-0.10.0.dist-info/RECORD +14 -0
- pepeunit_client-0.10.0.dist-info/WHEEL +4 -0
- pepeunit_client-0.10.0.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PepeunitClient - Мультиплатформенная библиотека для работы с Pepeunit Unit Storage
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "0.10.0"
|
|
6
|
+
|
|
7
|
+
from .client import PepeunitClient
|
|
8
|
+
from .abstract_clients import AbstractPepeunitMqttClient, AbstractPepeunitRestClient
|
|
9
|
+
from .enums import RestartMode
|
|
10
|
+
|
|
11
|
+
__all__ = ['PepeunitClient', 'AbstractPepeunitMqttClient', 'AbstractPepeunitRestClient', 'RestartMode']
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional, Callable, List, Dict, Any, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from .settings import Settings
|
|
6
|
+
from .schema_manager import SchemaManager
|
|
7
|
+
from .logger import Logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AbstractPepeunitMqttClient(ABC):
|
|
11
|
+
"""
|
|
12
|
+
Abstract base class for Pepeunit MQTT clients.
|
|
13
|
+
|
|
14
|
+
Users can inherit from this class to create custom MQTT clients
|
|
15
|
+
that integrate with the Pepeunit ecosystem.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, settings: 'Settings', schema_manager: 'SchemaManager', logger: 'Logger'):
|
|
19
|
+
self.settings = settings
|
|
20
|
+
self.schema_manager = schema_manager
|
|
21
|
+
self.logger = logger
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def connect(self) -> None:
|
|
25
|
+
"""Connect to MQTT broker using settings configuration."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def disconnect(self) -> None:
|
|
30
|
+
"""Disconnect from MQTT broker."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def subscribe_topics(self, topics: List[str]) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Subscribe to specific MQTT topics.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
topics: List of topic strings to subscribe to
|
|
40
|
+
"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def publish(self, topic: str, message: str) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Publish message to specific topic.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
topic: MQTT topic to publish to
|
|
50
|
+
message: Message content to publish
|
|
51
|
+
"""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def set_input_handler(self, handler: Callable) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Set user-defined input message handler.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
handler: Function to handle incoming MQTT messages
|
|
61
|
+
"""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AbstractPepeunitRestClient(ABC):
|
|
66
|
+
"""
|
|
67
|
+
Abstract base class for Pepeunit REST clients.
|
|
68
|
+
|
|
69
|
+
Users can inherit from this class to create custom REST clients
|
|
70
|
+
that integrate with the Pepeunit API.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, settings: 'Settings'):
|
|
74
|
+
self.settings = settings
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def download_update(self, file_path: str) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Download firmware update archive.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
unit_uuid: UUID of the unit to download update for
|
|
83
|
+
file_path: Local path where to save the downloaded file
|
|
84
|
+
"""
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def download_env(self, file_path: str) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Download env.json configuration.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
unit_uuid: UUID of the unit to download config for
|
|
94
|
+
file_path: Local path where to save the env.json file
|
|
95
|
+
"""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def download_schema(self, file_path: str) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Download schema.json configuration.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
unit_uuid: UUID of the unit to download schema for
|
|
105
|
+
file_path: Local path where to save the schema.json file
|
|
106
|
+
"""
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def set_state_storage(self, state: Dict[str, Any]) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Store state data in Pepeunit Unit Storage.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
unit_uuid: UUID of the unit
|
|
116
|
+
state: State data to store
|
|
117
|
+
"""
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
@abstractmethod
|
|
121
|
+
def get_state_storage(self, unit_uuid: str) -> Dict[str, Any]:
|
|
122
|
+
"""
|
|
123
|
+
Retrieve state data from Pepeunit Unit Storage.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
unit_uuid: UUID of the unit
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Retrieved state data
|
|
130
|
+
"""
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
def _get_auth_headers(self) -> Dict[str, str]:
|
|
134
|
+
"""
|
|
135
|
+
Get authentication headers for API requests.
|
|
136
|
+
|
|
137
|
+
Users can override this if they need custom authentication.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Dictionary with authentication headers
|
|
141
|
+
"""
|
|
142
|
+
return {
|
|
143
|
+
'accept': 'application/json',
|
|
144
|
+
'x-auth-token': self.settings.PEPEUNIT_TOKEN,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
def _get_base_url(self) -> str:
|
|
148
|
+
"""
|
|
149
|
+
Get base URL for Pepeunit API.
|
|
150
|
+
|
|
151
|
+
Users can override this if they need custom URL construction.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Base URL string
|
|
155
|
+
"""
|
|
156
|
+
return f"{self.settings.HTTP_TYPE}://{self.settings.PEPEUNIT_URL}{self.settings.PEPEUNIT_APP_PREFIX}{self.settings.PEPEUNIT_API_ACTUAL_PREFIX}"
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
import time
|
|
5
|
+
import sys
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Optional, Dict, Any, Callable
|
|
8
|
+
|
|
9
|
+
# Import for mocking in tests
|
|
10
|
+
try:
|
|
11
|
+
import psutil
|
|
12
|
+
except ImportError:
|
|
13
|
+
psutil = None
|
|
14
|
+
|
|
15
|
+
from .settings import Settings
|
|
16
|
+
from .file_manager import FileManager
|
|
17
|
+
from .logger import Logger
|
|
18
|
+
from .schema_manager import SchemaManager
|
|
19
|
+
from .abstract_clients import AbstractPepeunitMqttClient, AbstractPepeunitRestClient
|
|
20
|
+
from .pepeunit_mqtt_client import PepeunitMqttClient
|
|
21
|
+
from .pepeunit_rest_client import PepeunitRestClient
|
|
22
|
+
from .enums import BaseInputTopicType, BaseOutputTopicType, RestartMode
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PepeunitClient:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
env_file_path: str,
|
|
29
|
+
schema_file_path: str,
|
|
30
|
+
log_file_path: str,
|
|
31
|
+
enable_mqtt: bool = False,
|
|
32
|
+
enable_rest: bool = False,
|
|
33
|
+
mqtt_client: Optional[AbstractPepeunitMqttClient] = None,
|
|
34
|
+
rest_client: Optional[AbstractPepeunitRestClient] = None,
|
|
35
|
+
cycle_speed: float = 0.1,
|
|
36
|
+
restart_mode: RestartMode = RestartMode.RESTART_EXEC,
|
|
37
|
+
skip_version_check=False
|
|
38
|
+
):
|
|
39
|
+
self.env_file_path = env_file_path
|
|
40
|
+
self.schema_file_path = schema_file_path
|
|
41
|
+
self.log_file_path = log_file_path
|
|
42
|
+
self.enable_mqtt = enable_mqtt
|
|
43
|
+
self.enable_rest = enable_rest
|
|
44
|
+
self.cycle_speed = cycle_speed
|
|
45
|
+
self.restart_mode = restart_mode
|
|
46
|
+
self.skip_version_check = skip_version_check
|
|
47
|
+
|
|
48
|
+
self.settings = Settings(env_file_path)
|
|
49
|
+
self.schema = SchemaManager(schema_file_path)
|
|
50
|
+
self.logger = Logger(log_file_path, None, self.schema, self.settings)
|
|
51
|
+
|
|
52
|
+
self.mqtt_client = (mqtt_client if mqtt_client else self._get_default_mqtt_client()) if enable_mqtt else None
|
|
53
|
+
self.rest_client = (rest_client if rest_client else self._get_default_rest_client()) if enable_rest else None
|
|
54
|
+
|
|
55
|
+
if self.mqtt_client:
|
|
56
|
+
self.logger.mqtt_client = self.mqtt_client
|
|
57
|
+
|
|
58
|
+
self.mqtt_input_handler: Optional[Callable] = None
|
|
59
|
+
self.mqtt_output_handler: Optional[Callable] = None
|
|
60
|
+
self.custom_update_handler: Optional[Callable] = None
|
|
61
|
+
|
|
62
|
+
self._running = False
|
|
63
|
+
self._last_state_send = 0
|
|
64
|
+
|
|
65
|
+
def _get_default_mqtt_client(self) -> Optional[AbstractPepeunitMqttClient]:
|
|
66
|
+
return PepeunitMqttClient(self.settings, self.schema, self.logger)
|
|
67
|
+
|
|
68
|
+
def _get_default_rest_client(self) -> Optional[AbstractPepeunitRestClient]:
|
|
69
|
+
return PepeunitRestClient(self.settings)
|
|
70
|
+
|
|
71
|
+
def get_system_state(self) -> Dict[str, Any]:
|
|
72
|
+
if psutil is not None:
|
|
73
|
+
try:
|
|
74
|
+
memory_info = psutil.virtual_memory()
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
cpu_freq = psutil.cpu_freq()
|
|
78
|
+
freq = cpu_freq.current if cpu_freq else 0
|
|
79
|
+
except (AttributeError, OSError):
|
|
80
|
+
freq = 0
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
'millis': round(time.time() * 1000),
|
|
84
|
+
'mem_free': memory_info.available,
|
|
85
|
+
'mem_alloc': memory_info.total - memory_info.available,
|
|
86
|
+
'freq': freq,
|
|
87
|
+
'commit_version': self.settings.COMMIT_VERSION,
|
|
88
|
+
}
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
'millis': round(time.time() * 1000),
|
|
94
|
+
'mem_free': 0,
|
|
95
|
+
'mem_alloc': 0,
|
|
96
|
+
'freq': 0,
|
|
97
|
+
'commit_version': self.settings.COMMIT_VERSION,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
def set_mqtt_input_handler(self, handler: Callable) -> None:
|
|
101
|
+
|
|
102
|
+
if not self.mqtt_client:
|
|
103
|
+
raise RuntimeError("MQTT client is not available")
|
|
104
|
+
|
|
105
|
+
self.mqtt_input_handler = handler
|
|
106
|
+
|
|
107
|
+
def combined_handler(msg):
|
|
108
|
+
self._base_mqtt_input_func(msg)
|
|
109
|
+
if self.mqtt_input_handler:
|
|
110
|
+
self.mqtt_input_handler(self, msg)
|
|
111
|
+
|
|
112
|
+
self.mqtt_client.set_input_handler(combined_handler)
|
|
113
|
+
|
|
114
|
+
def _base_mqtt_input_func(self, msg) -> None:
|
|
115
|
+
try:
|
|
116
|
+
for topic_key in self.schema.input_base_topic:
|
|
117
|
+
if msg.topic in self.schema.input_base_topic[topic_key]:
|
|
118
|
+
self.logger.info(f'Get base MQTT command: {topic_key}')
|
|
119
|
+
|
|
120
|
+
if topic_key == BaseInputTopicType.ENV_UPDATE_PEPEUNIT.value:
|
|
121
|
+
self.download_env(self.env_file_path)
|
|
122
|
+
elif topic_key == BaseInputTopicType.SCHEMA_UPDATE_PEPEUNIT.value:
|
|
123
|
+
self.download_schema(self.schema_file_path)
|
|
124
|
+
elif topic_key == BaseInputTopicType.UPDATE_PEPEUNIT.value:
|
|
125
|
+
self._handle_update(msg)
|
|
126
|
+
elif topic_key == BaseInputTopicType.LOG_SYNC_PEPEUNIT.value:
|
|
127
|
+
self._handle_log_sync()
|
|
128
|
+
break
|
|
129
|
+
except Exception as e:
|
|
130
|
+
self.logger.error(f"Error in base MQTT input handler: {str(e)}")
|
|
131
|
+
|
|
132
|
+
def download_env(self, file_path: str) -> None:
|
|
133
|
+
if not self.rest_client:
|
|
134
|
+
raise RuntimeError("REST client is not available")
|
|
135
|
+
|
|
136
|
+
if not self.mqtt_client:
|
|
137
|
+
raise RuntimeError("MQTT client is not available")
|
|
138
|
+
|
|
139
|
+
self.rest_client.download_env(file_path)
|
|
140
|
+
self.settings.load_from_file()
|
|
141
|
+
self.logger.info('Success update env')
|
|
142
|
+
|
|
143
|
+
def download_schema(self, file_path: str) -> None:
|
|
144
|
+
if not self.rest_client:
|
|
145
|
+
raise RuntimeError("REST client is not available")
|
|
146
|
+
|
|
147
|
+
if not self.mqtt_client:
|
|
148
|
+
raise RuntimeError("MQTT client is not available")
|
|
149
|
+
|
|
150
|
+
self.rest_client.download_schema(file_path)
|
|
151
|
+
self.schema.update_from_file()
|
|
152
|
+
self.subscribe_all_schema_topics()
|
|
153
|
+
self.logger.info('Success update schema')
|
|
154
|
+
|
|
155
|
+
def set_state_storage(self, state: str) -> None:
|
|
156
|
+
if not self.rest_client:
|
|
157
|
+
raise RuntimeError("REST client is not available")
|
|
158
|
+
|
|
159
|
+
self.rest_client.set_state_storage(state)
|
|
160
|
+
|
|
161
|
+
def get_state_storage(self) -> str:
|
|
162
|
+
if not self.rest_client:
|
|
163
|
+
raise RuntimeError("REST client is not available")
|
|
164
|
+
|
|
165
|
+
return self.rest_client.get_state_storage()
|
|
166
|
+
|
|
167
|
+
def _handle_update(self, msg) -> None:
|
|
168
|
+
|
|
169
|
+
payload = json.loads(msg.payload) if msg.payload else {}
|
|
170
|
+
|
|
171
|
+
if not self.rest_client:
|
|
172
|
+
raise RuntimeError("REST client is not available")
|
|
173
|
+
|
|
174
|
+
if not self.mqtt_client:
|
|
175
|
+
raise RuntimeError("MQTT client is not available")
|
|
176
|
+
|
|
177
|
+
if not self.skip_version_check and self.settings.COMMIT_VERSION == payload.get('NEW_COMMIT_VERSION'):
|
|
178
|
+
self.logger.info('No update needed: current version = target version')
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
if self.custom_update_handler:
|
|
182
|
+
self.custom_update_handler(self, payload)
|
|
183
|
+
else:
|
|
184
|
+
temp_dir = tempfile.gettempdir()
|
|
185
|
+
archive_path = os.path.join(temp_dir, f"update_{self.settings.unit_uuid}.tar.gz")
|
|
186
|
+
|
|
187
|
+
self.rest_client.download_update(archive_path)
|
|
188
|
+
self.logger.info('Success download update archive', file_only=True)
|
|
189
|
+
|
|
190
|
+
self.update_device_program(archive_path)
|
|
191
|
+
self.logger.info('Success extract archive', file_only=True)
|
|
192
|
+
|
|
193
|
+
self.logger.info("Full update cycle completed successfully")
|
|
194
|
+
|
|
195
|
+
def update_device_program(self, archive_path: str) -> None:
|
|
196
|
+
|
|
197
|
+
unit_directory = os.path.dirname(self.env_file_path) or os.getcwd()
|
|
198
|
+
with tempfile.TemporaryDirectory() as temp_extract_dir:
|
|
199
|
+
FileManager.extract_tar_gz(archive_path, temp_extract_dir)
|
|
200
|
+
self.logger.info(f"Extracted archive to {temp_extract_dir}")
|
|
201
|
+
|
|
202
|
+
FileManager.copy_directory_contents(temp_extract_dir, unit_directory)
|
|
203
|
+
self.logger.info(f"Copied directory contents from {temp_extract_dir} to {unit_directory}")
|
|
204
|
+
|
|
205
|
+
os.remove(archive_path)
|
|
206
|
+
self.logger.info(f"Archive removed {archive_path}")
|
|
207
|
+
|
|
208
|
+
if self.restart_mode == RestartMode.RESTART_POPEN:
|
|
209
|
+
self.stop_main_cycle()
|
|
210
|
+
|
|
211
|
+
self.logger.info('Run new main cycle in other process')
|
|
212
|
+
subprocess.Popen([sys.executable] + sys.argv)
|
|
213
|
+
|
|
214
|
+
self.logger.info('I`ll Be Back - stop this process')
|
|
215
|
+
sys.exit(0)
|
|
216
|
+
elif self.restart_mode == RestartMode.RESTART_EXEC:
|
|
217
|
+
self.stop_main_cycle()
|
|
218
|
+
|
|
219
|
+
self.logger.info('I`ll Be Back - replacing current process')
|
|
220
|
+
os.execv(sys.executable, [sys.executable] + sys.argv)
|
|
221
|
+
elif self.restart_mode == RestartMode.ENV_SCHEMA_ONLY:
|
|
222
|
+
self.logger.info('Updating env and schema only, without restart')
|
|
223
|
+
self._update_env_schema_only()
|
|
224
|
+
elif self.restart_mode == RestartMode.NO_RESTART:
|
|
225
|
+
self.logger.info('Archive extracted, no restart or updates performed')
|
|
226
|
+
|
|
227
|
+
def _update_env_schema_only(self) -> None:
|
|
228
|
+
self.settings.load_from_file()
|
|
229
|
+
self.schema.update_from_file()
|
|
230
|
+
|
|
231
|
+
if self.enable_mqtt and self.mqtt_client:
|
|
232
|
+
self.subscribe_all_schema_topics()
|
|
233
|
+
|
|
234
|
+
self.logger.info('Environment and schema updated successfully')
|
|
235
|
+
|
|
236
|
+
def _handle_log_sync(self) -> None:
|
|
237
|
+
topic = self.schema.output_base_topic[BaseOutputTopicType.LOG_PEPEUNIT.value][0]
|
|
238
|
+
log_data = self.logger.get_full_log()
|
|
239
|
+
if self.mqtt_client:
|
|
240
|
+
self.mqtt_client.publish(topic, json.dumps(log_data))
|
|
241
|
+
self.logger.info("Log sync completed")
|
|
242
|
+
|
|
243
|
+
def subscribe_all_schema_topics(self) -> None:
|
|
244
|
+
if not self.mqtt_client:
|
|
245
|
+
raise RuntimeError("MQTT client is not available")
|
|
246
|
+
|
|
247
|
+
topics = []
|
|
248
|
+
|
|
249
|
+
for topic_list in self.schema.input_base_topic.values():
|
|
250
|
+
topics.extend(topic_list)
|
|
251
|
+
|
|
252
|
+
for topic_list in self.schema.input_topic.values():
|
|
253
|
+
topics.extend(topic_list)
|
|
254
|
+
|
|
255
|
+
self.logger.info(f'Need a subscription for {len(topic_list)} topics')
|
|
256
|
+
|
|
257
|
+
self.mqtt_client.subscribe_topics(topics)
|
|
258
|
+
|
|
259
|
+
def publish_to_topics(self, topic_key: str, message: str) -> None:
|
|
260
|
+
if not self.mqtt_client:
|
|
261
|
+
raise RuntimeError("MQTT client is not available")
|
|
262
|
+
|
|
263
|
+
topics = []
|
|
264
|
+
|
|
265
|
+
if topic_key in self.schema.output_topic:
|
|
266
|
+
topics.extend(self.schema.output_topic[topic_key])
|
|
267
|
+
elif topic_key in self.schema.output_base_topic:
|
|
268
|
+
topics.extend(self.schema.output_base_topic[topic_key])
|
|
269
|
+
|
|
270
|
+
for topic in topics:
|
|
271
|
+
self.mqtt_client.publish(topic, message)
|
|
272
|
+
|
|
273
|
+
def _base_mqtt_output_handler(self) -> None:
|
|
274
|
+
current_time = time.time()
|
|
275
|
+
if BaseOutputTopicType.STATE_PEPEUNIT.value in self.schema.output_base_topic:
|
|
276
|
+
if current_time - self._last_state_send >= self.settings.STATE_SEND_INTERVAL:
|
|
277
|
+
topic = self.schema.output_base_topic[BaseOutputTopicType.STATE_PEPEUNIT.value][0]
|
|
278
|
+
state_data = self.get_system_state()
|
|
279
|
+
if self.mqtt_client:
|
|
280
|
+
self.mqtt_client.publish(topic, json.dumps(state_data))
|
|
281
|
+
|
|
282
|
+
self._last_state_send = current_time
|
|
283
|
+
|
|
284
|
+
def run_main_cycle(self) -> None:
|
|
285
|
+
self._running = True
|
|
286
|
+
try:
|
|
287
|
+
while self._running:
|
|
288
|
+
self._base_mqtt_output_handler()
|
|
289
|
+
|
|
290
|
+
if self.mqtt_output_handler:
|
|
291
|
+
self.mqtt_output_handler(self)
|
|
292
|
+
|
|
293
|
+
time.sleep(self.cycle_speed)
|
|
294
|
+
|
|
295
|
+
except Exception as e:
|
|
296
|
+
self.logger.error(f"Error in main cycle: {str(e)}")
|
|
297
|
+
finally:
|
|
298
|
+
self._running = False
|
|
299
|
+
|
|
300
|
+
def set_output_handler(self, output_handler: Callable) -> None:
|
|
301
|
+
self.mqtt_output_handler = output_handler
|
|
302
|
+
|
|
303
|
+
def set_custom_update_handler(self, custom_update_handler: Callable) -> None:
|
|
304
|
+
self.custom_update_handler = custom_update_handler
|
|
305
|
+
|
|
306
|
+
def stop_main_cycle(self) -> None:
|
|
307
|
+
self.logger.info(f'Main cycle stopped')
|
|
308
|
+
self._running = False
|
pepeunit_client/enums.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LogLevel(Enum):
|
|
5
|
+
DEBUG = 'Debug'
|
|
6
|
+
INFO = 'Info'
|
|
7
|
+
WARNING = 'Warning'
|
|
8
|
+
ERROR = 'Error'
|
|
9
|
+
CRITICAL = 'Critical'
|
|
10
|
+
|
|
11
|
+
def get_int_level(self) -> int:
|
|
12
|
+
level_mapping = {
|
|
13
|
+
LogLevel.DEBUG: 0,
|
|
14
|
+
LogLevel.INFO: 1,
|
|
15
|
+
LogLevel.WARNING: 2,
|
|
16
|
+
LogLevel.ERROR: 3,
|
|
17
|
+
LogLevel.CRITICAL: 4,
|
|
18
|
+
}
|
|
19
|
+
return level_mapping[self]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SearchTopicType(Enum):
|
|
23
|
+
UNIT_NODE_UUID = 'unit_node_uuid'
|
|
24
|
+
FULL_NAME = 'full_name'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SearchScope(Enum):
|
|
28
|
+
ALL = 'all'
|
|
29
|
+
INPUT = 'input'
|
|
30
|
+
OUTPUT = 'output'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DestinationTopicType(Enum):
|
|
34
|
+
INPUT_BASE_TOPIC = 'input_base_topic'
|
|
35
|
+
OUTPUT_BASE_TOPIC = 'output_base_topic'
|
|
36
|
+
INPUT_TOPIC = 'input_topic'
|
|
37
|
+
OUTPUT_TOPIC = 'output_topic'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BaseInputTopicType(Enum):
|
|
41
|
+
UPDATE_PEPEUNIT = 'update/pepeunit'
|
|
42
|
+
ENV_UPDATE_PEPEUNIT = 'env_update/pepeunit'
|
|
43
|
+
SCHEMA_UPDATE_PEPEUNIT = 'schema_update/pepeunit'
|
|
44
|
+
LOG_SYNC_PEPEUNIT = 'log_sync/pepeunit'
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class BaseOutputTopicType(Enum):
|
|
48
|
+
LOG_PEPEUNIT = 'log/pepeunit'
|
|
49
|
+
STATE_PEPEUNIT = 'state/pepeunit'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RestartMode(Enum):
|
|
53
|
+
RESTART_POPEN = 'restart_popen'
|
|
54
|
+
RESTART_EXEC = 'restart_exec'
|
|
55
|
+
ENV_SCHEMA_ONLY = 'env_schema_only'
|
|
56
|
+
NO_RESTART = 'no_restart'
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import tarfile
|
|
5
|
+
import zlib
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FileManager:
|
|
11
|
+
@staticmethod
|
|
12
|
+
def read_json(file_path: str) -> Dict[str, Any]:
|
|
13
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
14
|
+
return json.load(f)
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def write_json(file_path: str, data: Dict[str, Any], indent: int = 4) -> None:
|
|
18
|
+
directory = os.path.dirname(file_path)
|
|
19
|
+
|
|
20
|
+
if directory:
|
|
21
|
+
os.makedirs(directory, exist_ok=True)
|
|
22
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
23
|
+
json.dump(data, f, indent=indent, ensure_ascii=False)
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def copy_file(source_path: str, destination_path: str) -> None:
|
|
27
|
+
shutil.copy2(source_path, destination_path)
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def file_exists(file_path: str) -> bool:
|
|
31
|
+
return os.path.exists(file_path)
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def create_directory(directory_path: str) -> None:
|
|
35
|
+
os.makedirs(directory_path, exist_ok=True)
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def extract_tar_gz(archive_path: str, extract_path: str) -> None:
|
|
39
|
+
FileManager.extract_pepeunit_archive(archive_path, extract_path)
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def extract_pepeunit_archive(file_path: str, extract_path: str) -> None:
|
|
43
|
+
with open(file_path, 'rb') as f:
|
|
44
|
+
producer = zlib.decompressobj(wbits=9)
|
|
45
|
+
tar_data = producer.decompress(f.read()) + producer.flush()
|
|
46
|
+
tar_filepath = f'{os.path.dirname(file_path)}/temp_update.tar'
|
|
47
|
+
with open(tar_filepath, 'wb') as tar_file:
|
|
48
|
+
tar_file.write(tar_data)
|
|
49
|
+
try:
|
|
50
|
+
# Use tarfile directly with filter for security (Python 3.12+)
|
|
51
|
+
with tarfile.open(tar_filepath, 'r') as tar:
|
|
52
|
+
# Check if filter parameter is supported
|
|
53
|
+
try:
|
|
54
|
+
tar.extractall(extract_path, filter='data')
|
|
55
|
+
except TypeError:
|
|
56
|
+
# Fallback for older Python versions
|
|
57
|
+
tar.extractall(extract_path)
|
|
58
|
+
finally:
|
|
59
|
+
if os.path.exists(tar_filepath):
|
|
60
|
+
os.remove(tar_filepath)
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def copy_directory_contents(source_path: str, destination_path: str) -> None:
|
|
64
|
+
if not os.path.exists(source_path):
|
|
65
|
+
raise FileNotFoundError(f"Source directory does not exist: {source_path}")
|
|
66
|
+
|
|
67
|
+
os.makedirs(destination_path, exist_ok=True)
|
|
68
|
+
|
|
69
|
+
for item in os.listdir(source_path):
|
|
70
|
+
source_item = os.path.join(source_path, item)
|
|
71
|
+
destination_item = os.path.join(destination_path, item)
|
|
72
|
+
|
|
73
|
+
if os.path.isdir(source_item):
|
|
74
|
+
shutil.copytree(source_item, destination_item, dirs_exist_ok=True)
|
|
75
|
+
else:
|
|
76
|
+
shutil.copy2(source_item, destination_item)
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def remove_directory(directory_path: str) -> None:
|
|
80
|
+
if os.path.exists(directory_path):
|
|
81
|
+
shutil.rmtree(directory_path)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def append_ndjson_with_limit(file_path: str, item: Dict[str, Any], max_lines: int = None) -> None:
|
|
85
|
+
directory = os.path.dirname(file_path)
|
|
86
|
+
if directory:
|
|
87
|
+
os.makedirs(directory, exist_ok=True)
|
|
88
|
+
|
|
89
|
+
if os.path.exists(file_path):
|
|
90
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
91
|
+
first_char = f.read(1)
|
|
92
|
+
if first_char == '[':
|
|
93
|
+
try:
|
|
94
|
+
f.seek(0)
|
|
95
|
+
data = json.load(f)
|
|
96
|
+
if isinstance(data, list):
|
|
97
|
+
with open(file_path, 'w', encoding='utf-8') as fw:
|
|
98
|
+
for it in data:
|
|
99
|
+
json.dump(it, fw, ensure_ascii=False)
|
|
100
|
+
fw.write('\n')
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
with open(file_path, 'a', encoding='utf-8') as f:
|
|
106
|
+
json.dump(item, f, ensure_ascii=False)
|
|
107
|
+
f.write('\n')
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
if max_lines is not None and max_lines > 0:
|
|
112
|
+
FileManager.trim_ndjson(file_path, max_lines)
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def iter_ndjson(file_path: str):
|
|
116
|
+
try:
|
|
117
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
118
|
+
for line in f:
|
|
119
|
+
line = line.strip()
|
|
120
|
+
if not line:
|
|
121
|
+
continue
|
|
122
|
+
try:
|
|
123
|
+
yield json.loads(line)
|
|
124
|
+
except Exception:
|
|
125
|
+
continue
|
|
126
|
+
except Exception:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def trim_ndjson(file_path: str, max_lines: int) -> None:
|
|
131
|
+
if max_lines <= 0:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
total = 0
|
|
136
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
137
|
+
for _ in f:
|
|
138
|
+
total += 1
|
|
139
|
+
|
|
140
|
+
if total <= max_lines:
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
to_skip = total - max_lines
|
|
144
|
+
tmp_path = file_path + '.tmp'
|
|
145
|
+
|
|
146
|
+
with open(file_path, 'r', encoding='utf-8') as src, open(tmp_path, 'w', encoding='utf-8') as dst:
|
|
147
|
+
for line in src:
|
|
148
|
+
if to_skip > 0:
|
|
149
|
+
to_skip -= 1
|
|
150
|
+
continue
|
|
151
|
+
dst.write(line)
|
|
152
|
+
|
|
153
|
+
os.replace(tmp_path, file_path)
|
|
154
|
+
except Exception:
|
|
155
|
+
if os.path.exists(tmp_path):
|
|
156
|
+
try:
|
|
157
|
+
os.remove(tmp_path)
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|