pyxecm 1.6__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.
- pyxecm/__init__.py +6 -4
- pyxecm/avts.py +673 -246
- pyxecm/coreshare.py +686 -467
- pyxecm/customizer/__init__.py +16 -4
- pyxecm/customizer/__main__.py +58 -0
- pyxecm/customizer/api/__init__.py +5 -0
- pyxecm/customizer/api/__main__.py +6 -0
- pyxecm/customizer/api/app.py +914 -0
- pyxecm/customizer/api/auth.py +154 -0
- pyxecm/customizer/api/metrics.py +92 -0
- pyxecm/customizer/api/models.py +13 -0
- pyxecm/customizer/api/payload_list.py +865 -0
- pyxecm/customizer/api/settings.py +103 -0
- pyxecm/customizer/browser_automation.py +332 -139
- pyxecm/customizer/customizer.py +1007 -1130
- pyxecm/customizer/exceptions.py +35 -0
- pyxecm/customizer/guidewire.py +322 -0
- pyxecm/customizer/k8s.py +713 -378
- pyxecm/customizer/log.py +107 -0
- pyxecm/customizer/m365.py +2867 -909
- pyxecm/customizer/nhc.py +1169 -0
- pyxecm/customizer/openapi.py +258 -0
- pyxecm/customizer/payload.py +16817 -7467
- pyxecm/customizer/pht.py +699 -285
- pyxecm/customizer/salesforce.py +516 -342
- pyxecm/customizer/sap.py +58 -41
- pyxecm/customizer/servicenow.py +593 -371
- pyxecm/customizer/settings.py +442 -0
- pyxecm/customizer/successfactors.py +408 -346
- pyxecm/customizer/translate.py +83 -48
- pyxecm/helper/__init__.py +5 -2
- pyxecm/helper/assoc.py +83 -43
- pyxecm/helper/data.py +2406 -870
- pyxecm/helper/logadapter.py +27 -0
- pyxecm/helper/web.py +229 -101
- pyxecm/helper/xml.py +527 -171
- pyxecm/maintenance_page/__init__.py +5 -0
- pyxecm/maintenance_page/__main__.py +6 -0
- pyxecm/maintenance_page/app.py +51 -0
- pyxecm/maintenance_page/settings.py +28 -0
- pyxecm/maintenance_page/static/favicon.avif +0 -0
- pyxecm/maintenance_page/templates/maintenance.html +165 -0
- pyxecm/otac.py +234 -140
- pyxecm/otawp.py +1436 -557
- pyxecm/otcs.py +7716 -3161
- pyxecm/otds.py +2150 -919
- pyxecm/otiv.py +36 -21
- pyxecm/otmm.py +1272 -325
- pyxecm/otpd.py +231 -127
- pyxecm-2.0.0.dist-info/METADATA +145 -0
- pyxecm-2.0.0.dist-info/RECORD +54 -0
- {pyxecm-1.6.dist-info → pyxecm-2.0.0.dist-info}/WHEEL +1 -1
- pyxecm-1.6.dist-info/METADATA +0 -53
- pyxecm-1.6.dist-info/RECORD +0 -32
- {pyxecm-1.6.dist-info → pyxecm-2.0.0.dist-info/licenses}/LICENSE +0 -0
- {pyxecm-1.6.dist-info → pyxecm-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Utility library to handle the authentication with OTDS."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|
10
|
+
|
|
11
|
+
from pyxecm.customizer.api.models import User
|
|
12
|
+
from pyxecm.customizer.api.settings import api_settings
|
|
13
|
+
|
|
14
|
+
router = APIRouter()
|
|
15
|
+
|
|
16
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_groups(response: dict, token: str) -> list:
|
|
20
|
+
"""Get the groups of the user.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
response (_type_): _description_
|
|
24
|
+
token (_type_): _description_
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
list: _description_
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
headers = {
|
|
32
|
+
"Accept": "application/json",
|
|
33
|
+
"otdsticket": token,
|
|
34
|
+
}
|
|
35
|
+
url = api_settings.otds_url + "/otdsws/rest/users/" + response["user"]["id"] + "/memberof"
|
|
36
|
+
|
|
37
|
+
response = requests.request("GET", url, headers=headers, timeout=5)
|
|
38
|
+
if response.ok:
|
|
39
|
+
response = json.loads(response.text)
|
|
40
|
+
return [group["id"] for group in response.get("groups", [])]
|
|
41
|
+
|
|
42
|
+
# Retur empty list if request wasn't successful
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> User:
|
|
47
|
+
"""Get the current user from OTDS and verify it."""
|
|
48
|
+
|
|
49
|
+
if api_settings.api_token is not None and token == api_settings.api_token:
|
|
50
|
+
return User(
|
|
51
|
+
id="api",
|
|
52
|
+
full_name="API Token",
|
|
53
|
+
groups=["otadmins@otds.admin"],
|
|
54
|
+
is_admin=True,
|
|
55
|
+
is_sysadmin=True,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
url = api_settings.otds_url + "/otdsws/rest/currentuser"
|
|
59
|
+
headers = {
|
|
60
|
+
"Accept": "application/json",
|
|
61
|
+
"otdsticket": token,
|
|
62
|
+
}
|
|
63
|
+
response = requests.request("GET", url, headers=headers, timeout=2)
|
|
64
|
+
|
|
65
|
+
if response.ok:
|
|
66
|
+
response = json.loads(response.text)
|
|
67
|
+
|
|
68
|
+
user = User(
|
|
69
|
+
id=response["user"]["id"],
|
|
70
|
+
full_name=response["user"]["name"],
|
|
71
|
+
groups=get_groups(response, token),
|
|
72
|
+
is_admin=response["isAdmin"],
|
|
73
|
+
is_sysadmin=response["isSysAdmin"],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return user
|
|
77
|
+
else:
|
|
78
|
+
raise HTTPException(
|
|
79
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
80
|
+
detail="Invalid authentication credentials",
|
|
81
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def get_authorized_user(current_user: Annotated[User, Depends(get_current_user)]) -> User:
|
|
86
|
+
"""Check if the user is authorized (member of the Group otadmin@otds.admin)."""
|
|
87
|
+
|
|
88
|
+
if "otadmins@otds.admin" not in current_user.groups:
|
|
89
|
+
raise HTTPException(
|
|
90
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
91
|
+
detail=f"User {current_user.id} is not authorized",
|
|
92
|
+
)
|
|
93
|
+
return current_user
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@router.post("/token", tags=["auth"])
|
|
97
|
+
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> JSONResponse:
|
|
98
|
+
"""Login using OTDS and return a token."""
|
|
99
|
+
|
|
100
|
+
url = api_settings.otds_url + "/otdsws/rest/authentication/credentials"
|
|
101
|
+
|
|
102
|
+
payload = json.dumps(
|
|
103
|
+
{"userName": form_data.username, "password": form_data.password},
|
|
104
|
+
)
|
|
105
|
+
headers = {
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
"Accept": "application/json",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
response = requests.request(
|
|
112
|
+
"POST",
|
|
113
|
+
url,
|
|
114
|
+
headers=headers,
|
|
115
|
+
data=payload,
|
|
116
|
+
timeout=10,
|
|
117
|
+
)
|
|
118
|
+
except requests.exceptions.ConnectionError as exc:
|
|
119
|
+
raise HTTPException(
|
|
120
|
+
status_code=500,
|
|
121
|
+
detail=f"{exc.request.url} cannot be reached",
|
|
122
|
+
) from exc
|
|
123
|
+
|
|
124
|
+
if response.ok:
|
|
125
|
+
response = json.loads(response.text)
|
|
126
|
+
else:
|
|
127
|
+
raise HTTPException(status_code=400, detail="Incorrect username or password")
|
|
128
|
+
|
|
129
|
+
return JSONResponse(
|
|
130
|
+
{
|
|
131
|
+
"access_token": response["ticket"],
|
|
132
|
+
"token_type": "bearer",
|
|
133
|
+
"userId": response["userId"],
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@router.get("/users/me", tags=["auth"])
|
|
139
|
+
async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]) -> JSONResponse:
|
|
140
|
+
"""Get the current user.
|
|
141
|
+
|
|
142
|
+
current_user:
|
|
143
|
+
type: User
|
|
144
|
+
description: The current user.
|
|
145
|
+
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
if "otadmins@otds.admin" in current_user.groups:
|
|
149
|
+
return JSONResponse(current_user.model_dump())
|
|
150
|
+
else:
|
|
151
|
+
raise HTTPException(
|
|
152
|
+
status_code=403,
|
|
153
|
+
detail=f"User {current_user.id} is not authorized",
|
|
154
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Metics for payload logs."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from prometheus_client import Gauge
|
|
6
|
+
from prometheus_fastapi_instrumentator.metrics import Info
|
|
7
|
+
|
|
8
|
+
from pyxecm.customizer.api.payload_list import PayloadList
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## By Payload
|
|
12
|
+
def payload_logs_by_payload(payload_list: PayloadList) -> Callable[[Info], None]:
|
|
13
|
+
"""Metrics for payload logs by payload."""
|
|
14
|
+
|
|
15
|
+
metrics_error = Gauge(
|
|
16
|
+
"payload_error",
|
|
17
|
+
"Number of ERROR log messages for by payload",
|
|
18
|
+
labelnames=("index", "name", "logfile"),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
metrics_warning = Gauge(
|
|
22
|
+
"payload_warning",
|
|
23
|
+
"Number of WARNING log messages for by payload",
|
|
24
|
+
labelnames=("index", "name", "logfile"),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
metrics_info = Gauge(
|
|
28
|
+
"payload_info",
|
|
29
|
+
"Number of INFO log messages for by payload",
|
|
30
|
+
labelnames=("index", "name", "logfile"),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
metrics_debug = Gauge(
|
|
34
|
+
"payload_debug",
|
|
35
|
+
"Number of DEBUG log messages for by payload",
|
|
36
|
+
labelnames=("index", "name", "logfile"),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def instrumentation(info: Info) -> None: # noqa: ARG001
|
|
40
|
+
df = payload_list.get_payload_items()
|
|
41
|
+
data = [{"index": idx, **row} for idx, row in df.iterrows()]
|
|
42
|
+
|
|
43
|
+
for item in data:
|
|
44
|
+
metrics_error.labels(item["index"], item["name"], item["logfile"]).set(
|
|
45
|
+
item["log_error"],
|
|
46
|
+
)
|
|
47
|
+
metrics_warning.labels(item["index"], item["name"], item["logfile"]).set(
|
|
48
|
+
item["log_warning"],
|
|
49
|
+
)
|
|
50
|
+
metrics_info.labels(item["index"], item["name"], item["logfile"]).set(
|
|
51
|
+
item["log_info"],
|
|
52
|
+
)
|
|
53
|
+
metrics_debug.labels(item["index"], item["name"], item["logfile"]).set(
|
|
54
|
+
item["log_debug"],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return instrumentation
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
## Total
|
|
61
|
+
def payload_logs_total(payload_list: PayloadList) -> Callable[[Info], None]:
|
|
62
|
+
"""Metrics for total payload logs messages."""
|
|
63
|
+
|
|
64
|
+
metrics_error = Gauge(
|
|
65
|
+
"payload_error_total",
|
|
66
|
+
"Total number of ERROR log messages",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
metrics_warning = Gauge(
|
|
70
|
+
"payload_warning_total",
|
|
71
|
+
"Total number of WARNING log messages",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
metrics_info = Gauge(
|
|
75
|
+
"payload_info_total",
|
|
76
|
+
"Total number of INFO log messages",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
metrics_debug = Gauge(
|
|
80
|
+
"payload_debug_total",
|
|
81
|
+
"Total number of DEBUG log messages",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def instrumentation(info: Info) -> None: # noqa: ARG001
|
|
85
|
+
df = payload_list.get_payload_items()
|
|
86
|
+
|
|
87
|
+
metrics_error.set(df["log_error"].sum())
|
|
88
|
+
metrics_warning.set(df["log_warning"].sum())
|
|
89
|
+
metrics_info.set(df["log_info"].sum())
|
|
90
|
+
metrics_debug.set(df["log_debug"].sum())
|
|
91
|
+
|
|
92
|
+
return instrumentation
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Models for FastAPI."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class User(BaseModel):
|
|
7
|
+
"""Model for users authenticated by OTDS."""
|
|
8
|
+
|
|
9
|
+
id: str
|
|
10
|
+
full_name: str | None = None
|
|
11
|
+
groups: list[str] | None = None
|
|
12
|
+
is_admin: bool = False
|
|
13
|
+
is_sysadmin: bool = False
|