pyxecm 1.6__py3-none-any.whl → 2.0.1__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.

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