pyxecm 2.0.0__py3-none-any.whl → 2.0.2__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 (50) hide show
  1. pyxecm/__init__.py +2 -1
  2. pyxecm/avts.py +79 -33
  3. pyxecm/customizer/api/app.py +45 -796
  4. pyxecm/customizer/api/auth/__init__.py +1 -0
  5. pyxecm/customizer/api/{auth.py → auth/functions.py} +2 -64
  6. pyxecm/customizer/api/auth/router.py +78 -0
  7. pyxecm/customizer/api/common/__init__.py +1 -0
  8. pyxecm/customizer/api/common/functions.py +47 -0
  9. pyxecm/customizer/api/{metrics.py → common/metrics.py} +1 -1
  10. pyxecm/customizer/api/common/models.py +21 -0
  11. pyxecm/customizer/api/{payload_list.py → common/payload_list.py} +6 -1
  12. pyxecm/customizer/api/common/router.py +72 -0
  13. pyxecm/customizer/api/settings.py +25 -0
  14. pyxecm/customizer/api/terminal/__init__.py +1 -0
  15. pyxecm/customizer/api/terminal/router.py +87 -0
  16. pyxecm/customizer/api/v1_csai/__init__.py +1 -0
  17. pyxecm/customizer/api/v1_csai/router.py +87 -0
  18. pyxecm/customizer/api/v1_maintenance/__init__.py +1 -0
  19. pyxecm/customizer/api/v1_maintenance/functions.py +100 -0
  20. pyxecm/customizer/api/v1_maintenance/models.py +12 -0
  21. pyxecm/customizer/api/v1_maintenance/router.py +76 -0
  22. pyxecm/customizer/api/v1_otcs/__init__.py +1 -0
  23. pyxecm/customizer/api/v1_otcs/functions.py +61 -0
  24. pyxecm/customizer/api/v1_otcs/router.py +179 -0
  25. pyxecm/customizer/api/v1_payload/__init__.py +1 -0
  26. pyxecm/customizer/api/v1_payload/functions.py +179 -0
  27. pyxecm/customizer/api/v1_payload/models.py +51 -0
  28. pyxecm/customizer/api/v1_payload/router.py +499 -0
  29. pyxecm/customizer/browser_automation.py +567 -324
  30. pyxecm/customizer/customizer.py +204 -430
  31. pyxecm/customizer/guidewire.py +907 -43
  32. pyxecm/customizer/k8s.py +243 -56
  33. pyxecm/customizer/m365.py +104 -15
  34. pyxecm/customizer/payload.py +1943 -885
  35. pyxecm/customizer/pht.py +19 -2
  36. pyxecm/customizer/servicenow.py +22 -5
  37. pyxecm/customizer/settings.py +9 -6
  38. pyxecm/helper/xml.py +69 -0
  39. pyxecm/otac.py +1 -1
  40. pyxecm/otawp.py +2104 -1535
  41. pyxecm/otca.py +569 -0
  42. pyxecm/otcs.py +202 -38
  43. pyxecm/otds.py +35 -13
  44. {pyxecm-2.0.0.dist-info → pyxecm-2.0.2.dist-info}/METADATA +6 -32
  45. pyxecm-2.0.2.dist-info/RECORD +76 -0
  46. {pyxecm-2.0.0.dist-info → pyxecm-2.0.2.dist-info}/WHEEL +1 -1
  47. pyxecm-2.0.0.dist-info/RECORD +0 -54
  48. /pyxecm/customizer/api/{models.py → auth/models.py} +0 -0
  49. {pyxecm-2.0.0.dist-info → pyxecm-2.0.2.dist-info}/licenses/LICENSE +0 -0
  50. {pyxecm-2.0.0.dist-info → pyxecm-2.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,12 @@
1
+ """Define Models for the Maintenance Page Config."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class MaintenanceModel(BaseModel):
7
+ """Status object of the Maintenance Page."""
8
+
9
+ enabled: bool
10
+ title: str | None = ""
11
+ text: str | None = ""
12
+ footer: str | None = ""
@@ -0,0 +1,76 @@
1
+ """Define router for v1_maintenance."""
2
+
3
+ import logging
4
+ from typing import Annotated
5
+
6
+ from fastapi import APIRouter, Depends, Form
7
+
8
+ from pyxecm.customizer.api.auth import models
9
+ from pyxecm.customizer.api.auth.functions import get_authorized_user
10
+ from pyxecm.customizer.api.common.functions import get_k8s_object
11
+ from pyxecm.customizer.api.v1_maintenance.functions import get_maintenance_mode_status, set_maintenance_mode_via_ingress
12
+ from pyxecm.customizer.api.v1_maintenance.models import MaintenanceModel
13
+ from pyxecm.customizer.k8s import K8s
14
+ from pyxecm.maintenance_page.settings import settings as maint_settings
15
+
16
+ router = APIRouter(prefix="/api/v1/maintenance", tags=["maintenance"])
17
+
18
+ logger = logging.getLogger("pyxecm.customizer.api.v1_maintenance")
19
+
20
+
21
+ @router.get(path="")
22
+ async def status(
23
+ user: Annotated[models.User, Depends(get_authorized_user)], # noqa: ARG001
24
+ k8s_object: Annotated[K8s, Depends(get_k8s_object)],
25
+ ) -> MaintenanceModel:
26
+ """Return status of maintenance mode.
27
+
28
+ Args:
29
+ user (models.User):
30
+ Added to enforce authentication requirement
31
+ k8s_object (K8s):
32
+ K8s object instance of pyxecm K8s class
33
+
34
+
35
+ Returns:
36
+ dict:
37
+ Details of maintenance mode.
38
+
39
+ """
40
+
41
+ return get_maintenance_mode_status(k8s_object)
42
+
43
+
44
+ @router.post(path="")
45
+ async def set_maintenance_mode_options(
46
+ user: Annotated[models.User, Depends(get_authorized_user)], # noqa: ARG001
47
+ k8s_object: Annotated[K8s, Depends(get_k8s_object)],
48
+ config: Annotated[MaintenanceModel, Form()],
49
+ ) -> MaintenanceModel:
50
+ """Configure the Maintenance Mode and set options.
51
+
52
+ Args:
53
+ user (models.User):
54
+ Added to enforce authentication requirement
55
+ k8s_object (K8s):
56
+ K8s object instance of pyxecm K8s class
57
+ config (MaintenanceModel):
58
+ instance of the Maintenance Model
59
+
60
+ Returns:
61
+ dict: _description_
62
+
63
+ """
64
+ # Enable / Disable the acutual Maintenance Mode
65
+ set_maintenance_mode_via_ingress(config.enabled, k8s_object)
66
+
67
+ if config.title:
68
+ maint_settings.title = config.title
69
+
70
+ if config.text:
71
+ maint_settings.text = config.text
72
+
73
+ if config.footer:
74
+ maint_settings.footer = config.footer
75
+
76
+ return get_maintenance_mode_status(k8s_object)
@@ -0,0 +1 @@
1
+ """init module."""
@@ -0,0 +1,61 @@
1
+ """Define functions for v1_otcs."""
2
+
3
+ import logging
4
+ from datetime import datetime, timezone
5
+ from threading import Lock
6
+
7
+ from fastapi import APIRouter
8
+
9
+ from pyxecm.customizer.api.settings import CustomizerAPISettings
10
+ from pyxecm.customizer.k8s import K8s
11
+
12
+ router = APIRouter(prefix="/api/v1/otcs", tags=["otcs"])
13
+
14
+ logger = logging.getLogger("pyxecm.customizer.api.v1_otcs")
15
+
16
+
17
+ def collect_otcs_logs(host: str, k8s_object: K8s, logs_lock: Lock, settings: CustomizerAPISettings) -> None:
18
+ """Collect the logs for the given OTCS instance."""
19
+
20
+ with logs_lock:
21
+ timestamp = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d_%H-%M")
22
+ tgz_file = f"/tmp/{timestamp}_{host}.tar.gz" # noqa: S108
23
+
24
+ if host.startswith("otcs-frontend"):
25
+ container = "otcs-frontend-container"
26
+ elif host.startswith("otcs-backend-search"):
27
+ container = "otcs-backend-search-container"
28
+ elif host.startswith("otcs-admin"):
29
+ container = "otcs-admin-container"
30
+ else:
31
+ container = None
32
+
33
+ logger.info("Collecting logs for %s", host)
34
+ k8s_object.exec_pod_command(
35
+ pod_name=host,
36
+ command=["tar", "-czvf", tgz_file, "/opt/opentext/cs/logs", "/opt/opentext/cs_persist/contentserver.log"],
37
+ container=container,
38
+ timeout=1800,
39
+ )
40
+
41
+ logger.info("Uploading logs for %s", host)
42
+ k8s_object.exec_pod_command(
43
+ pod_name=host,
44
+ command=[
45
+ "curl",
46
+ "-X",
47
+ "POST",
48
+ "-F",
49
+ f"file=@{tgz_file}",
50
+ f"{settings.upload_url}?key={settings.upload_key}",
51
+ ],
52
+ container=container,
53
+ timeout=1800,
54
+ )
55
+
56
+ logger.info("Cleanup logs for %s", host)
57
+ k8s_object.exec_pod_command(
58
+ pod_name=host,
59
+ command=["rm", tgz_file],
60
+ container=container,
61
+ )
@@ -0,0 +1,179 @@
1
+ """Define router for v1_otcs."""
2
+
3
+ import logging
4
+ import os
5
+ import shutil
6
+ from http import HTTPStatus
7
+ from threading import Lock, Thread
8
+ from typing import Annotated
9
+
10
+ import anyio
11
+ from fastapi import APIRouter, Body, Depends, File, HTTPException, UploadFile
12
+ from fastapi.responses import FileResponse, JSONResponse
13
+
14
+ from pyxecm.customizer.api.auth.functions import get_authorized_user
15
+ from pyxecm.customizer.api.auth.models import User
16
+ from pyxecm.customizer.api.common.functions import get_k8s_object, get_otcs_logs_lock, get_settings
17
+ from pyxecm.customizer.api.settings import CustomizerAPISettings
18
+ from pyxecm.customizer.api.v1_otcs.functions import collect_otcs_logs
19
+ from pyxecm.customizer.k8s import K8s
20
+
21
+ router = APIRouter(prefix="/api/v1/otcs", tags=["otcs"])
22
+
23
+ logger = logging.getLogger("pyxecm.customizer.api.v1_otcs")
24
+
25
+
26
+ @router.put(path="/logs", tags=["otcs"])
27
+ async def put_otcs_logs(
28
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
29
+ k8s_object: Annotated[K8s, Depends(get_k8s_object)],
30
+ settings: Annotated[CustomizerAPISettings, Depends(get_settings)],
31
+ otcs_logs_lock: Annotated[dict[str, Lock], Depends(get_otcs_logs_lock)],
32
+ hosts: Annotated[list[str], Body()],
33
+ ) -> JSONResponse:
34
+ """Collect the logs from the given OTCS instances."""
35
+
36
+ if "all" in hosts:
37
+ hosts = []
38
+ for sts in ["otcs-admin", "otcs-frontend", "otcs-backend-search"]:
39
+ try:
40
+ sts_replicas = k8s_object.get_stateful_set_scale(sts).status.replicas
41
+ hosts.extend([f"{sts}-{i}" for i in range(sts_replicas)])
42
+ except Exception as e:
43
+ raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR) from e
44
+
45
+ msg = {}
46
+ for host in hosts:
47
+ if host not in otcs_logs_lock:
48
+ otcs_logs_lock[host] = Lock()
49
+
50
+ if not otcs_logs_lock[host].locked():
51
+ Thread(target=collect_otcs_logs, args=(host, k8s_object, otcs_logs_lock[host], settings)).start()
52
+ msg[host] = {"status": "ok", "message": "Logs are being collected"}
53
+ else:
54
+ msg[host] = {"status": "error", "message": "Logs are already being collected"}
55
+
56
+ status = (
57
+ HTTPStatus.TOO_MANY_REQUESTS if any(msg[host]["status"] == "error" for host in msg) else HTTPStatus.ACCEPTED
58
+ )
59
+ return JSONResponse(msg, status_code=status)
60
+
61
+
62
+ @router.post("/logs/upload", tags=["otcs"], include_in_schema=True)
63
+ async def post_otcs_log_file(
64
+ file: Annotated[UploadFile, File(...)],
65
+ settings: Annotated[CustomizerAPISettings, Depends(get_settings)],
66
+ key: str = "",
67
+ ) -> JSONResponse:
68
+ """Upload a file to disk.
69
+
70
+ Args:
71
+ file: File to be uploaded.
72
+ settings: CustomizerAPISettings.
73
+ key: Key to validate the upload.
74
+
75
+ Returns:
76
+ JSONResponse: Status of the upload
77
+
78
+ """
79
+ if key != settings.upload_key:
80
+ raise HTTPException(status_code=403, detail="Invalid Uploadkey")
81
+
82
+ os.makedirs(settings.upload_folder, exist_ok=True)
83
+
84
+ try:
85
+ async with await anyio.open_file(os.path.join(settings.upload_folder, file.filename), "wb") as f:
86
+ # Process the file in chunks instead of loading the entire file into memory
87
+ while True:
88
+ chunk = await file.read(65536) # Read 64KB at a time
89
+ if not chunk:
90
+ break
91
+ await f.write(chunk)
92
+ except Exception as e:
93
+ raise HTTPException(status_code=500, detail="Something went wrong") from e
94
+ finally:
95
+ await file.close()
96
+
97
+ return {"message": f"Successfully uploaded {file.filename}"}
98
+
99
+
100
+ @router.get("/logs", tags=["otcs"])
101
+ async def get_otcs_log_files(
102
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
103
+ settings: Annotated[CustomizerAPISettings, Depends(get_settings)],
104
+ k8s_object: Annotated[K8s, Depends(get_k8s_object)],
105
+ otcs_logs_lock: Annotated[dict[str, Lock], Depends(get_otcs_logs_lock)],
106
+ ) -> JSONResponse:
107
+ """List all otcs logs that can be downloaded."""
108
+
109
+ os.makedirs(settings.upload_folder, exist_ok=True)
110
+
111
+ files = []
112
+ for filename in sorted(os.listdir(settings.upload_folder)):
113
+ file_path = os.path.join(settings.upload_folder, filename)
114
+ if os.path.isfile(file_path):
115
+ file_size = os.path.getsize(file_path)
116
+ files.append({"filename": filename, "size": file_size})
117
+
118
+ response = {"status": {host: bool(otcs_logs_lock[host].locked()) for host in otcs_logs_lock}, "files": files}
119
+
120
+ # Extend response with all hosts
121
+ for sts in ["otcs-admin", "otcs-frontend", "otcs-backend-search"]:
122
+ try:
123
+ sts_replicas = k8s_object.get_stateful_set_scale(sts).status.replicas
124
+ for i in range(sts_replicas):
125
+ host = f"{sts}-{i}"
126
+
127
+ if host in otcs_logs_lock:
128
+ response["status"][host] = otcs_logs_lock[host].locked()
129
+ else:
130
+ response["status"][host] = False
131
+
132
+ except Exception as e:
133
+ raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR) from e
134
+
135
+ return JSONResponse(response, status_code=HTTPStatus.OK)
136
+
137
+
138
+ @router.delete("/logs", tags=["otcs"])
139
+ async def delete_otcs_log_files(
140
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
141
+ settings: Annotated[CustomizerAPISettings, Depends(get_settings)],
142
+ ) -> JSONResponse:
143
+ """Delete all otcs log files."""
144
+ shutil.rmtree(settings.upload_folder)
145
+ return JSONResponse({"message": "Successfully deleted all files"}, status_code=HTTPStatus.OK)
146
+
147
+
148
+ @router.delete("/logs/{file_name}", tags=["otcs"])
149
+ async def delete_otcs_log_file(
150
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
151
+ settings: Annotated[CustomizerAPISettings, Depends(get_settings)],
152
+ file_name: str,
153
+ ) -> FileResponse:
154
+ """Delete single OTCS log archive."""
155
+ file_path = os.path.join(settings.upload_folder, file_name)
156
+
157
+ if not os.path.exists(file_path):
158
+ raise HTTPException(status_code=404, detail="File not found")
159
+
160
+ try:
161
+ os.remove(file_path)
162
+ except Exception as e:
163
+ raise HTTPException(status_code=500, detail=f"{e}") from e
164
+
165
+ return JSONResponse({"message": f"Successfully deleted {file_name}"}, status_code=HTTPStatus.OK)
166
+
167
+
168
+ @router.get("/logs/{file_name}", tags=["otcs"])
169
+ async def get_otcs_log_file(
170
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
171
+ settings: Annotated[CustomizerAPISettings, Depends(get_settings)],
172
+ file_name: str,
173
+ ) -> FileResponse:
174
+ """Download OTCS log archive."""
175
+ file_path = os.path.join(settings.upload_folder, file_name)
176
+ if not os.path.exists(file_path):
177
+ raise HTTPException(status_code=404, detail="File not found")
178
+
179
+ return FileResponse(file_path, media_type="application/octet-stream", filename=file_name)
@@ -0,0 +1 @@
1
+ """init module."""
@@ -0,0 +1,179 @@
1
+ """API Implemenation for the Customizer to start and control the payload processing."""
2
+
3
+ __author__ = "Dr. Marc Diefenbruch"
4
+ __copyright__ = "Copyright (C) 2024-2025, OpenText"
5
+ __credits__ = ["Kai-Philip Gatzweiler"]
6
+ __maintainer__ = "Dr. Marc Diefenbruch"
7
+ __email__ = "mdiefenb@opentext.com"
8
+
9
+ import asyncio
10
+ import logging
11
+ import os
12
+ from collections.abc import AsyncGenerator
13
+
14
+ import anyio
15
+
16
+ from pyxecm.customizer.api.common.functions import PAYLOAD_LIST
17
+ from pyxecm.customizer.api.settings import api_settings
18
+ from pyxecm.customizer.exceptions import PayloadImportError
19
+ from pyxecm.customizer.payload import load_payload
20
+
21
+ logger = logging.getLogger("pyxecm.customizer.api.v1_payload")
22
+
23
+ # Initialize the globel Payloadlist object
24
+
25
+
26
+ def import_payload(
27
+ payload: str | None = None,
28
+ payload_dir: str | None = None,
29
+ enabled: bool | None = None,
30
+ dependencies: bool | None = None,
31
+ ) -> None:
32
+ """Automatically load payload items from disk of a given directory.
33
+
34
+ Args:
35
+ payload (str):
36
+ The name of the payload.
37
+ payload_dir (str):
38
+ The local path.
39
+ enabled (bool, optional):
40
+ Automatically start the processing (True), or only define items (False).
41
+ Defaults to False.
42
+ dependencies (bool, optional):
43
+ Automatically add dependency on the last payload in the queue
44
+
45
+ """
46
+
47
+ def import_payload_file(
48
+ filename: str,
49
+ enabled: bool | None,
50
+ dependencies: bool | None,
51
+ ) -> None:
52
+ if not os.path.isfile(filename):
53
+ return
54
+
55
+ if not (filename.endswith((".yaml", ".tfvars", ".tf", ".yml.gz.b64"))):
56
+ logger.debug("Skipping file: %s", filename)
57
+ return
58
+
59
+ # Load payload file
60
+ payload_content = load_payload(filename)
61
+ if payload_content is None:
62
+ exception = f"The import of payload -> {filename} failed. Payload content could not be loaded."
63
+ raise PayloadImportError(exception)
64
+
65
+ payload_options = payload_content.get("payloadOptions", {})
66
+
67
+ if enabled is None:
68
+ enabled = payload_options.get("enabled", True)
69
+
70
+ # read name from options section if specified, otherwise take filename
71
+ name = payload_options.get("name", os.path.basename(filename))
72
+
73
+ # Get the loglevel from payloadOptions if set, otherwise use the default loglevel
74
+ loglevel = payload_options.get("loglevel", api_settings.loglevel)
75
+
76
+ # Get the git_url
77
+ git_url = payload_options.get("git_url", None)
78
+
79
+ # Dependency Management
80
+ if dependencies is None:
81
+ dependencies = []
82
+
83
+ # Get all dependencies from payloadOptions and resolve their ID
84
+ for dependency_name in payload_options.get("dependencies", []):
85
+ dependend_item = PAYLOAD_LIST.get_payload_item_by_name(dependency_name)
86
+
87
+ if dependend_item is None:
88
+ exception = (
89
+ f"The import of payload -> {name} failed. Dependencies cannot be resovled: {dependency_name}",
90
+ )
91
+ raise PayloadImportError(
92
+ exception,
93
+ )
94
+ # Add the ID to the list of dependencies
95
+ dependencies.append(dependend_item["index"])
96
+
97
+ elif dependencies:
98
+ try:
99
+ payload_items = len(PAYLOAD_LIST.get_payload_items()) - 1
100
+ dependencies = [payload_items] if payload_items != -1 else []
101
+ except Exception:
102
+ dependencies = []
103
+ else:
104
+ dependencies = []
105
+
106
+ customizer_settings = payload_content.get("customizerSettings", {})
107
+
108
+ logger.info("Adding payload: %s", filename)
109
+ payload = PAYLOAD_LIST.add_payload_item(
110
+ name=name,
111
+ filename=filename,
112
+ status="planned",
113
+ logfile=f"{api_settings.logfolder}/{name}.log",
114
+ dependencies=dependencies,
115
+ enabled=enabled,
116
+ git_url=git_url,
117
+ loglevel=loglevel,
118
+ customizer_settings=customizer_settings,
119
+ )
120
+ dependencies = payload["index"]
121
+
122
+ return
123
+
124
+ if payload is None and payload_dir is None:
125
+ exception = "No payload or payload_dir provided"
126
+ raise ValueError(exception)
127
+
128
+ if payload and os.path.isdir(payload) and payload_dir is None:
129
+ payload_dir = payload
130
+
131
+ if payload_dir is None:
132
+ import_payload_file(payload, enabled, dependencies)
133
+ return
134
+ elif not os.path.isdir(payload_dir):
135
+ return
136
+
137
+ for filename in sorted(os.listdir(payload_dir)):
138
+ try:
139
+ import_payload_file(os.path.join(payload_dir, filename), enabled, dependencies)
140
+ except PayloadImportError:
141
+ logger.error("Payload import failed")
142
+
143
+
144
+ def prepare_dependencies(dependencies: list) -> list | None:
145
+ """Convert the dependencies string to a list of integers."""
146
+ try:
147
+ list_all = dependencies[0].split(",")
148
+ except IndexError:
149
+ return None
150
+
151
+ # Remove empty values from the list
152
+ items = list(filter(None, list_all))
153
+ converted_list = []
154
+ for item in items:
155
+ try:
156
+ converted_list.append(int(item))
157
+ except ValueError:
158
+ continue
159
+
160
+ return converted_list
161
+
162
+
163
+ async def tail_log(file_path: str) -> AsyncGenerator[str, None]:
164
+ """Asynchronously follow the log file like `tail -f`."""
165
+ try:
166
+ async with await anyio.open_file(file_path) as file:
167
+ # Move the pointer to the end of the file
168
+ await file.seek(0, os.SEEK_END)
169
+
170
+ while True:
171
+ # Read new line
172
+ line = await file.readline()
173
+ if not line:
174
+ # Sleep for a little while before checking for new lines
175
+ await asyncio.sleep(0.5)
176
+ continue
177
+ yield line
178
+ except asyncio.exceptions.CancelledError:
179
+ pass
@@ -0,0 +1,51 @@
1
+ """Define Models for Payload."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class PayloadStats(BaseModel):
9
+ """Defines PayloadStats Model."""
10
+
11
+ count: int = 0
12
+ status: dict = {}
13
+ logs: dict = {}
14
+
15
+
16
+ class PayloadListItem(BaseModel):
17
+ """Defines PayloadListItem Model."""
18
+
19
+ index: int
20
+ name: str
21
+ filename: str
22
+ dependencies: list
23
+ logfile: str
24
+ status: str
25
+ enabled: bool
26
+ git_url: str | None
27
+ loglevel: str = "INFO"
28
+ start_time: Any | None
29
+ stop_time: Any | None
30
+ duration: Any | None
31
+ log_debug: int = 0
32
+ log_info: int = 0
33
+ log_warning: int = 0
34
+ log_error: int = 0
35
+ log_critical: int = 0
36
+ customizer_settings: dict = {}
37
+
38
+
39
+ class PayloadListItems(BaseModel):
40
+ """Defines PayloadListItems Model."""
41
+
42
+ stats: PayloadStats
43
+ results: list[PayloadListItem]
44
+
45
+
46
+ class UpdatedPayloadListItem(BaseModel):
47
+ """Defines UpdatedPayloadListItem Model."""
48
+
49
+ message: str
50
+ payload: PayloadListItem
51
+ updated_fields: dict