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.

Files changed (56) hide show
  1. pyxecm/__init__.py +6 -2
  2. pyxecm/avts.py +1492 -0
  3. pyxecm/coreshare.py +1075 -960
  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 +914 -0
  9. pyxecm/customizer/api/auth.py +154 -0
  10. pyxecm/customizer/api/metrics.py +92 -0
  11. pyxecm/customizer/api/models.py +13 -0
  12. pyxecm/customizer/api/payload_list.py +865 -0
  13. pyxecm/customizer/api/settings.py +103 -0
  14. pyxecm/customizer/browser_automation.py +332 -139
  15. pyxecm/customizer/customizer.py +1075 -1057
  16. pyxecm/customizer/exceptions.py +35 -0
  17. pyxecm/customizer/guidewire.py +322 -0
  18. pyxecm/customizer/k8s.py +787 -338
  19. pyxecm/customizer/log.py +107 -0
  20. pyxecm/customizer/m365.py +3424 -2270
  21. pyxecm/customizer/nhc.py +1169 -0
  22. pyxecm/customizer/openapi.py +258 -0
  23. pyxecm/customizer/payload.py +18201 -7030
  24. pyxecm/customizer/pht.py +1047 -210
  25. pyxecm/customizer/salesforce.py +836 -727
  26. pyxecm/customizer/sap.py +58 -41
  27. pyxecm/customizer/servicenow.py +851 -383
  28. pyxecm/customizer/settings.py +442 -0
  29. pyxecm/customizer/successfactors.py +408 -346
  30. pyxecm/customizer/translate.py +83 -48
  31. pyxecm/helper/__init__.py +5 -2
  32. pyxecm/helper/assoc.py +98 -38
  33. pyxecm/helper/data.py +2482 -742
  34. pyxecm/helper/logadapter.py +27 -0
  35. pyxecm/helper/web.py +229 -101
  36. pyxecm/helper/xml.py +528 -172
  37. pyxecm/maintenance_page/__init__.py +5 -0
  38. pyxecm/maintenance_page/__main__.py +6 -0
  39. pyxecm/maintenance_page/app.py +51 -0
  40. pyxecm/maintenance_page/settings.py +28 -0
  41. pyxecm/maintenance_page/static/favicon.avif +0 -0
  42. pyxecm/maintenance_page/templates/maintenance.html +165 -0
  43. pyxecm/otac.py +234 -140
  44. pyxecm/otawp.py +2689 -0
  45. pyxecm/otcs.py +12344 -7547
  46. pyxecm/otds.py +3166 -2219
  47. pyxecm/otiv.py +36 -21
  48. pyxecm/otmm.py +1363 -296
  49. pyxecm/otpd.py +231 -127
  50. pyxecm-2.0.0.dist-info/METADATA +145 -0
  51. pyxecm-2.0.0.dist-info/RECORD +54 -0
  52. {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info}/WHEEL +1 -1
  53. pyxecm-1.5.dist-info/METADATA +0 -51
  54. pyxecm-1.5.dist-info/RECORD +0 -30
  55. {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info/licenses}/LICENSE +0 -0
  56. {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