pyxecm 1.5__py3-none-any.whl → 2.0.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.
Potentially problematic release.
This version of pyxecm might be problematic. Click here for more details.
- pyxecm/__init__.py +6 -2
- pyxecm/avts.py +1492 -0
- pyxecm/coreshare.py +1075 -960
- pyxecm/customizer/__init__.py +16 -4
- pyxecm/customizer/__main__.py +58 -0
- pyxecm/customizer/api/__init__.py +5 -0
- pyxecm/customizer/api/__main__.py +6 -0
- pyxecm/customizer/api/app.py +914 -0
- pyxecm/customizer/api/auth.py +154 -0
- pyxecm/customizer/api/metrics.py +92 -0
- pyxecm/customizer/api/models.py +13 -0
- pyxecm/customizer/api/payload_list.py +865 -0
- pyxecm/customizer/api/settings.py +103 -0
- pyxecm/customizer/browser_automation.py +332 -139
- pyxecm/customizer/customizer.py +1075 -1057
- pyxecm/customizer/exceptions.py +35 -0
- pyxecm/customizer/guidewire.py +322 -0
- pyxecm/customizer/k8s.py +787 -338
- pyxecm/customizer/log.py +107 -0
- pyxecm/customizer/m365.py +3424 -2270
- pyxecm/customizer/nhc.py +1169 -0
- pyxecm/customizer/openapi.py +258 -0
- pyxecm/customizer/payload.py +18201 -7030
- pyxecm/customizer/pht.py +1047 -210
- pyxecm/customizer/salesforce.py +836 -727
- pyxecm/customizer/sap.py +58 -41
- pyxecm/customizer/servicenow.py +851 -383
- pyxecm/customizer/settings.py +442 -0
- pyxecm/customizer/successfactors.py +408 -346
- pyxecm/customizer/translate.py +83 -48
- pyxecm/helper/__init__.py +5 -2
- pyxecm/helper/assoc.py +98 -38
- pyxecm/helper/data.py +2482 -742
- pyxecm/helper/logadapter.py +27 -0
- pyxecm/helper/web.py +229 -101
- pyxecm/helper/xml.py +528 -172
- pyxecm/maintenance_page/__init__.py +5 -0
- pyxecm/maintenance_page/__main__.py +6 -0
- pyxecm/maintenance_page/app.py +51 -0
- pyxecm/maintenance_page/settings.py +28 -0
- pyxecm/maintenance_page/static/favicon.avif +0 -0
- pyxecm/maintenance_page/templates/maintenance.html +165 -0
- pyxecm/otac.py +234 -140
- pyxecm/otawp.py +2689 -0
- pyxecm/otcs.py +12344 -7547
- pyxecm/otds.py +3166 -2219
- pyxecm/otiv.py +36 -21
- pyxecm/otmm.py +1363 -296
- pyxecm/otpd.py +231 -127
- pyxecm-2.0.0.dist-info/METADATA +145 -0
- pyxecm-2.0.0.dist-info/RECORD +54 -0
- {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info}/WHEEL +1 -1
- pyxecm-1.5.dist-info/METADATA +0 -51
- pyxecm-1.5.dist-info/RECORD +0 -30
- {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info/licenses}/LICENSE +0 -0
- {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
"""Payload List Module to implement methods to maintain and process a list of payload files.
|
|
2
|
+
|
|
3
|
+
This code typically runs in a container as part of the cloud automation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__author__ = "Dr. Marc Diefenbruch"
|
|
7
|
+
__copyright__ = "Copyright (C) 2024-2025, OpenText"
|
|
8
|
+
__credits__ = ["Kai-Philip Gatzweiler"]
|
|
9
|
+
__maintainer__ = "Dr. Marc Diefenbruch"
|
|
10
|
+
__email__ = "mdiefenb@opentext.com"
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import pprint
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
import traceback
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
|
|
20
|
+
from pydantic import ValidationError
|
|
21
|
+
|
|
22
|
+
from pyxecm.customizer.api.settings import api_settings
|
|
23
|
+
|
|
24
|
+
# OpenText specific modules:
|
|
25
|
+
from pyxecm.customizer.customizer import Customizer
|
|
26
|
+
from pyxecm.customizer.exceptions import StopOnError
|
|
27
|
+
from pyxecm.customizer.log import LogCountFilter, VictoriaLogsHandler
|
|
28
|
+
from pyxecm.customizer.payload import load_payload
|
|
29
|
+
|
|
30
|
+
default_logger = logging.getLogger("pyxecm.customizer.payload_list")
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import pandas as pd
|
|
34
|
+
|
|
35
|
+
pandas_installed = True
|
|
36
|
+
except ModuleNotFoundError:
|
|
37
|
+
default_logger.warning(
|
|
38
|
+
"Module pandas is not installed. Customizer will not support bulk workspace creation.",
|
|
39
|
+
)
|
|
40
|
+
pandas_installed = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PayloadList:
|
|
44
|
+
"""Manage a sorted list of payload items using a pandas data frame.
|
|
45
|
+
|
|
46
|
+
Each payload item with metadata such as name, filename, dependency (referencing another item by index),
|
|
47
|
+
logfile, and status. Provides list-like functionality with additional methods
|
|
48
|
+
for adding, removing, and reordering items.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
logger: logging.Logger = default_logger
|
|
52
|
+
|
|
53
|
+
_stopped: bool = True
|
|
54
|
+
payload_items: pd.DataFrame
|
|
55
|
+
|
|
56
|
+
def __init__(self, logger: logging.Logger = default_logger) -> None:
|
|
57
|
+
"""Initialize the Payload List object.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
logger (logging.Logger, optional):
|
|
61
|
+
The logging object to use for all log messages. Defaults to default_logger.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
if logger != default_logger:
|
|
65
|
+
self.logger = logging.getLogger(f"{logger.name}.payload_list")
|
|
66
|
+
|
|
67
|
+
self.payload_items = pd.DataFrame(
|
|
68
|
+
columns=[
|
|
69
|
+
"name",
|
|
70
|
+
"filename",
|
|
71
|
+
"dependencies",
|
|
72
|
+
"logfile",
|
|
73
|
+
"status",
|
|
74
|
+
"enabled",
|
|
75
|
+
"git_url",
|
|
76
|
+
"loglevel",
|
|
77
|
+
"start_time",
|
|
78
|
+
"stop_time",
|
|
79
|
+
"duration",
|
|
80
|
+
"log_debug",
|
|
81
|
+
"log_info",
|
|
82
|
+
"log_warning",
|
|
83
|
+
"log_error",
|
|
84
|
+
"log_critical",
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# end method definition
|
|
89
|
+
|
|
90
|
+
def calculate_payload_item_duration(self) -> None:
|
|
91
|
+
"""Update the dataframe column "duration" for all running items."""
|
|
92
|
+
|
|
93
|
+
def calculate_duration(row: pd.Series) -> str:
|
|
94
|
+
if row["status"] == "running":
|
|
95
|
+
now = datetime.now(timezone.utc)
|
|
96
|
+
start_time = pd.to_datetime(row["start_time"])
|
|
97
|
+
|
|
98
|
+
duration = now - start_time
|
|
99
|
+
hours, remainder = divmod(duration.total_seconds(), 3600)
|
|
100
|
+
minutes, seconds = divmod(remainder, 60)
|
|
101
|
+
formatted_duration = f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}"
|
|
102
|
+
|
|
103
|
+
return formatted_duration
|
|
104
|
+
else:
|
|
105
|
+
return str(row["duration"]) # or whatever the original value should be
|
|
106
|
+
|
|
107
|
+
# updates the "duration" column of the DataFrame self.payload_items
|
|
108
|
+
# by applying the method calculate_duration() to each row:
|
|
109
|
+
self.payload_items["duration"] = self.payload_items.apply(
|
|
110
|
+
calculate_duration,
|
|
111
|
+
axis=1,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# end method definition
|
|
115
|
+
|
|
116
|
+
def get_payload_items(self) -> pd.DataFrame:
|
|
117
|
+
"""Get the payload items in their current order in the PayloadList.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
pd.DataFrame:
|
|
121
|
+
A data frame containing all items in their current order.
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
self.calculate_payload_item_duration()
|
|
126
|
+
|
|
127
|
+
return self.payload_items
|
|
128
|
+
|
|
129
|
+
# end method definition
|
|
130
|
+
|
|
131
|
+
def get_payload_item(self, index: int) -> pd.Series:
|
|
132
|
+
"""Get the payload item by index if it exists, otherwise return None.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
index (int): index of the row
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
pd.Series: row with the matching index or None if there is no row with that index
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
self.calculate_payload_item_duration()
|
|
143
|
+
|
|
144
|
+
if index not in self.payload_items.index:
|
|
145
|
+
self.logger.error("Index -> %s is out of range", str(index))
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
return self.payload_items.loc[index]
|
|
149
|
+
|
|
150
|
+
# end method definition
|
|
151
|
+
|
|
152
|
+
def get_payload_item_by_name(self, name: str) -> pd.Series:
|
|
153
|
+
"""Get the payload item by name if it exists, otherwise return None.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
name (str):
|
|
157
|
+
The name of the payload.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
pd.Series:
|
|
161
|
+
Row with the matching name or None if there is no row with that index
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
self.calculate_payload_item_duration()
|
|
166
|
+
|
|
167
|
+
df = self.get_payload_items()
|
|
168
|
+
data = [{"index": idx, **row} for idx, row in df.iterrows()]
|
|
169
|
+
|
|
170
|
+
return next((item for item in data if item.get("name") == name), None)
|
|
171
|
+
|
|
172
|
+
# end method definition
|
|
173
|
+
|
|
174
|
+
def get_payload_items_by_value(
|
|
175
|
+
self,
|
|
176
|
+
column: str,
|
|
177
|
+
value: str,
|
|
178
|
+
) -> pd.DataFrame | None:
|
|
179
|
+
"""Filter the PayloadList by a given value in a specific column.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
column (str):
|
|
183
|
+
The column to filter by.
|
|
184
|
+
value (str):
|
|
185
|
+
The value to match in the specified column.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
pd.DataFrame: A DataFrame containing rows where the given column matches the value.
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> payload_list = PayloadList()
|
|
192
|
+
>>> payload_list.add_item("Task1", "task1.txt", status="running")
|
|
193
|
+
>>> payload_list.add_item("Task2", "task2.txt", status="completed")
|
|
194
|
+
>>> payload_list.add_item("Task3", "task3.txt", status="running")
|
|
195
|
+
>>> payload_list.get_payload_items_by_value(column="status", value="running")
|
|
196
|
+
name file dependencies logfile status enabled
|
|
197
|
+
0 Task1 task1.txt NaN None running True
|
|
198
|
+
2 Task3 task3.txt NaN None running True
|
|
199
|
+
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
if column not in self.payload_items.columns:
|
|
203
|
+
self.logger.error(
|
|
204
|
+
"Column -> '%s' does not exist in the payload list!",
|
|
205
|
+
str(column),
|
|
206
|
+
)
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
filtered_items = self.payload_items[self.payload_items[column] == value]
|
|
210
|
+
|
|
211
|
+
return filtered_items
|
|
212
|
+
|
|
213
|
+
# end method definition
|
|
214
|
+
|
|
215
|
+
def add_payload_item(
|
|
216
|
+
self,
|
|
217
|
+
name: str,
|
|
218
|
+
filename: str,
|
|
219
|
+
logfile: str,
|
|
220
|
+
dependencies: list | None = None,
|
|
221
|
+
status: str = "pending",
|
|
222
|
+
enabled: bool = True,
|
|
223
|
+
git_url: str | None = None,
|
|
224
|
+
loglevel: str = "INFO",
|
|
225
|
+
) -> dict:
|
|
226
|
+
"""Add a new item to the PayloadList.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
name (str):
|
|
230
|
+
The name of the item.
|
|
231
|
+
filename (str):
|
|
232
|
+
The file associated with the item.
|
|
233
|
+
logfile (str):
|
|
234
|
+
Log file information for the item. Defaults to None.
|
|
235
|
+
dependencies (list):
|
|
236
|
+
The index of another item this item depends on. Defaults to None.
|
|
237
|
+
status (str):
|
|
238
|
+
The status of the item. Must be one of 'planned', 'running',
|
|
239
|
+
'completed', or 'failed'. Defaults to 'planned'.
|
|
240
|
+
enabled (bool):
|
|
241
|
+
True if the payload is enabled. False otherwise.
|
|
242
|
+
git_url (str):
|
|
243
|
+
Link to the payload in the GIT repository.
|
|
244
|
+
loglevel (str):
|
|
245
|
+
The log level for processing the payload. Either "INFO" or "DEBUG".
|
|
246
|
+
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
new_item = {
|
|
250
|
+
"name": name if name else filename,
|
|
251
|
+
"filename": filename,
|
|
252
|
+
"dependencies": dependencies if dependencies else [],
|
|
253
|
+
"logfile": logfile,
|
|
254
|
+
"status": status,
|
|
255
|
+
"enabled": enabled,
|
|
256
|
+
"git_url": git_url,
|
|
257
|
+
"loglevel": loglevel,
|
|
258
|
+
"log_debug": 0,
|
|
259
|
+
"log_info": 0,
|
|
260
|
+
"log_warning": 0,
|
|
261
|
+
"log_error": 0,
|
|
262
|
+
"log_critical": 0,
|
|
263
|
+
}
|
|
264
|
+
self.payload_items = pd.concat(
|
|
265
|
+
[self.payload_items, pd.DataFrame([new_item])],
|
|
266
|
+
ignore_index=True,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
new_item = self.payload_items.tail(1).to_dict(orient="records")[0]
|
|
270
|
+
new_item["index"] = self.payload_items.index[-1]
|
|
271
|
+
|
|
272
|
+
return new_item
|
|
273
|
+
|
|
274
|
+
# end method definition
|
|
275
|
+
|
|
276
|
+
def update_payload_item(
|
|
277
|
+
self,
|
|
278
|
+
index: int,
|
|
279
|
+
update_data: dict,
|
|
280
|
+
) -> bool:
|
|
281
|
+
"""Update an existing item in the PayloadList.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
index (int):
|
|
285
|
+
The position of the payload.
|
|
286
|
+
update_data (str):
|
|
287
|
+
The data of the item.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
bool:
|
|
291
|
+
True = success, False = error.
|
|
292
|
+
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
if index not in self.payload_items.index:
|
|
296
|
+
self.logger.error("Illegal index -> %s for payload update!", index)
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
for column in self.payload_items.columns:
|
|
300
|
+
if column in update_data:
|
|
301
|
+
tmp = self.payload_items.loc[index].astype(object)
|
|
302
|
+
tmp[column] = update_data[column]
|
|
303
|
+
self.payload_items.loc[index] = tmp
|
|
304
|
+
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
# end method definition
|
|
308
|
+
|
|
309
|
+
def remove_payload_item(self, index: int) -> bool:
|
|
310
|
+
"""Remove an item by its index from the PayloadList.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
index (int):
|
|
314
|
+
The index of the item to remove.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
bool:
|
|
318
|
+
True = success. False = failure.
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
IndexError: If the index is out of range.
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
if index not in self.payload_items.index:
|
|
326
|
+
self.logger.error("Index -> %s is out of range!", index)
|
|
327
|
+
return False
|
|
328
|
+
|
|
329
|
+
self.payload_items.drop(index, inplace=True)
|
|
330
|
+
|
|
331
|
+
return True
|
|
332
|
+
|
|
333
|
+
# end method definition
|
|
334
|
+
|
|
335
|
+
def move_payload_item_up(self, index: int) -> int | None:
|
|
336
|
+
"""Move an item up by one position in the PayloadList.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
index (int): The index of the item to move up.
|
|
340
|
+
|
|
341
|
+
Results:
|
|
342
|
+
bool: False, if the index is out of range or the item is already at the top.
|
|
343
|
+
True otherwise
|
|
344
|
+
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
if index <= 0 or index >= len(self.payload_items):
|
|
348
|
+
self.logger.error(
|
|
349
|
+
"Index -> %s is out of range or already at the top!",
|
|
350
|
+
str(index),
|
|
351
|
+
)
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
self.payload_items.iloc[[index - 1, index]] = self.payload_items.iloc[[index, index - 1]].to_numpy()
|
|
355
|
+
|
|
356
|
+
new_postion = self.payload_items.index.get_loc(index)
|
|
357
|
+
|
|
358
|
+
return new_postion
|
|
359
|
+
|
|
360
|
+
# end method definition
|
|
361
|
+
|
|
362
|
+
def move_payload_item_down(self, index: int) -> int | None:
|
|
363
|
+
"""Move an item down by one position in the PayloadList.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
index (int):
|
|
367
|
+
The index of the item to move down.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
int:
|
|
371
|
+
The new position of the payload item.
|
|
372
|
+
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
if index < 0 or index >= len(self.payload_items) - 1:
|
|
376
|
+
self.logger.error(
|
|
377
|
+
"Index -> %s is out of range or already at the bottom!",
|
|
378
|
+
str(index),
|
|
379
|
+
)
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
self.payload_items.iloc[[index, index + 1]] = self.payload_items.iloc[[index + 1, index]].to_numpy()
|
|
383
|
+
|
|
384
|
+
new_postion = self.payload_items.index.get_loc(index)
|
|
385
|
+
|
|
386
|
+
return new_postion
|
|
387
|
+
|
|
388
|
+
# end method definition
|
|
389
|
+
|
|
390
|
+
def __len__(self) -> int:
|
|
391
|
+
"""Return the number of items in the PayloadList.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
int:
|
|
395
|
+
The count of items in the list.
|
|
396
|
+
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
return len(self.payload_items)
|
|
400
|
+
|
|
401
|
+
# end method definition
|
|
402
|
+
|
|
403
|
+
def __getitem__(self, index: int) -> pd.Series:
|
|
404
|
+
"""Get an item by its index using the "[index]" syntax.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
index (int):
|
|
408
|
+
The index of the item to retrieve.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
pd.Series:
|
|
412
|
+
The item at the specified index as a Series.
|
|
413
|
+
|
|
414
|
+
Raises:
|
|
415
|
+
IndexError: If the index is out of range.
|
|
416
|
+
|
|
417
|
+
Example:
|
|
418
|
+
>>> payload_list = PayloadList()
|
|
419
|
+
>>> payload_list.add_item("Task1", "task1.txt")
|
|
420
|
+
>>> payload_list[0]
|
|
421
|
+
name Task1
|
|
422
|
+
file task1.txt
|
|
423
|
+
dependencies NaN
|
|
424
|
+
logfile None
|
|
425
|
+
status planned
|
|
426
|
+
Name: 0, dtype: object
|
|
427
|
+
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
if index not in self.payload_items.index:
|
|
431
|
+
exception = "Index -> {} is out of range".format(index)
|
|
432
|
+
raise IndexError(exception)
|
|
433
|
+
|
|
434
|
+
return self.payload_items.loc[index]
|
|
435
|
+
|
|
436
|
+
# end method definition
|
|
437
|
+
|
|
438
|
+
def __setitem__(self, index: int, value: dict) -> None:
|
|
439
|
+
"""Set an item at the specified index using the "[index]" syntax.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
index (int): The index to set the item at.
|
|
443
|
+
value (dict): The item dictionary to set, which must include 'name' and 'file' keys.
|
|
444
|
+
|
|
445
|
+
Raises:
|
|
446
|
+
IndexError: If the index is out of range.
|
|
447
|
+
ValueError: If the provided value is not a valid item dictionary.
|
|
448
|
+
|
|
449
|
+
Example:
|
|
450
|
+
>>> payload_list = PayloadList()
|
|
451
|
+
>>> payload_list.add_item("Task1", "task1.txt")
|
|
452
|
+
>>> payload_list[0]
|
|
453
|
+
name Task1
|
|
454
|
+
filename task1.txt
|
|
455
|
+
dependencies NaN
|
|
456
|
+
logfile None
|
|
457
|
+
status planned
|
|
458
|
+
Name: 0, dtype: object
|
|
459
|
+
>>> payload_list[0] = {"name": "Updated Task1", "file": "updated_task1.txt", "status": "completed"}
|
|
460
|
+
>>> payload_list[0]
|
|
461
|
+
name Updated Task1
|
|
462
|
+
filename updated_task1.txt
|
|
463
|
+
dependencies NaN
|
|
464
|
+
logfile None
|
|
465
|
+
status completed
|
|
466
|
+
Name: 0, dtype: object
|
|
467
|
+
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
if not {"name", "filename"}.issubset(value):
|
|
471
|
+
exception = ("Value must be a dictionary with at least 'name' and 'filename' keys",)
|
|
472
|
+
raise ValueError(
|
|
473
|
+
exception,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
if index not in self.payload_items.index:
|
|
477
|
+
exception = "Index -> {} is out of range".format(index)
|
|
478
|
+
raise IndexError(exception)
|
|
479
|
+
|
|
480
|
+
self.payload_items.loc[index] = value
|
|
481
|
+
|
|
482
|
+
# end method definition
|
|
483
|
+
|
|
484
|
+
def __delitem__(self, index: int) -> None:
|
|
485
|
+
"""Delete an item by its index.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
index (int): The index of the item to delete.
|
|
489
|
+
|
|
490
|
+
Raises:
|
|
491
|
+
IndexError: If the index is out of range.
|
|
492
|
+
|
|
493
|
+
"""
|
|
494
|
+
|
|
495
|
+
self.remove_item(index=index)
|
|
496
|
+
|
|
497
|
+
# end method definition
|
|
498
|
+
|
|
499
|
+
def __getattr__(self, attribute: str) -> pd.Series:
|
|
500
|
+
"""Provide dynamic access to columns using the "." syntax.
|
|
501
|
+
|
|
502
|
+
For example, `payload_list.name` will return the 'name' column values.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
attribute (str): The column name to retrieve.
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
pd.Series: The specified column as a pandas Series.
|
|
509
|
+
|
|
510
|
+
Example:
|
|
511
|
+
>>> payload_list = PayloadList()
|
|
512
|
+
>>> payload_list.add_item("Task1", "task1.txt")
|
|
513
|
+
>>> payload_list.name
|
|
514
|
+
0 Task1
|
|
515
|
+
Name: name, dtype: object
|
|
516
|
+
|
|
517
|
+
"""
|
|
518
|
+
|
|
519
|
+
if attribute in self.payload_items.columns:
|
|
520
|
+
return self.payload_items[attribute]
|
|
521
|
+
|
|
522
|
+
self.logger.error("Payload list has no attribute -> '%s'", attribute)
|
|
523
|
+
return None
|
|
524
|
+
|
|
525
|
+
# end method definition
|
|
526
|
+
|
|
527
|
+
def __repr__(self) -> str:
|
|
528
|
+
"""Return a string representation of the PayloadList for logging and debugging.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
str:
|
|
532
|
+
A string representing the items in the DataFrame.
|
|
533
|
+
|
|
534
|
+
"""
|
|
535
|
+
|
|
536
|
+
return self.payload_items.to_string(index=True)
|
|
537
|
+
|
|
538
|
+
# end method definition
|
|
539
|
+
|
|
540
|
+
def __iter__(self) -> iter:
|
|
541
|
+
"""Iterate over the rows of the PayloadList.
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
iterator: An iterator over the rows of the payload_items DataFrame.
|
|
545
|
+
|
|
546
|
+
Example:
|
|
547
|
+
>>> payload_list = PayloadList()
|
|
548
|
+
>>> payload_list.add_item("Task1", "task1.txt")
|
|
549
|
+
>>> payload_list.add_item("Task2", "task2.txt")
|
|
550
|
+
>>> for payload in payload_list:
|
|
551
|
+
>>> print(payload)
|
|
552
|
+
name Task1
|
|
553
|
+
filename task1.txt
|
|
554
|
+
dependencies NaN
|
|
555
|
+
logfile None
|
|
556
|
+
status planned
|
|
557
|
+
Name: 0, dtype: object
|
|
558
|
+
name Task2
|
|
559
|
+
file task2.txt
|
|
560
|
+
dependencies NaN
|
|
561
|
+
logfile None
|
|
562
|
+
status planned
|
|
563
|
+
Name: 1, dtype: object
|
|
564
|
+
|
|
565
|
+
"""
|
|
566
|
+
|
|
567
|
+
# Return an iterator for the rows of the DataFrame
|
|
568
|
+
for _, row in self.payload_items.iterrows():
|
|
569
|
+
yield row
|
|
570
|
+
|
|
571
|
+
# end method definition
|
|
572
|
+
|
|
573
|
+
def pick_runnables(self) -> pd.DataFrame:
|
|
574
|
+
"""Pick all PayloadItems with status "planned" and no dependencies on items that are not in status "completed".
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
pd.DataFrame:
|
|
578
|
+
A list of runnable payload items.
|
|
579
|
+
|
|
580
|
+
"""
|
|
581
|
+
|
|
582
|
+
def is_runnable(row: pd.Series) -> bool:
|
|
583
|
+
# Check if item is enabled:
|
|
584
|
+
if not row["enabled"]:
|
|
585
|
+
return False
|
|
586
|
+
|
|
587
|
+
# Check if all dependencies have been completed
|
|
588
|
+
dependencies: list[int] = row["dependencies"]
|
|
589
|
+
|
|
590
|
+
return all(self.payload_items.loc[dep, "status"] == "completed" for dep in dependencies or [])
|
|
591
|
+
|
|
592
|
+
# end sub-method definition
|
|
593
|
+
|
|
594
|
+
if self.payload_items.empty:
|
|
595
|
+
return None
|
|
596
|
+
|
|
597
|
+
# Filter payload items to find runnable items
|
|
598
|
+
runnable_df: pd.DataFrame = self.payload_items[
|
|
599
|
+
(self.payload_items["status"] == "planned") & self.payload_items.apply(is_runnable, axis=1)
|
|
600
|
+
].copy()
|
|
601
|
+
|
|
602
|
+
# Add index as a column to the resulting DataFrame
|
|
603
|
+
runnable_df["index"] = runnable_df.index
|
|
604
|
+
|
|
605
|
+
# Log each runnable item
|
|
606
|
+
for _, row in runnable_df.iterrows():
|
|
607
|
+
self.logger.info(
|
|
608
|
+
"Added payload file -> '%s' with index -> %s to runnable queue.",
|
|
609
|
+
row["name"] if row["name"] else row["filename"],
|
|
610
|
+
row["index"],
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
return runnable_df
|
|
614
|
+
|
|
615
|
+
# end method definition
|
|
616
|
+
|
|
617
|
+
def process_payload_list(self) -> None:
|
|
618
|
+
"""Process runnable payloads.
|
|
619
|
+
|
|
620
|
+
Continuously checks for runnable payload items and starts their
|
|
621
|
+
"process_payload" method in separate threads.
|
|
622
|
+
Runs as a daemon until the customizer ends.
|
|
623
|
+
"""
|
|
624
|
+
|
|
625
|
+
def run_and_complete_payload(payload_item: pd.Series) -> None:
|
|
626
|
+
"""Run the payload's process_payload method and marks the status as completed afterward."""
|
|
627
|
+
|
|
628
|
+
start_time = datetime.now(timezone.utc)
|
|
629
|
+
self.update_payload_item(payload_item["index"], {"start_time": start_time})
|
|
630
|
+
|
|
631
|
+
# Create a logger with thread_id:
|
|
632
|
+
thread_logger = logging.getLogger(
|
|
633
|
+
name="Payload_{}".format(payload_item["index"]),
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
thread_logger.setLevel(level=payload_item["loglevel"])
|
|
637
|
+
|
|
638
|
+
# Check if the logger already has handlers. If it does, they are removed before creating new ones.
|
|
639
|
+
if thread_logger.hasHandlers():
|
|
640
|
+
thread_logger.handlers.clear()
|
|
641
|
+
|
|
642
|
+
# Create a handler for the logger:
|
|
643
|
+
handler = logging.FileHandler(filename=payload_item.logfile)
|
|
644
|
+
|
|
645
|
+
# Create a formatter:
|
|
646
|
+
formatter = logging.Formatter(
|
|
647
|
+
fmt="%(asctime)s %(levelname)s [%(name)s] [%(threadName)s] %(message)s",
|
|
648
|
+
datefmt="%d-%b-%Y %H:%M:%S",
|
|
649
|
+
)
|
|
650
|
+
# Add the formatter to the handler
|
|
651
|
+
handler.setFormatter(fmt=formatter)
|
|
652
|
+
thread_logger.addHandler(hdlr=handler)
|
|
653
|
+
|
|
654
|
+
# If hostname is set, configure log handler so forward logs
|
|
655
|
+
if api_settings.victorialogs_host:
|
|
656
|
+
handler_kwargs = {
|
|
657
|
+
"host": api_settings.victorialogs_host,
|
|
658
|
+
"port": api_settings.victorialogs_port,
|
|
659
|
+
"app": "Customizer",
|
|
660
|
+
"payload_item": payload_item["index"],
|
|
661
|
+
"payload_file": payload_item["filename"],
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
# Read namespace if available and add as kwarg to loghandler
|
|
665
|
+
file_path = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
|
|
666
|
+
if os.path.isfile(file_path):
|
|
667
|
+
with open(file_path) as file:
|
|
668
|
+
handler_kwargs["namespace"] = file.read()
|
|
669
|
+
|
|
670
|
+
thread_logger.addHandler(VictoriaLogsHandler(**handler_kwargs))
|
|
671
|
+
|
|
672
|
+
if len(thread_logger.filters) == 0:
|
|
673
|
+
thread_logger.debug("Adding log count filter to logger")
|
|
674
|
+
thread_logger.addFilter(
|
|
675
|
+
LogCountFilter(
|
|
676
|
+
payload_items=self.payload_items,
|
|
677
|
+
index=payload_item["index"],
|
|
678
|
+
),
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
thread_logger.info(
|
|
682
|
+
"Start processing of payload -> '%s' (%s) from filename -> '%s'",
|
|
683
|
+
payload_item["name"],
|
|
684
|
+
payload_item["index"],
|
|
685
|
+
payload_item["filename"],
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
local = threading.local()
|
|
689
|
+
|
|
690
|
+
# Read customizer Settings from customizerSettings in the payload:
|
|
691
|
+
payload = load_payload(payload_item["filename"])
|
|
692
|
+
|
|
693
|
+
if not payload:
|
|
694
|
+
success = False
|
|
695
|
+
|
|
696
|
+
if payload:
|
|
697
|
+
customizer_settings = payload.get("customizerSettings", {})
|
|
698
|
+
|
|
699
|
+
# Overwrite the customizer settings with the payload specific ones:
|
|
700
|
+
customizer_settings.update(
|
|
701
|
+
{
|
|
702
|
+
"cust_payload": payload_item["filename"],
|
|
703
|
+
"cust_payload_gz": "",
|
|
704
|
+
"cust_payload_external": "",
|
|
705
|
+
"cust_log_file": payload_item.logfile,
|
|
706
|
+
},
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
try:
|
|
710
|
+
local.customizer_thread_object = Customizer(
|
|
711
|
+
settings=customizer_settings,
|
|
712
|
+
logger=thread_logger,
|
|
713
|
+
)
|
|
714
|
+
thread_logger.info("Customizer initialized successfully.")
|
|
715
|
+
|
|
716
|
+
thread_logger.debug(
|
|
717
|
+
"Customizer Settings -> \n %s",
|
|
718
|
+
pprint.pformat(
|
|
719
|
+
local.customizer_thread_object.settings.model_dump(),
|
|
720
|
+
),
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
if customizer_settings.get("profiling", False):
|
|
724
|
+
from pyinstrument import Profiler
|
|
725
|
+
|
|
726
|
+
profiler = Profiler()
|
|
727
|
+
profiler.start()
|
|
728
|
+
|
|
729
|
+
if customizer_settings.get("cprofiling", False):
|
|
730
|
+
import cProfile
|
|
731
|
+
import pstats
|
|
732
|
+
|
|
733
|
+
cprofiler = cProfile.Profile()
|
|
734
|
+
cprofiler.enable()
|
|
735
|
+
|
|
736
|
+
success = local.customizer_thread_object.customization_run()
|
|
737
|
+
|
|
738
|
+
if customizer_settings.get("cprofiling", False):
|
|
739
|
+
cprofiler.disable()
|
|
740
|
+
|
|
741
|
+
if customizer_settings.get("profiling", False):
|
|
742
|
+
profiler.stop()
|
|
743
|
+
|
|
744
|
+
now = datetime.now(timezone.utc)
|
|
745
|
+
log_path = os.path.dirname(payload_item.logfile)
|
|
746
|
+
profile_log_prefix = (
|
|
747
|
+
f"{log_path}/{payload_item['index']}_{payload_item['name']}_{now.strftime('%Y-%m-%d_%H-%M-%S')}"
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
if customizer_settings.get("cprofiling", False):
|
|
751
|
+
import io
|
|
752
|
+
|
|
753
|
+
s = io.StringIO()
|
|
754
|
+
stats = pstats.Stats(cprofiler, stream=s).sort_stats("cumtime")
|
|
755
|
+
stats.print_stats()
|
|
756
|
+
with open(f"{profile_log_prefix}.log", "w+") as f:
|
|
757
|
+
f.write(s.getvalue())
|
|
758
|
+
stats.dump_stats(filename=f"{profile_log_prefix}.cprof")
|
|
759
|
+
|
|
760
|
+
if customizer_settings.get("profiling", False):
|
|
761
|
+
with open(f"{profile_log_prefix}.html", "w") as f:
|
|
762
|
+
f.write(profiler.output_html())
|
|
763
|
+
|
|
764
|
+
except ValidationError:
|
|
765
|
+
thread_logger.error("Validation error!")
|
|
766
|
+
success = False
|
|
767
|
+
|
|
768
|
+
except StopOnError:
|
|
769
|
+
success = False
|
|
770
|
+
thread_logger.error(
|
|
771
|
+
"StopOnErrorException occurred. Stopping payload processing...",
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
except Exception:
|
|
775
|
+
success = False
|
|
776
|
+
thread_logger.error(
|
|
777
|
+
"An exception occurred: \n%s",
|
|
778
|
+
traceback.format_exc(),
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
if not success:
|
|
782
|
+
thread_logger.error(
|
|
783
|
+
"Failed to initialize payload -> '%s'!",
|
|
784
|
+
payload_item["filename"],
|
|
785
|
+
)
|
|
786
|
+
# Update the status to "failed" in the DataFrame after processing finishes
|
|
787
|
+
self.update_payload_item(payload_item["index"], {"status": "failed"})
|
|
788
|
+
|
|
789
|
+
else:
|
|
790
|
+
# Update the status to "completed" in the DataFrame after processing finishes
|
|
791
|
+
self.update_payload_item(payload_item["index"], {"status": "completed"})
|
|
792
|
+
|
|
793
|
+
stop_time = datetime.now(timezone.utc)
|
|
794
|
+
duration = stop_time - start_time
|
|
795
|
+
|
|
796
|
+
# Format duration in hh:mm:ss
|
|
797
|
+
hours, remainder = divmod(duration.total_seconds(), 3600)
|
|
798
|
+
minutes, seconds = divmod(remainder, 60)
|
|
799
|
+
formatted_duration = f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}"
|
|
800
|
+
|
|
801
|
+
self.update_payload_item(
|
|
802
|
+
payload_item["index"],
|
|
803
|
+
{"stop_time": stop_time, "duration": formatted_duration},
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
# end def run_and_complete_payload()
|
|
807
|
+
|
|
808
|
+
while not self._stopped:
|
|
809
|
+
# Get runnable items as subset of the initial data frame:
|
|
810
|
+
runnable_items: pd.DataFrame = self.pick_runnables()
|
|
811
|
+
|
|
812
|
+
# Start a thread for each runnable item (item is a pd.Series)
|
|
813
|
+
if runnable_items is not None:
|
|
814
|
+
for _, item in runnable_items.iterrows():
|
|
815
|
+
# Update the status to "running" in the data frame to prevent re-processing
|
|
816
|
+
self.payload_items.loc[
|
|
817
|
+
self.payload_items["name"] == item["name"],
|
|
818
|
+
"status",
|
|
819
|
+
] = "running"
|
|
820
|
+
|
|
821
|
+
# Start the process_payload method in a new thread
|
|
822
|
+
thread = threading.Thread(
|
|
823
|
+
target=run_and_complete_payload,
|
|
824
|
+
args=(item,),
|
|
825
|
+
name=item["name"],
|
|
826
|
+
)
|
|
827
|
+
thread.start()
|
|
828
|
+
break
|
|
829
|
+
|
|
830
|
+
# Sleep briefly to avoid a busy wait loop
|
|
831
|
+
time.sleep(1)
|
|
832
|
+
|
|
833
|
+
# end method definition
|
|
834
|
+
|
|
835
|
+
def run_payload_processing(self) -> None:
|
|
836
|
+
"""Start the `process_payload_list` method in a daemon thread."""
|
|
837
|
+
|
|
838
|
+
scheduler_thread = threading.Thread(
|
|
839
|
+
target=self.process_payload_list,
|
|
840
|
+
daemon=True,
|
|
841
|
+
name="Scheduler",
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
self.logger.info(
|
|
845
|
+
"Starting '%s' thread for payload list processing...",
|
|
846
|
+
str(scheduler_thread.name),
|
|
847
|
+
)
|
|
848
|
+
self._stopped = False
|
|
849
|
+
scheduler_thread.start()
|
|
850
|
+
|
|
851
|
+
self.logger.info(
|
|
852
|
+
"Waiting for thread -> '%s' to complete...",
|
|
853
|
+
str(scheduler_thread.name),
|
|
854
|
+
)
|
|
855
|
+
scheduler_thread.join()
|
|
856
|
+
self.logger.info("Thread -> '%s' has completed.", str(scheduler_thread.name))
|
|
857
|
+
|
|
858
|
+
# end method definition
|
|
859
|
+
|
|
860
|
+
def stop_payload_processing(self) -> None:
|
|
861
|
+
"""Set a stop flag which triggers the stopping of further payload processing."""
|
|
862
|
+
|
|
863
|
+
self._stopped = True
|
|
864
|
+
|
|
865
|
+
# end method definition
|