pyxecm 2.0.0__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 (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 +568 -326
  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 +201 -37
  43. pyxecm/otds.py +35 -13
  44. {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/METADATA +6 -29
  45. pyxecm-2.0.1.dist-info/RECORD +76 -0
  46. {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.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.1.dist-info}/licenses/LICENSE +0 -0
  50. {pyxecm-2.0.0.dist-info → pyxecm-2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,499 @@
1
+ """Define router for v1_payload."""
2
+
3
+ import base64
4
+ import gzip
5
+ import json
6
+ import logging
7
+ import os
8
+ import shutil
9
+ from datetime import datetime, timezone
10
+ from http import HTTPStatus
11
+ from typing import Annotated, Literal
12
+
13
+ from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
14
+ from fastapi.responses import FileResponse, JSONResponse, Response, StreamingResponse
15
+
16
+ from pyxecm.customizer.api.auth.functions import get_authorized_user
17
+ from pyxecm.customizer.api.auth.models import User
18
+ from pyxecm.customizer.api.common.functions import PAYLOAD_LIST, get_settings
19
+ from pyxecm.customizer.api.settings import CustomizerAPISettings
20
+ from pyxecm.customizer.api.v1_payload.functions import prepare_dependencies, tail_log
21
+ from pyxecm.customizer.api.v1_payload.models import PayloadListItem, PayloadListItems, UpdatedPayloadListItem
22
+ from pyxecm.customizer.payload import load_payload
23
+
24
+ router = APIRouter(prefix="/api/v1/payload", tags=["payload"])
25
+
26
+ logger = logging.getLogger("pyxecm.customizer.api.v1_payload")
27
+
28
+
29
+ @router.post(path="")
30
+ def create_payload_item(
31
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
32
+ settings: Annotated[CustomizerAPISettings, Depends(get_settings)],
33
+ upload_file: Annotated[UploadFile, File(...)],
34
+ name: Annotated[str, Form()] = "",
35
+ dependencies: Annotated[list[int] | list[str] | None, Form()] = None,
36
+ enabled: Annotated[bool, Form()] = True,
37
+ loglevel: Annotated[
38
+ Literal["DEBUG", "INFO", "WARNING"] | None,
39
+ Form(
40
+ description="Loglevel for the Payload processing",
41
+ ),
42
+ ] = "INFO",
43
+ ) -> PayloadListItem:
44
+ """Upload a new payload item.
45
+
46
+ Args:
47
+ user (User, optional):
48
+ The user who is uploading the payload. Defaults to None.
49
+ settings (CustomizerAPISettings):
50
+ The settings object.
51
+ upload_file (UploadFile, optional):
52
+ The file to upload. Defaults to File(...).
53
+ name (str, optional):
54
+ The name of the payload (if not provided we will use the file name).
55
+ dependencies (list of integers):
56
+ List of other payload items this item depends on.
57
+ enabled (bool):
58
+ Flag indicating if the payload is enabled or not.
59
+ loglevel (str, optional):
60
+ The loglevel for the payload processing. Defaults to "INFO".
61
+
62
+ Raises:
63
+ HTTPException:
64
+ Raised, if payload list is not initialized.
65
+
66
+ Returns:
67
+ dict:
68
+ The HTTP response.
69
+
70
+ """
71
+ if dependencies:
72
+ dependencies = prepare_dependencies(dependencies)
73
+
74
+ # Set name if not provided
75
+ name = name or os.path.splitext(os.path.basename(upload_file.filename))[0]
76
+ file_extension = os.path.splitext(upload_file.filename)[1]
77
+ file_name = os.path.join(settings.temp_dir, f"{name}{file_extension}")
78
+
79
+ with open(file_name, "wb") as buffer:
80
+ shutil.copyfileobj(upload_file.file, buffer)
81
+
82
+ if dependencies == [-1]:
83
+ dependencies = []
84
+
85
+ return PayloadListItem(
86
+ PAYLOAD_LIST.add_payload_item(
87
+ name=name,
88
+ filename=file_name,
89
+ status="planned",
90
+ logfile=os.path.join(settings.temp_dir, "{}.log".format(name)),
91
+ dependencies=dependencies or [],
92
+ enabled=enabled,
93
+ loglevel=loglevel,
94
+ )
95
+ )
96
+
97
+
98
+ @router.get(path="")
99
+ async def get_payload_items(
100
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
101
+ ) -> PayloadListItems:
102
+ """Get all Payload items.
103
+
104
+ Raises:
105
+ HTTPException: payload list not initialized
106
+ HTTPException: payload list is empty
107
+
108
+ Returns:
109
+ dict:
110
+ HTTP response with the result data
111
+
112
+ """
113
+
114
+ df = PAYLOAD_LIST.get_payload_items()
115
+
116
+ if df is None:
117
+ raise HTTPException(
118
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
119
+ detail="Payload list is empty.",
120
+ )
121
+
122
+ data = [PayloadListItem(index=idx, **row) for idx, row in df.iterrows()]
123
+
124
+ stats = {
125
+ "count": len(df),
126
+ "status": df["status"].value_counts().to_dict(),
127
+ "logs": {
128
+ "debug": df["log_debug"].sum(),
129
+ "info": df["log_info"].sum(),
130
+ "warning": df["log_warning"].sum(),
131
+ "error": df["log_error"].sum(),
132
+ "critical": df["log_critical"].sum(),
133
+ },
134
+ }
135
+
136
+ return PayloadListItems(stats=stats, results=data)
137
+
138
+
139
+ @router.get(path="/{payload_id}")
140
+ async def get_payload_item(
141
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
142
+ payload_id: int,
143
+ ) -> PayloadListItem:
144
+ """Get a payload item based on its ID.
145
+
146
+ Args:
147
+ user: Annotated[User, Depends(get_authorized_user)]
148
+ payload_id (int): payload item ID
149
+
150
+ Raises:
151
+ HTTPException: a payload item with the given ID couldn't be found
152
+
153
+ Returns:
154
+ dict:
155
+ HTTP response.
156
+
157
+ """
158
+ data = PAYLOAD_LIST.get_payload_item(index=payload_id)
159
+
160
+ if data is None:
161
+ raise HTTPException(
162
+ status_code=HTTPStatus.NOT_FOUND,
163
+ detail="Payload with index -> {} not found".format(payload_id),
164
+ )
165
+
166
+ return PayloadListItem(index=payload_id, **data, asd="123")
167
+
168
+
169
+ @router.put(path="/{payload_id}")
170
+ async def update_payload_item(
171
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
172
+ payload_id: int,
173
+ name: Annotated[str | None, Form()] = None,
174
+ dependencies: Annotated[list[int] | list[str] | None, Form()] = None,
175
+ enabled: Annotated[bool | None, Form()] = None,
176
+ status: Annotated[Literal["planned", "completed"] | None, Form()] = None,
177
+ loglevel: Annotated[
178
+ Literal["DEBUG", "INFO", "WARNING"] | None,
179
+ Form(
180
+ description="Loglevel for the Payload processing",
181
+ ),
182
+ ] = None,
183
+ customizer_settings: Annotated[str | None, Form()] = None,
184
+ ) -> UpdatedPayloadListItem:
185
+ """Update an existing payload item.
186
+
187
+ Args:
188
+ user (Optional[User]): User performing the update.
189
+ payload_id (int): ID of the payload to update.
190
+ upload_file (UploadFile, optional): replace the file name
191
+ name (Optional[str]): Updated name.
192
+ dependencies (Optional[List[int]]): Updated list of dependencies.
193
+ enabled (Optional[bool]): Updated enabled status.
194
+ loglevel (Optional[str]): Updated loglevel.
195
+ status (Optional[str]): Updated status.
196
+ customizer_settings (Optional[str]): Updated customizer settings.
197
+
198
+ Returns:
199
+ dict: HTTP response with the updated payload details.
200
+
201
+ """
202
+
203
+ if dependencies:
204
+ dependencies = prepare_dependencies(dependencies)
205
+ # Check if the payload exists
206
+ payload_item = PAYLOAD_LIST.get_payload_item(
207
+ payload_id,
208
+ ) # Assumes a method to retrieve payload by ID
209
+ if payload_item is None:
210
+ raise HTTPException(
211
+ status_code=HTTPStatus.NOT_FOUND,
212
+ detail="Payload with ID -> {} not found.".format(payload_id),
213
+ )
214
+
215
+ update_data = {}
216
+
217
+ # Update fields if provided
218
+ if name is not None:
219
+ update_data["name"] = name
220
+ if dependencies is not None:
221
+ update_data["dependencies"] = dependencies
222
+ if enabled is not None:
223
+ update_data["enabled"] = enabled
224
+ if status is not None:
225
+ update_data["status"] = status
226
+ if loglevel is not None:
227
+ update_data["loglevel"] = loglevel
228
+
229
+ thread_logger = logging.getLogger(name=f"Payload_{payload_id}")
230
+ thread_logger.setLevel(loglevel)
231
+
232
+ if customizer_settings is not None:
233
+ try:
234
+ update_data["customizer_settings"] = json.loads(customizer_settings)
235
+ except Exception as e:
236
+ raise HTTPException(detail=e, status_code=HTTPStatus.BAD_REQUEST) from e
237
+
238
+ if "status" in update_data and update_data["status"] == "planned":
239
+ logger.info("Resetting log message counters for -> %s", payload_id)
240
+ update_data["log_debug"] = 0
241
+ update_data["log_info"] = 0
242
+ update_data["log_warning"] = 0
243
+ update_data["log_error"] = 0
244
+ update_data["log_critical"] = 0
245
+
246
+ update_data["start_time"] = None
247
+ update_data["stop_time"] = None
248
+ update_data["duration"] = None
249
+
250
+ data = PAYLOAD_LIST.get_payload_item(index=payload_id)
251
+ if os.path.isfile(data.logfile):
252
+ logger.info(
253
+ "Deleting log file (for payload) -> %s (%s)",
254
+ data.logfile,
255
+ payload_id,
256
+ )
257
+
258
+ now = datetime.now(timezone.utc)
259
+ old_log_name = (
260
+ os.path.dirname(data.logfile)
261
+ + "/"
262
+ + os.path.splitext(os.path.basename(data.logfile))[0]
263
+ + now.strftime("_%Y-%m-%d_%H-%M-%S.log")
264
+ )
265
+
266
+ os.rename(data.logfile, old_log_name)
267
+
268
+ # Save the updated payload back to the list (or database)
269
+ result = PAYLOAD_LIST.update_payload_item(
270
+ index=payload_id,
271
+ update_data=update_data,
272
+ ) # Assumes a method to update the payload
273
+ if not result:
274
+ raise HTTPException(
275
+ status_code=HTTPStatus.NOT_FOUND,
276
+ detail="Failed to update Payload with ID -> {} with data -> {}".format(
277
+ payload_id,
278
+ update_data,
279
+ ),
280
+ )
281
+
282
+ return UpdatedPayloadListItem(
283
+ message="Payload updated successfully",
284
+ payload=PayloadListItem(index=payload_id, **PAYLOAD_LIST.get_payload_item(index=payload_id)),
285
+ updated_fields=update_data,
286
+ )
287
+
288
+
289
+ @router.delete(path="/{payload_id}")
290
+ async def delete_payload_item(
291
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
292
+ payload_id: int,
293
+ ) -> JSONResponse:
294
+ """Delete an existing payload item.
295
+
296
+ Args:
297
+ user (Optional[User]): User performing the update.
298
+ payload_id (int): The ID of the payload to update.
299
+
300
+ Returns:
301
+ dict: response or None
302
+
303
+ """
304
+
305
+ # Check if the payload exists
306
+ result = PAYLOAD_LIST.remove_payload_item(payload_id)
307
+ if not result:
308
+ raise HTTPException(
309
+ status_code=HTTPStatus.NOT_FOUND,
310
+ detail="Payload with ID -> {} not found.".format(payload_id),
311
+ )
312
+
313
+
314
+ @router.put(path="/{payload_id}/up")
315
+ async def move_payload_item_up(
316
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
317
+ payload_id: int,
318
+ ) -> dict:
319
+ """Move a payload item up in the list.
320
+
321
+ Args:
322
+ user: Annotated[User, Depends(get_authorized_user)]
323
+ payload_id (int): payload item ID
324
+
325
+ Raises:
326
+ HTTPException: a payload item with the given ID couldn't be found
327
+
328
+ Returns:
329
+ dict: HTTP response
330
+
331
+ """
332
+
333
+ position = PAYLOAD_LIST.move_payload_item_up(index=payload_id)
334
+
335
+ if position is None:
336
+ raise HTTPException(
337
+ status_code=HTTPStatus.NOT_FOUND,
338
+ detail="Payload item with index -> {} is either out of range or is already on top of the payload list!".format(
339
+ payload_id,
340
+ ),
341
+ )
342
+
343
+ return {"result": {"new_position": position}}
344
+
345
+
346
+ @router.put(path="/{payload_id}/down")
347
+ async def move_payload_item_down(
348
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
349
+ payload_id: int,
350
+ ) -> dict:
351
+ """Move a payload item down in the list.
352
+
353
+ Args:
354
+ user: Annotated[User, Depends(get_authorized_user)]
355
+ payload_id (int):
356
+ The payload item ID.
357
+
358
+ Raises:
359
+ HTTPException: a payload item with the given ID couldn't be found
360
+
361
+ Returns:
362
+ dict: HTTP response
363
+
364
+ """
365
+
366
+ position = PAYLOAD_LIST.move_payload_item_down(index=payload_id)
367
+
368
+ if position is None:
369
+ raise HTTPException(
370
+ status_code=HTTPStatus.NOT_FOUND,
371
+ detail="Payload item with index -> {} is either out of range or is already on bottom of the payload list!".format(
372
+ payload_id,
373
+ ),
374
+ )
375
+
376
+ return {"result": {"new_position": position}}
377
+
378
+
379
+ @router.get(path="/{payload_id}/content")
380
+ async def get_payload_content(
381
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
382
+ # pylint: disable=unused-argument
383
+ payload_id: int,
384
+ ) -> dict | None:
385
+ """Get a payload item based on its ID.
386
+
387
+ Args:
388
+ user: Annotated[User, Depends(get_authorized_user)]
389
+ payload_id (int):
390
+ The payload item ID.
391
+
392
+ Raises:
393
+ HTTPException:
394
+ A payload item with the given ID couldn't be found.
395
+
396
+ Returns:
397
+ dict:
398
+ HTTP response.
399
+
400
+ """
401
+
402
+ data = PAYLOAD_LIST.get_payload_item(index=payload_id)
403
+
404
+ if data is None:
405
+ raise HTTPException(
406
+ status_code=HTTPStatus.NOT_FOUND,
407
+ detail="Payload with ID -> {} not found!".format(payload_id),
408
+ )
409
+
410
+ filename = data.filename
411
+
412
+ return load_payload(payload_source=filename)
413
+
414
+
415
+ @router.get(path="/{payload_id}/download")
416
+ def download_payload_content(
417
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
418
+ payload_id: int,
419
+ ) -> FileResponse:
420
+ """Download the payload for a specific payload item."""
421
+
422
+ payload = PAYLOAD_LIST.get_payload_item(index=payload_id)
423
+
424
+ if payload is None:
425
+ raise HTTPException(
426
+ status_code=HTTPStatus.NOT_FOUND,
427
+ detail="Payload with ID -> {} not found!".format(payload_id),
428
+ )
429
+
430
+ if not os.path.isfile(payload.filename):
431
+ raise HTTPException(
432
+ status_code=HTTPStatus.NOT_FOUND,
433
+ detail="Payload file -> '{}' not found".format(payload.filename),
434
+ )
435
+
436
+ with open(payload.filename, encoding="UTF-8") as file:
437
+ content = file.read()
438
+
439
+ if payload.filename.endswith(".gz.b64"):
440
+ content = base64.b64decode(content)
441
+ content = gzip.decompress(content)
442
+
443
+ return Response(
444
+ content,
445
+ media_type="application/octet-stream",
446
+ headers={
447
+ "Content-Disposition": f'attachment; filename="{os.path.basename(payload.filename.removesuffix(".gz.b64"))}"',
448
+ },
449
+ )
450
+
451
+
452
+ @router.get(path="/{payload_id}/log")
453
+ def download_payload_logfile(
454
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
455
+ payload_id: int,
456
+ ) -> FileResponse:
457
+ """Download the logfile for a specific payload."""
458
+
459
+ payload = PAYLOAD_LIST.get_payload_item(index=payload_id)
460
+
461
+ if payload is None:
462
+ raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Payload not found")
463
+
464
+ filename = payload.logfile
465
+
466
+ if not os.path.isfile(filename):
467
+ raise HTTPException(
468
+ status_code=HTTPStatus.NOT_FOUND,
469
+ detail="Log file -> '{}' not found".format(filename),
470
+ )
471
+ with open(filename, encoding="UTF-8") as file:
472
+ content = file.read()
473
+ return Response(
474
+ content,
475
+ media_type="application/octet-stream",
476
+ headers={
477
+ "Content-Disposition": f'attachment; filename="{os.path.basename(filename)}"',
478
+ },
479
+ )
480
+
481
+
482
+ @router.get(path="/{payload_id}/log/stream")
483
+ async def stream_logfile(
484
+ user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
485
+ payload_id: int,
486
+ ) -> StreamingResponse:
487
+ """Stream the logfile and follow changes."""
488
+
489
+ payload = PAYLOAD_LIST.get_payload_item(index=payload_id)
490
+
491
+ if payload is None:
492
+ raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Payload not found")
493
+
494
+ filename = payload.logfile
495
+
496
+ if os.path.isfile(filename):
497
+ return StreamingResponse(tail_log(filename), media_type="text/plain")
498
+
499
+ raise HTTPException(status_code=HTTPStatus.NOT_FOUND)