pyxecm 2.0.2__py3-none-any.whl → 2.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyxecm might be problematic. Click here for more details.
- pyxecm/__init__.py +3 -2
- pyxecm/avts.py +3 -1
- pyxecm/coreshare.py +71 -5
- pyxecm/customizer/api/app.py +7 -13
- pyxecm/customizer/api/auth/functions.py +37 -30
- pyxecm/customizer/api/common/functions.py +54 -0
- pyxecm/customizer/api/common/payload_list.py +39 -10
- pyxecm/customizer/api/common/router.py +55 -6
- pyxecm/customizer/api/settings.py +14 -3
- pyxecm/customizer/api/terminal/router.py +43 -18
- pyxecm/customizer/api/v1_csai/models.py +18 -0
- pyxecm/customizer/api/v1_csai/router.py +26 -1
- pyxecm/customizer/api/v1_otcs/router.py +16 -6
- pyxecm/customizer/api/v1_payload/functions.py +9 -3
- pyxecm/customizer/browser_automation.py +506 -199
- pyxecm/customizer/customizer.py +123 -22
- pyxecm/customizer/guidewire.py +170 -37
- pyxecm/customizer/payload.py +723 -330
- pyxecm/customizer/settings.py +21 -3
- pyxecm/customizer/translate.py +14 -10
- pyxecm/helper/data.py +12 -20
- pyxecm/helper/xml.py +1 -1
- pyxecm/maintenance_page/app.py +6 -2
- pyxecm/otawp.py +10 -6
- pyxecm/otca.py +187 -21
- pyxecm/otcs.py +2424 -415
- pyxecm/otds.py +4 -11
- pyxecm/otkd.py +1369 -0
- pyxecm/otmm.py +190 -66
- {pyxecm-2.0.2.dist-info → pyxecm-2.0.4.dist-info}/METADATA +2 -2
- {pyxecm-2.0.2.dist-info → pyxecm-2.0.4.dist-info}/RECORD +34 -32
- {pyxecm-2.0.2.dist-info → pyxecm-2.0.4.dist-info}/WHEEL +1 -1
- {pyxecm-2.0.2.dist-info → pyxecm-2.0.4.dist-info}/licenses/LICENSE +0 -0
- {pyxecm-2.0.2.dist-info → pyxecm-2.0.4.dist-info}/top_level.txt +0 -0
pyxecm/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""pyxecm - A python library to interact with Opentext
|
|
1
|
+
"""pyxecm - A python library to interact with Opentext REST APIs."""
|
|
2
2
|
|
|
3
3
|
from .avts import AVTS
|
|
4
4
|
from .coreshare import CoreShare
|
|
@@ -8,7 +8,8 @@ from .otca import OTCA
|
|
|
8
8
|
from .otcs import OTCS
|
|
9
9
|
from .otds import OTDS
|
|
10
10
|
from .otiv import OTIV
|
|
11
|
+
from .otkd import OTKD
|
|
11
12
|
from .otmm import OTMM
|
|
12
13
|
from .otpd import OTPD
|
|
13
14
|
|
|
14
|
-
__all__ = ["AVTS", "OTAC", "OTAWP", "OTCA", "OTCS", "OTDS", "OTIV", "OTMM", "OTPD", "CoreShare"]
|
|
15
|
+
__all__ = ["AVTS", "OTAC", "OTAWP", "OTCA", "OTCS", "OTDS", "OTIV", "OTKD", "OTMM", "OTPD", "CoreShare"]
|
pyxecm/avts.py
CHANGED
pyxecm/coreshare.py
CHANGED
|
@@ -27,6 +27,7 @@ import time
|
|
|
27
27
|
import urllib.parse
|
|
28
28
|
from http import HTTPStatus
|
|
29
29
|
from importlib.metadata import version
|
|
30
|
+
from urllib.parse import parse_qs, urlparse
|
|
30
31
|
|
|
31
32
|
import requests
|
|
32
33
|
|
|
@@ -522,7 +523,7 @@ class CoreShare:
|
|
|
522
523
|
Can be used to provide a more specific error message
|
|
523
524
|
in case an error occurs.
|
|
524
525
|
show_error (bool, optional):
|
|
525
|
-
True: write an error to the log file
|
|
526
|
+
True: write an error to the log file (this is the default)
|
|
526
527
|
False: write a warning to the log file
|
|
527
528
|
|
|
528
529
|
Returns:
|
|
@@ -933,6 +934,71 @@ class CoreShare:
|
|
|
933
934
|
|
|
934
935
|
# end method definition
|
|
935
936
|
|
|
937
|
+
def get_groups_iterator(
|
|
938
|
+
self,
|
|
939
|
+
count: int | None = None,
|
|
940
|
+
) -> iter:
|
|
941
|
+
"""Get an iterator object that can be used to traverse all Core Share groups.
|
|
942
|
+
|
|
943
|
+
Returning a generator avoids loading a large number of items into memory at once. Instead you
|
|
944
|
+
can iterate over the potential large list of Core Share groups.
|
|
945
|
+
|
|
946
|
+
Example usage:
|
|
947
|
+
groups = core_share_object.get_groups_iterator(page_size=10)
|
|
948
|
+
for group in groups:
|
|
949
|
+
logger.info("Traversing Core Share group -> %s", group["name"])
|
|
950
|
+
|
|
951
|
+
Args:
|
|
952
|
+
count (int | None, optional):
|
|
953
|
+
The chunk size for the number of groups returned by one
|
|
954
|
+
REST API call. If None, then a default of 250 is used.
|
|
955
|
+
|
|
956
|
+
Returns:
|
|
957
|
+
iter:
|
|
958
|
+
A generator yielding one OTDS group per iteration.
|
|
959
|
+
If the REST API fails, returns no value.
|
|
960
|
+
|
|
961
|
+
"""
|
|
962
|
+
|
|
963
|
+
offset = 0
|
|
964
|
+
|
|
965
|
+
while True:
|
|
966
|
+
response = self.get_groups(
|
|
967
|
+
offset=offset,
|
|
968
|
+
count=count,
|
|
969
|
+
)
|
|
970
|
+
if not response or not response.get("results", []):
|
|
971
|
+
# Don't return None! Plain return is what we need for iterators.
|
|
972
|
+
# Natural Termination: If the generator does not yield, it behaves
|
|
973
|
+
# like an empty iterable when used in a loop or converted to a list:
|
|
974
|
+
return
|
|
975
|
+
|
|
976
|
+
# Yield users one at a time:
|
|
977
|
+
yield from response["results"]
|
|
978
|
+
|
|
979
|
+
# See if we have an additional result page.
|
|
980
|
+
# If not terminate the iterator and return
|
|
981
|
+
# no value.
|
|
982
|
+
|
|
983
|
+
next_page_url = response["_links"].get("next")
|
|
984
|
+
if not next_page_url:
|
|
985
|
+
# Don't return None! Plain return is what we need for iterators.
|
|
986
|
+
# Natural Termination: If the generator does not yield, it behaves
|
|
987
|
+
# like an empty iterable when used in a loop or converted to a list:
|
|
988
|
+
return
|
|
989
|
+
next_page_url = next_page_url.get("href")
|
|
990
|
+
|
|
991
|
+
# Extract the query string from the URL
|
|
992
|
+
query = urlparse(next_page_url).query
|
|
993
|
+
|
|
994
|
+
# Parse the query parameters into a dictionary
|
|
995
|
+
params = parse_qs(query)
|
|
996
|
+
|
|
997
|
+
# Get the 'offset' value as an integer (it's a list by default)
|
|
998
|
+
offset = int(params.get("offset", [0])[0])
|
|
999
|
+
|
|
1000
|
+
# end method definition
|
|
1001
|
+
|
|
936
1002
|
def add_group(
|
|
937
1003
|
self,
|
|
938
1004
|
group_name: str,
|
|
@@ -1276,7 +1342,7 @@ class CoreShare:
|
|
|
1276
1342
|
dict | None:
|
|
1277
1343
|
Dictionary with the Core Share group data or None if the request fails.
|
|
1278
1344
|
|
|
1279
|
-
Example
|
|
1345
|
+
Example Response:
|
|
1280
1346
|
{
|
|
1281
1347
|
'results': [
|
|
1282
1348
|
{
|
|
@@ -1343,15 +1409,15 @@ class CoreShare:
|
|
|
1343
1409
|
|
|
1344
1410
|
# end method definition
|
|
1345
1411
|
|
|
1346
|
-
def get_users(self) ->
|
|
1412
|
+
def get_users(self) -> list | None:
|
|
1347
1413
|
"""Get Core Share users.
|
|
1348
1414
|
|
|
1349
1415
|
Args:
|
|
1350
1416
|
None
|
|
1351
1417
|
|
|
1352
1418
|
Returns:
|
|
1353
|
-
|
|
1354
|
-
|
|
1419
|
+
list | None:
|
|
1420
|
+
List with the Core Share user data or None if the request fails.
|
|
1355
1421
|
|
|
1356
1422
|
Example response (it is a list!):
|
|
1357
1423
|
[
|
pyxecm/customizer/api/app.py
CHANGED
|
@@ -13,7 +13,6 @@ from collections.abc import AsyncGenerator
|
|
|
13
13
|
from contextlib import asynccontextmanager
|
|
14
14
|
from datetime import datetime, timezone
|
|
15
15
|
from importlib.metadata import version
|
|
16
|
-
from threading import Thread
|
|
17
16
|
|
|
18
17
|
import uvicorn
|
|
19
18
|
from fastapi import FastAPI
|
|
@@ -85,17 +84,11 @@ async def lifespan(
|
|
|
85
84
|
# Optional Payload
|
|
86
85
|
import_payload(payload_dir=api_settings.payload_dir_optional)
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
maint_thread = Thread(target=run_maintenance_page, name="maintenance_page")
|
|
91
|
-
maint_thread.start()
|
|
87
|
+
logger.info("Starting maintenance_page thread...")
|
|
88
|
+
run_maintenance_page()
|
|
92
89
|
|
|
93
90
|
logger.info("Starting processing thread...")
|
|
94
|
-
|
|
95
|
-
target=PAYLOAD_LIST.run_payload_processing,
|
|
96
|
-
name="customization_run_api",
|
|
97
|
-
)
|
|
98
|
-
thread.start()
|
|
91
|
+
PAYLOAD_LIST.run_payload_processing(concurrent=api_settings.concurrent_payloads)
|
|
99
92
|
|
|
100
93
|
yield
|
|
101
94
|
logger.info("Shutdown")
|
|
@@ -104,8 +97,10 @@ async def lifespan(
|
|
|
104
97
|
|
|
105
98
|
app = FastAPI(
|
|
106
99
|
docs_url="/api",
|
|
107
|
-
title=
|
|
108
|
-
|
|
100
|
+
title=api_settings.title,
|
|
101
|
+
description=api_settings.description,
|
|
102
|
+
openapi_url=api_settings.openapi_url,
|
|
103
|
+
root_path=api_settings.root_path,
|
|
109
104
|
lifespan=lifespan,
|
|
110
105
|
version=version("pyxecm"),
|
|
111
106
|
openapi_tags=[
|
|
@@ -135,7 +130,6 @@ if api_settings.ws_terminal:
|
|
|
135
130
|
if api_settings.csai:
|
|
136
131
|
app.include_router(router=v1_csai_router)
|
|
137
132
|
|
|
138
|
-
|
|
139
133
|
logger = logging.getLogger("CustomizerAPI")
|
|
140
134
|
app.add_middleware(
|
|
141
135
|
CORSMiddleware,
|
|
@@ -5,14 +5,20 @@ from typing import Annotated
|
|
|
5
5
|
|
|
6
6
|
import requests
|
|
7
7
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
8
|
-
from fastapi.security import OAuth2PasswordBearer
|
|
8
|
+
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer
|
|
9
9
|
|
|
10
10
|
from pyxecm.customizer.api.auth.models import User
|
|
11
11
|
from pyxecm.customizer.api.settings import api_settings
|
|
12
12
|
|
|
13
13
|
router = APIRouter()
|
|
14
14
|
|
|
15
|
-
oauth2_scheme = OAuth2PasswordBearer(
|
|
15
|
+
oauth2_scheme = OAuth2PasswordBearer(
|
|
16
|
+
tokenUrl="token",
|
|
17
|
+
auto_error=False,
|
|
18
|
+
description="Authenticate with OTDS, user needs to be member of 'otadmins@otds.admin' group",
|
|
19
|
+
scheme_name="OTDS Authentication",
|
|
20
|
+
)
|
|
21
|
+
apikey_header = APIKeyHeader(name="x-api-key", auto_error=False, scheme_name="APIKey")
|
|
16
22
|
|
|
17
23
|
|
|
18
24
|
def get_groups(response: dict, token: str) -> list:
|
|
@@ -42,43 +48,44 @@ def get_groups(response: dict, token: str) -> list:
|
|
|
42
48
|
return []
|
|
43
49
|
|
|
44
50
|
|
|
45
|
-
async def get_current_user(
|
|
51
|
+
async def get_current_user(
|
|
52
|
+
token: Annotated[str, Depends(oauth2_scheme)], api_key: Annotated[str, Depends(apikey_header)]
|
|
53
|
+
) -> User:
|
|
46
54
|
"""Get the current user from OTDS and verify it."""
|
|
47
55
|
|
|
48
|
-
if api_settings.
|
|
56
|
+
if api_settings.api_key is not None and api_key == api_settings.api_key:
|
|
49
57
|
return User(
|
|
50
58
|
id="api",
|
|
51
|
-
full_name="API
|
|
59
|
+
full_name="API Key",
|
|
52
60
|
groups=["otadmins@otds.admin"],
|
|
53
61
|
is_admin=True,
|
|
54
62
|
is_sysadmin=True,
|
|
55
63
|
)
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
)
|
|
65
|
+
if token and token.startswith("*OTDSSSO*"):
|
|
66
|
+
url = api_settings.otds_url + "/otdsws/rest/currentuser"
|
|
67
|
+
headers = {
|
|
68
|
+
"Accept": "application/json",
|
|
69
|
+
"otdsticket": token,
|
|
70
|
+
}
|
|
71
|
+
response = requests.request("GET", url, headers=headers, timeout=2)
|
|
72
|
+
|
|
73
|
+
if response.ok:
|
|
74
|
+
response = json.loads(response.text)
|
|
75
|
+
|
|
76
|
+
return User(
|
|
77
|
+
id=response["user"]["id"],
|
|
78
|
+
full_name=response["user"]["name"],
|
|
79
|
+
groups=get_groups(response, token),
|
|
80
|
+
is_admin=response["isAdmin"],
|
|
81
|
+
is_sysadmin=response["isSysAdmin"],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
raise HTTPException(
|
|
85
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
86
|
+
detail="Invalid authentication credentials",
|
|
87
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
88
|
+
)
|
|
82
89
|
|
|
83
90
|
|
|
84
91
|
async def get_authorized_user(current_user: Annotated[User, Depends(get_current_user)]) -> User:
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""Define common functions."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import os
|
|
4
5
|
|
|
5
6
|
from pyxecm.customizer.api.common.payload_list import PayloadList
|
|
6
7
|
from pyxecm.customizer.api.settings import CustomizerAPISettings, api_settings
|
|
7
8
|
from pyxecm.customizer.k8s import K8s
|
|
9
|
+
from pyxecm.customizer.settings import Settings
|
|
10
|
+
from pyxecm.otcs import OTCS
|
|
8
11
|
|
|
9
12
|
logger = logging.getLogger("pyxecm.customizer.api")
|
|
10
13
|
|
|
@@ -25,6 +28,36 @@ def get_k8s_object() -> K8s:
|
|
|
25
28
|
return K8s(logger=logger, namespace=api_settings.namespace)
|
|
26
29
|
|
|
27
30
|
|
|
31
|
+
def get_otcs_object() -> OTCS:
|
|
32
|
+
"""Get an instance of a K8s object.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
K8s: Return a K8s object
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
settings = Settings()
|
|
39
|
+
|
|
40
|
+
cs = OTCS(
|
|
41
|
+
protocol=settings.otcs.url_backend.scheme,
|
|
42
|
+
hostname=settings.otcs.url_backend.host,
|
|
43
|
+
port=settings.otcs.url_backend.port,
|
|
44
|
+
public_url=str(settings.otcs.url),
|
|
45
|
+
username=settings.otcs.username,
|
|
46
|
+
password=settings.otcs.password.get_secret_value(),
|
|
47
|
+
user_partition=settings.otcs.partition,
|
|
48
|
+
resource_name=settings.otcs.resource_name,
|
|
49
|
+
base_path=settings.otcs.base_path,
|
|
50
|
+
support_path=settings.otcs.support_path,
|
|
51
|
+
download_dir=settings.otcs.download_dir,
|
|
52
|
+
feme_uri=settings.otcs.feme_uri,
|
|
53
|
+
logger=logger,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
cs.authenticate()
|
|
57
|
+
|
|
58
|
+
return cs
|
|
59
|
+
|
|
60
|
+
|
|
28
61
|
def get_settings() -> CustomizerAPISettings:
|
|
29
62
|
"""Get the API Settings object.
|
|
30
63
|
|
|
@@ -45,3 +78,24 @@ def get_otcs_logs_lock() -> dict:
|
|
|
45
78
|
"""
|
|
46
79
|
|
|
47
80
|
return LOGS_LOCK
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def list_files_in_directory(directory: str) -> dict:
|
|
84
|
+
"""Recursively list files in a directory and return a nested JSON structure with URLs."""
|
|
85
|
+
result = {}
|
|
86
|
+
for root, dirs, files in os.walk(directory):
|
|
87
|
+
# Sort directories and files alphabetically
|
|
88
|
+
dirs.sort()
|
|
89
|
+
files.sort()
|
|
90
|
+
|
|
91
|
+
current_level = result
|
|
92
|
+
path_parts = root.split(os.sep)
|
|
93
|
+
relative_path = os.path.relpath(root, directory)
|
|
94
|
+
for part in path_parts[len(directory.split(os.sep)) :]:
|
|
95
|
+
if part not in current_level:
|
|
96
|
+
current_level[part] = {}
|
|
97
|
+
current_level = current_level[part]
|
|
98
|
+
for file in files:
|
|
99
|
+
file_path = os.path.join(relative_path, file)
|
|
100
|
+
current_level[file] = file_path
|
|
101
|
+
return result
|
|
@@ -609,7 +609,7 @@ class PayloadList:
|
|
|
609
609
|
|
|
610
610
|
# Log each runnable item
|
|
611
611
|
for _, row in runnable_df.iterrows():
|
|
612
|
-
self.logger.
|
|
612
|
+
self.logger.debug(
|
|
613
613
|
"Added payload file -> '%s' with index -> %s to runnable queue.",
|
|
614
614
|
row["name"] if row["name"] else row["filename"],
|
|
615
615
|
row["index"],
|
|
@@ -619,12 +619,35 @@ class PayloadList:
|
|
|
619
619
|
|
|
620
620
|
# end method definition
|
|
621
621
|
|
|
622
|
-
def
|
|
622
|
+
def pick_running(self) -> int:
|
|
623
|
+
"""Pick all PayloadItems with status "running".
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
pd.DataFrame:
|
|
627
|
+
A list of running payload items.
|
|
628
|
+
|
|
629
|
+
"""
|
|
630
|
+
|
|
631
|
+
if self.payload_items.empty:
|
|
632
|
+
return 0
|
|
633
|
+
|
|
634
|
+
all_status = self.payload_items["status"].value_counts().to_dict()
|
|
635
|
+
|
|
636
|
+
return all_status.get("running", 0)
|
|
637
|
+
|
|
638
|
+
# end method definition
|
|
639
|
+
|
|
640
|
+
def process_payload_list(self, concurrent: int | None = None) -> None:
|
|
623
641
|
"""Process runnable payloads.
|
|
624
642
|
|
|
643
|
+
Args:
|
|
644
|
+
concurrent (int | None, optional):
|
|
645
|
+
The maximum number of concurrent payloads to run at any given time.
|
|
646
|
+
|
|
625
647
|
Continuously checks for runnable payload items and starts their
|
|
626
648
|
"process_payload" method in separate threads.
|
|
627
649
|
Runs as a daemon until the customizer ends.
|
|
650
|
+
|
|
628
651
|
"""
|
|
629
652
|
|
|
630
653
|
def run_and_complete_payload(payload_item: pd.Series) -> None:
|
|
@@ -817,6 +840,18 @@ class PayloadList:
|
|
|
817
840
|
# Start a thread for each runnable item (item is a pd.Series)
|
|
818
841
|
if runnable_items is not None:
|
|
819
842
|
for _, item in runnable_items.iterrows():
|
|
843
|
+
if concurrent and self.pick_running() >= concurrent:
|
|
844
|
+
self.logger.debug(
|
|
845
|
+
"Reached concurrency limit of %s payloads. Waiting for one to finish.",
|
|
846
|
+
)
|
|
847
|
+
break
|
|
848
|
+
|
|
849
|
+
self.logger.info(
|
|
850
|
+
"Added payload file -> '%s' with index -> %s to runnable queue.",
|
|
851
|
+
item["name"] if item["name"] else item["filename"],
|
|
852
|
+
item["index"],
|
|
853
|
+
)
|
|
854
|
+
|
|
820
855
|
# Update the status to "running" in the data frame to prevent re-processing
|
|
821
856
|
self.payload_items.loc[
|
|
822
857
|
self.payload_items["name"] == item["name"],
|
|
@@ -837,13 +872,14 @@ class PayloadList:
|
|
|
837
872
|
|
|
838
873
|
# end method definition
|
|
839
874
|
|
|
840
|
-
def run_payload_processing(self) -> None:
|
|
875
|
+
def run_payload_processing(self, concurrent: int | None = None) -> None:
|
|
841
876
|
"""Start the `process_payload_list` method in a daemon thread."""
|
|
842
877
|
|
|
843
878
|
scheduler_thread = threading.Thread(
|
|
844
879
|
target=self.process_payload_list,
|
|
845
880
|
daemon=True,
|
|
846
881
|
name="Scheduler",
|
|
882
|
+
kwargs={"concurrent": concurrent},
|
|
847
883
|
)
|
|
848
884
|
|
|
849
885
|
self.logger.info(
|
|
@@ -853,13 +889,6 @@ class PayloadList:
|
|
|
853
889
|
self._stopped = False
|
|
854
890
|
scheduler_thread.start()
|
|
855
891
|
|
|
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
892
|
# end method definition
|
|
864
893
|
|
|
865
894
|
def stop_payload_processing(self) -> None:
|
|
@@ -1,34 +1,38 @@
|
|
|
1
1
|
"""API Implemenation for the Customizer to start and control the payload processing."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import mimetypes
|
|
4
5
|
import os
|
|
5
6
|
import signal
|
|
7
|
+
import tempfile
|
|
6
8
|
from http import HTTPStatus
|
|
7
9
|
from typing import Annotated
|
|
8
10
|
|
|
9
|
-
from fastapi import APIRouter, Depends, HTTPException
|
|
10
|
-
from fastapi.responses import JSONResponse, RedirectResponse
|
|
11
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
|
|
12
|
+
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
|
|
11
13
|
|
|
12
14
|
from pyxecm.customizer.api.auth.functions import get_authorized_user
|
|
13
15
|
from pyxecm.customizer.api.auth.models import User
|
|
14
|
-
from pyxecm.customizer.api.common.functions import PAYLOAD_LIST
|
|
16
|
+
from pyxecm.customizer.api.common.functions import PAYLOAD_LIST, list_files_in_directory
|
|
15
17
|
from pyxecm.customizer.api.common.models import CustomizerStatus
|
|
16
18
|
|
|
17
|
-
router = APIRouter(
|
|
19
|
+
router = APIRouter()
|
|
18
20
|
|
|
19
21
|
logger = logging.getLogger("pyxecm.customizer.api.common")
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
@router.get("/", include_in_schema=False)
|
|
23
|
-
async def redirect_to_api() -> RedirectResponse:
|
|
25
|
+
async def redirect_to_api(request: Request) -> RedirectResponse:
|
|
24
26
|
"""Redirect from / to /api.
|
|
25
27
|
|
|
26
28
|
Returns:
|
|
27
29
|
None
|
|
28
30
|
|
|
29
31
|
"""
|
|
30
|
-
|
|
32
|
+
# Construct the new URL by appending /api
|
|
33
|
+
new_url = f"{request.url.path!s}api"
|
|
31
34
|
|
|
35
|
+
return RedirectResponse(url=new_url)
|
|
32
36
|
|
|
33
37
|
@router.get(path="/status", name="Get Status")
|
|
34
38
|
async def get_status() -> CustomizerStatus:
|
|
@@ -70,3 +74,48 @@ def shutdown(user: Annotated[User, Depends(get_authorized_user)]) -> JSONRespons
|
|
|
70
74
|
os.kill(os.getpid(), signal.SIGTERM)
|
|
71
75
|
|
|
72
76
|
return JSONResponse({"status": "shutdown"}, status_code=HTTPStatus.ACCEPTED)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@router.get(path="/browser_automations/assets", tags=["payload"])
|
|
80
|
+
def list_browser_automation_files(
|
|
81
|
+
user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
|
|
82
|
+
) -> JSONResponse:
|
|
83
|
+
"""List all browser automation files."""
|
|
84
|
+
|
|
85
|
+
result = list_files_in_directory(
|
|
86
|
+
os.path.join(
|
|
87
|
+
tempfile.gettempdir(),
|
|
88
|
+
"browser_automations",
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return JSONResponse(result)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@router.get(path="/browser_automations/download", tags=["payload"])
|
|
96
|
+
def get_browser_automation_file(
|
|
97
|
+
user: Annotated[User, Depends(get_authorized_user)], # noqa: ARG001
|
|
98
|
+
file: Annotated[str, Query(description="File name")],
|
|
99
|
+
) -> FileResponse:
|
|
100
|
+
"""Download the logfile for a specific payload."""
|
|
101
|
+
|
|
102
|
+
filename = os.path.join(tempfile.gettempdir(), "browser_automations", file)
|
|
103
|
+
|
|
104
|
+
if not os.path.isfile(filename):
|
|
105
|
+
raise HTTPException(
|
|
106
|
+
status_code=HTTPStatus.NOT_FOUND,
|
|
107
|
+
detail="File -> '{}' not found".format(filename),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
media_type, _ = mimetypes.guess_type(filename)
|
|
111
|
+
|
|
112
|
+
with open(filename, "rb") as f:
|
|
113
|
+
content = f.read()
|
|
114
|
+
|
|
115
|
+
return Response(
|
|
116
|
+
content,
|
|
117
|
+
media_type=media_type,
|
|
118
|
+
headers={
|
|
119
|
+
"Content-Disposition": f'attachment; filename="{os.path.basename(filename)}"',
|
|
120
|
+
},
|
|
121
|
+
)
|
|
@@ -16,14 +16,25 @@ from pydantic_settings import (
|
|
|
16
16
|
class CustomizerAPISettings(BaseSettings):
|
|
17
17
|
"""Settings for the Customizer API."""
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
title: str = Field(default="Customizer API", description="Name of the API Service")
|
|
20
|
+
description: str = Field(
|
|
21
|
+
default="API provided by [pyxecm](https://github.com/opentext/pyxecm). The documentation for the payload syntax can be found [here](https://opentext.github.io/pyxecm/payload-syntax/).",
|
|
22
|
+
description="Descriptive text on the SwaggerUI page.",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
api_key: str | None = Field(
|
|
20
26
|
default=None,
|
|
21
|
-
description="Optional
|
|
27
|
+
description="Optional API KEY that can be specified that has access to the Customizer API, bypassing the OTDS authentication.",
|
|
22
28
|
)
|
|
23
29
|
bind_address: str = Field(default="0.0.0.0", description="Interface to bind the Customizer API.") # noqa: S104
|
|
24
30
|
bind_port: int = Field(default=8000, description="Port to bind the Customizer API to")
|
|
25
31
|
workers: int = Field(default=1, description="Number of workers to use for the API BackgroundTasks")
|
|
32
|
+
root_path: str = Field(default="/", description="Root path for the Customizer API")
|
|
33
|
+
openapi_url: str = Field(default="/api/openapi.json", description="OpenAPI URL")
|
|
26
34
|
|
|
35
|
+
concurrent_payloads: int = Field(
|
|
36
|
+
default=3, description="Maximum number of concurrent payloads that are executed at the same time."
|
|
37
|
+
)
|
|
27
38
|
import_payload: bool = Field(default=False)
|
|
28
39
|
payload: str = Field(
|
|
29
40
|
default="/payload/payload.yml.gz.b64",
|
|
@@ -58,7 +69,7 @@ class CustomizerAPISettings(BaseSettings):
|
|
|
58
69
|
description="Namespace to use for otxecm resource lookups",
|
|
59
70
|
)
|
|
60
71
|
maintenance_mode: bool = Field(
|
|
61
|
-
default=
|
|
72
|
+
default=False,
|
|
62
73
|
description="Automatically enable and disable the maintenance mode during payload deployments.",
|
|
63
74
|
)
|
|
64
75
|
|