pyxecm 2.0.3__py3-none-any.whl → 3.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/coreshare.py +76 -8
- pyxecm/helper/data.py +16 -24
- pyxecm/helper/otel_config.py +26 -0
- pyxecm/helper/web.py +1 -2
- pyxecm/otca.py +1356 -16
- pyxecm/otcs.py +4238 -758
- pyxecm/otds.py +4 -12
- pyxecm/otmm.py +4 -5
- pyxecm/py.typed +0 -0
- pyxecm-3.0.0.dist-info/METADATA +48 -0
- pyxecm-3.0.0.dist-info/RECORD +96 -0
- {pyxecm-2.0.3.dist-info → pyxecm-3.0.0.dist-info}/WHEEL +1 -2
- pyxecm-3.0.0.dist-info/entry_points.txt +4 -0
- {pyxecm/customizer/api → pyxecm_api}/__main__.py +1 -1
- pyxecm_api/agents/__init__.py +7 -0
- pyxecm_api/agents/app.py +13 -0
- pyxecm_api/agents/functions.py +119 -0
- pyxecm_api/agents/models.py +10 -0
- pyxecm_api/agents/otcm_knowledgegraph/functions.py +85 -0
- pyxecm_api/agents/otcm_knowledgegraph/models.py +61 -0
- pyxecm_api/agents/otcm_knowledgegraph/router.py +74 -0
- pyxecm_api/agents/otcm_user_agent/models.py +20 -0
- pyxecm_api/agents/otcm_user_agent/router.py +65 -0
- pyxecm_api/agents/otcm_workspace_agent/models.py +40 -0
- pyxecm_api/agents/otcm_workspace_agent/router.py +200 -0
- pyxecm_api/app.py +221 -0
- {pyxecm/customizer/api → pyxecm_api}/auth/functions.py +10 -2
- {pyxecm/customizer/api → pyxecm_api}/auth/router.py +4 -3
- {pyxecm/customizer/api → pyxecm_api}/common/functions.py +39 -9
- {pyxecm/customizer/api → pyxecm_api}/common/metrics.py +1 -2
- {pyxecm/customizer/api → pyxecm_api}/common/router.py +12 -11
- {pyxecm/customizer/api → pyxecm_api}/settings.py +30 -6
- {pyxecm/customizer/api → pyxecm_api}/terminal/router.py +1 -1
- {pyxecm/customizer/api → pyxecm_api}/v1_csai/router.py +39 -10
- pyxecm_api/v1_csai/statics/bindings/utils.js +189 -0
- pyxecm_api/v1_csai/statics/tom-select/tom-select.complete.min.js +356 -0
- pyxecm_api/v1_csai/statics/tom-select/tom-select.css +334 -0
- pyxecm_api/v1_csai/statics/vis-9.1.2/vis-network.css +1 -0
- pyxecm_api/v1_csai/statics/vis-9.1.2/vis-network.min.js +27 -0
- pyxecm_api/v1_maintenance/__init__.py +1 -0
- {pyxecm/customizer/api → pyxecm_api}/v1_maintenance/functions.py +3 -3
- {pyxecm/customizer/api → pyxecm_api}/v1_maintenance/router.py +8 -8
- pyxecm_api/v1_otcs/__init__.py +1 -0
- {pyxecm/customizer/api → pyxecm_api}/v1_otcs/functions.py +7 -5
- {pyxecm/customizer/api → pyxecm_api}/v1_otcs/router.py +24 -13
- pyxecm_api/v1_payload/__init__.py +1 -0
- {pyxecm/customizer/api → pyxecm_api}/v1_payload/functions.py +10 -7
- {pyxecm/customizer/api → pyxecm_api}/v1_payload/router.py +11 -10
- {pyxecm/customizer → pyxecm_customizer}/__init__.py +8 -0
- {pyxecm/customizer → pyxecm_customizer}/__main__.py +15 -21
- {pyxecm/customizer → pyxecm_customizer}/browser_automation.py +414 -103
- {pyxecm/customizer → pyxecm_customizer}/customizer.py +178 -116
- {pyxecm/customizer → pyxecm_customizer}/guidewire.py +60 -20
- {pyxecm/customizer → pyxecm_customizer}/k8s.py +4 -4
- pyxecm_customizer/knowledge_graph.py +719 -0
- pyxecm_customizer/log.py +35 -0
- {pyxecm/customizer → pyxecm_customizer}/m365.py +41 -33
- {pyxecm/customizer → pyxecm_customizer}/payload.py +2359 -1991
- {pyxecm/customizer/api/common → pyxecm_customizer}/payload_list.py +57 -65
- {pyxecm/customizer → pyxecm_customizer}/salesforce.py +1 -1
- {pyxecm/customizer → pyxecm_customizer}/sap.py +6 -2
- {pyxecm/customizer → pyxecm_customizer}/servicenow.py +2 -4
- {pyxecm/customizer → pyxecm_customizer}/settings.py +7 -6
- {pyxecm/customizer → pyxecm_customizer}/successfactors.py +40 -28
- {pyxecm/customizer → pyxecm_customizer}/translate.py +14 -10
- {pyxecm/maintenance_page → pyxecm_maintenance_page}/__main__.py +1 -1
- {pyxecm/maintenance_page → pyxecm_maintenance_page}/app.py +16 -6
- pyxecm/customizer/api/app.py +0 -163
- pyxecm/customizer/log.py +0 -107
- pyxecm/customizer/nhc.py +0 -1169
- pyxecm/customizer/openapi.py +0 -258
- pyxecm/customizer/pht.py +0 -1357
- pyxecm-2.0.3.dist-info/METADATA +0 -119
- pyxecm-2.0.3.dist-info/RECORD +0 -78
- pyxecm-2.0.3.dist-info/licenses/LICENSE +0 -202
- pyxecm-2.0.3.dist-info/top_level.txt +0 -1
- {pyxecm/customizer/api → pyxecm_api}/__init__.py +0 -0
- {pyxecm/customizer/api/auth → pyxecm_api/agents/otcm_knowledgegraph}/__init__.py +0 -0
- {pyxecm/customizer/api/common → pyxecm_api/agents/otcm_user_agent}/__init__.py +0 -0
- {pyxecm/customizer/api/v1_csai → pyxecm_api/agents/otcm_workspace_agent}/__init__.py +0 -0
- {pyxecm/customizer/api/v1_maintenance → pyxecm_api/auth}/__init__.py +0 -0
- {pyxecm/customizer/api → pyxecm_api}/auth/models.py +0 -0
- {pyxecm/customizer/api/v1_otcs → pyxecm_api/common}/__init__.py +0 -0
- {pyxecm/customizer/api → pyxecm_api}/common/models.py +0 -0
- {pyxecm/customizer/api → pyxecm_api}/terminal/__init__.py +0 -0
- {pyxecm/customizer/api/v1_payload → pyxecm_api/v1_csai}/__init__.py +0 -0
- {pyxecm/customizer/api → pyxecm_api}/v1_csai/models.py +0 -0
- {pyxecm/customizer/api → pyxecm_api}/v1_maintenance/models.py +0 -0
- {pyxecm/customizer/api → pyxecm_api}/v1_payload/models.py +0 -0
- {pyxecm/customizer → pyxecm_customizer}/exceptions.py +0 -0
- {pyxecm/maintenance_page → pyxecm_maintenance_page}/__init__.py +0 -0
- {pyxecm/maintenance_page → pyxecm_maintenance_page}/settings.py +0 -0
- {pyxecm/maintenance_page → pyxecm_maintenance_page}/static/favicon.avif +0 -0
- {pyxecm/maintenance_page → pyxecm_maintenance_page}/templates/maintenance.html +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Define router for User endpoints."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Body, Depends, HTTPException, status
|
|
7
|
+
from pyxecm.otcs import OTCS
|
|
8
|
+
|
|
9
|
+
from pyxecm_api.common.functions import get_otcs_object_from_otcsticket
|
|
10
|
+
|
|
11
|
+
from .models import UserModel
|
|
12
|
+
|
|
13
|
+
router = APIRouter(prefix="/otcm_user_agent", tags=["csai agents"])
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("pyxecm_api.agents.otcm_user_agent")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.post(
|
|
19
|
+
"/user",
|
|
20
|
+
summary="Find a user by name or user attributes",
|
|
21
|
+
responses={
|
|
22
|
+
200: {"description": "User found"},
|
|
23
|
+
403: {"description": "Invalid credentials"},
|
|
24
|
+
400: {"description": "User not found"},
|
|
25
|
+
},
|
|
26
|
+
response_model=UserModel,
|
|
27
|
+
response_description="Details about a user. Best presented as a table with each user property in a row.",
|
|
28
|
+
)
|
|
29
|
+
def otcm_user_agent_find_user(
|
|
30
|
+
otcs: Annotated[OTCS, Depends(get_otcs_object_from_otcsticket)],
|
|
31
|
+
user: Annotated[UserModel, Body()],
|
|
32
|
+
) -> UserModel | None:
|
|
33
|
+
"""Find a user by by its name or other attributes.
|
|
34
|
+
|
|
35
|
+
The user ID, the user name (login), user first name, user last name and user email will be returned.
|
|
36
|
+
When a user is returned, display the user data as a markdown table.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
response = otcs.get_users(
|
|
40
|
+
where_name=user.login,
|
|
41
|
+
where_first_name=user.first_name,
|
|
42
|
+
where_last_name=user.last_name,
|
|
43
|
+
where_business_email=user.email,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if not response or not response["results"]:
|
|
47
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
department_id = otcs.get_result_value(response=response, key="group_id")
|
|
51
|
+
department = otcs.get_group(group_id=department_id) if department_id else None
|
|
52
|
+
department_name = otcs.get_result_value(response=department, key="name") if department else None
|
|
53
|
+
return UserModel(
|
|
54
|
+
user_id=otcs.get_result_value(response=response, key="id"),
|
|
55
|
+
login=otcs.get_result_value(response=response, key="name"),
|
|
56
|
+
first_name=otcs.get_result_value(response=response, key="first_name"),
|
|
57
|
+
last_name=otcs.get_result_value(response=response, key="last_name"),
|
|
58
|
+
email=otcs.get_result_value(response=response, key="business_email"),
|
|
59
|
+
department=department_name,
|
|
60
|
+
)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") from e
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# end function definition
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Define Models for workspaces."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WorkspaceAttributeModel(BaseModel):
|
|
9
|
+
"""Model for a workspace attribute."""
|
|
10
|
+
|
|
11
|
+
category: Annotated[str, Field(description="Category for the workspace attributes")]
|
|
12
|
+
attribute: Annotated[str, Field(description="Name of the attribute")]
|
|
13
|
+
value: Annotated[str, Field(description="Value of the attribute")]
|
|
14
|
+
set: Annotated[str, Field(description="Optional attribute group or set name")]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WorkspaceModel(BaseModel):
|
|
18
|
+
"""Defines Model for describing workspaces in OTCM (Opentext Content Management).
|
|
19
|
+
|
|
20
|
+
To display an instance of this model, please display the link.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
id: Annotated[int, Field(description="ID of the workspace")] = None
|
|
24
|
+
name: Annotated[str, Field(description="Name of the workspace")] = None
|
|
25
|
+
type: Annotated[str, Field(description="Name of the workspace type")] = None
|
|
26
|
+
description: Annotated[str, Field(description="Description of the workspace")] = None
|
|
27
|
+
|
|
28
|
+
link: Annotated[str, Field(description="Link to the workspace, should be used when the instance is displayed")] = (
|
|
29
|
+
None
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# attributes: Annotated[
|
|
33
|
+
# list[WorkspaceAttributeModel] | None,
|
|
34
|
+
# Body(description="List of custom attributes associated with the workspace"),
|
|
35
|
+
# ] = None
|
|
36
|
+
|
|
37
|
+
def model_post_init(self, context: Any) -> None: # noqa: ARG002, ANN401
|
|
38
|
+
"""Post model initialization."""
|
|
39
|
+
# this could also be done with `default_factory`:
|
|
40
|
+
self.link = f"[{self.name}](/cs/cs/app/nodes/{self.id})"
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Define router for workspace endpoints."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Body, Depends, HTTPException, status
|
|
7
|
+
from pyxecm.otcs import OTCS
|
|
8
|
+
|
|
9
|
+
from pyxecm_api.agents.models import Context
|
|
10
|
+
from pyxecm_api.common.functions import get_otcs_object_from_otcsticket
|
|
11
|
+
|
|
12
|
+
from .models import WorkspaceModel
|
|
13
|
+
|
|
14
|
+
router = APIRouter(prefix="/otcm_workspace_agent", tags=["csai agents"])
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("pyxecm_api.agents.otcm_workspace_agent")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.post(
|
|
20
|
+
path="/find_workspace",
|
|
21
|
+
response_model=WorkspaceModel,
|
|
22
|
+
summary="Find the markdown link to a workspace by workspace name and workspace type and display the link.",
|
|
23
|
+
responses={
|
|
24
|
+
200: {"description": "Workspace found"},
|
|
25
|
+
403: {"description": "Invalid credentials"},
|
|
26
|
+
404: {"description": "Workspace not found"},
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
def otcm_workspace_agent_find_workspace(
|
|
30
|
+
context: Context,
|
|
31
|
+
otcs: Annotated[OTCS, Depends(get_otcs_object_from_otcsticket)],
|
|
32
|
+
workspace: Annotated[WorkspaceModel, Body()],
|
|
33
|
+
) -> WorkspaceModel | None:
|
|
34
|
+
"""Find a workspace by workspace name and workspace type.
|
|
35
|
+
|
|
36
|
+
The returned workspace is an OTCS workspace object. Show the markdown link in the chat response, sothat the user can click on it.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
logger.info("Got context -> %s", context)
|
|
40
|
+
|
|
41
|
+
response = otcs.get_workspace_by_type_and_name(type_name=workspace.type, name="contains_" + workspace.name)
|
|
42
|
+
|
|
43
|
+
if response and len(response["results"]) == 1:
|
|
44
|
+
result = WorkspaceModel(
|
|
45
|
+
id=otcs.get_result_value(response=response, key="id"),
|
|
46
|
+
name=otcs.get_result_value(response=response, key="name"),
|
|
47
|
+
type=workspace.type,
|
|
48
|
+
)
|
|
49
|
+
logger.info("Workspace found -> %s", result)
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
if response is None or not response["results"]:
|
|
53
|
+
response = otcs.search(search_term=workspace.name)
|
|
54
|
+
|
|
55
|
+
if response is None or not response["results"]:
|
|
56
|
+
raise HTTPException(
|
|
57
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
58
|
+
detail="Workspace not found.",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
return WorkspaceModel(
|
|
63
|
+
id=otcs.get_result_value(response=response, key="id"),
|
|
64
|
+
name=otcs.get_result_value(response=response, key="name"),
|
|
65
|
+
type=workspace.type,
|
|
66
|
+
)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
raise HTTPException(
|
|
69
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
70
|
+
detail="Workspace not found.",
|
|
71
|
+
) from e
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# end function definition
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@router.post(
|
|
78
|
+
path="/lookup_workspace",
|
|
79
|
+
response_model=WorkspaceModel,
|
|
80
|
+
summary="Lookup a workspace based on its type and a value of one of the workspace attributes.",
|
|
81
|
+
responses={
|
|
82
|
+
200: {"description": "Workspace found"},
|
|
83
|
+
403: {"description": "Invalid credentials"},
|
|
84
|
+
400: {"description": "Workspace not found"},
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
def otcm_workspace_agent_lookup_workspace(
|
|
88
|
+
otcs: Annotated[OTCS, Depends(get_otcs_object_from_otcsticket)],
|
|
89
|
+
workspace: Annotated[WorkspaceModel, Body()],
|
|
90
|
+
) -> WorkspaceModel | None:
|
|
91
|
+
"""Lookup a workspace based on its type and a value of one of the workspace attributes.
|
|
92
|
+
|
|
93
|
+
Use this tool if the workspace name is _not_ specified but the user asks for a specific
|
|
94
|
+
workspace attribute value like cities, products, or other attributes.
|
|
95
|
+
|
|
96
|
+
Return the workspace data if it is found. If it is not found confirm with the user if the workspace should be created or not.
|
|
97
|
+
If it should be created call the tool: otcm_workspace_agent_create_workspace
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
# otcs._otcs_ticket = otcsticket
|
|
101
|
+
|
|
102
|
+
workspace_attributes = workspace.attributes or {}
|
|
103
|
+
if not workspace_attributes:
|
|
104
|
+
raise HTTPException(
|
|
105
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
106
|
+
detail="Workspace attributes must be provided for lookup.",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
category = next((attr.category for attr in workspace_attributes if attr.category), None)
|
|
110
|
+
attribute = next((attr.attribute for attr in workspace_attributes if attr.attribute), None)
|
|
111
|
+
attribute_set = next((attr.set for attr in workspace_attributes if attr.set), None)
|
|
112
|
+
value = next((attr.value for attr in workspace_attributes if attr.value), None)
|
|
113
|
+
|
|
114
|
+
response = otcs.lookup_workspace(
|
|
115
|
+
type_name=workspace.type, category=category, attribute=attribute, attribute_set=attribute_set, value=value
|
|
116
|
+
)
|
|
117
|
+
if response is None or not response["results"]:
|
|
118
|
+
raise HTTPException(
|
|
119
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
120
|
+
detail="No workspace of type -> '{}' with attribute -> '{}'{} and value -> '{}' found!".format(
|
|
121
|
+
workspace.type, attribute, " (set -> {})".format(attribute_set), value
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
return WorkspaceModel(
|
|
127
|
+
id=otcs.get_result_value(response=response, key="id"),
|
|
128
|
+
name=otcs.get_result_value(response=response, key="name"),
|
|
129
|
+
type=workspace.type,
|
|
130
|
+
)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workspace not found") from e
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# end function definition
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@router.post(
|
|
139
|
+
path="/create_workspace",
|
|
140
|
+
response_model=WorkspaceModel,
|
|
141
|
+
summary="Create a workspace with given workspace name and workspace type and display the link.",
|
|
142
|
+
responses={
|
|
143
|
+
200: {"description": "Workspace created"},
|
|
144
|
+
403: {"description": "Invalid credentials"},
|
|
145
|
+
404: {"description": "Workspace creation failed"},
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
def otcm_workspace_agent_create_workspace(
|
|
149
|
+
otcs: Annotated[OTCS, Depends(get_otcs_object_from_otcsticket)],
|
|
150
|
+
workspace: Annotated[WorkspaceModel, Body()],
|
|
151
|
+
) -> WorkspaceModel | None:
|
|
152
|
+
"""Create a workspace with given workspace name and workspace type.
|
|
153
|
+
|
|
154
|
+
The ID of the created workspace, the final workspace name, and the workspace type will be returned.
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
workspace_type_id, workspace_templates = otcs.get_workspace_templates(type_name=workspace.type)
|
|
159
|
+
if workspace_type_id is None:
|
|
160
|
+
raise HTTPException(
|
|
161
|
+
status_code=status.HTTP_404_NOT_FOUND, detail="Workspace type -> '{}' not found!".format(workspace.type)
|
|
162
|
+
)
|
|
163
|
+
if not workspace_templates:
|
|
164
|
+
raise HTTPException(
|
|
165
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
166
|
+
detail="Workspace type -> '{}' has no templates!".format(workspace.type),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
workspace_template_id = workspace_templates[0].get("id")
|
|
170
|
+
|
|
171
|
+
response = otcs.create_workspace(
|
|
172
|
+
workspace_template_id=workspace_template_id,
|
|
173
|
+
workspace_type=workspace_type_id,
|
|
174
|
+
workspace_name=workspace.name,
|
|
175
|
+
workspace_description="",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if response is None or not response["results"]:
|
|
179
|
+
raise HTTPException(
|
|
180
|
+
status_code=status.HTTP_404_NOT_FOUND, detail="Failed to create workspace -> '{}'".format(workspace.name)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
workspace_id = response.get("results", {}).get("id")
|
|
185
|
+
|
|
186
|
+
# The resulting workspace name can be different from the given name,
|
|
187
|
+
# so we need to fetch the workspace details again to get the final name.
|
|
188
|
+
response = otcs.get_workspace(node_id=workspace_id)
|
|
189
|
+
workspace_name = otcs.get_result_value(response=response, key="name")
|
|
190
|
+
|
|
191
|
+
return WorkspaceModel(
|
|
192
|
+
id=workspace_id,
|
|
193
|
+
name=workspace_name,
|
|
194
|
+
type=workspace.type,
|
|
195
|
+
)
|
|
196
|
+
except Exception as e:
|
|
197
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workspace not found") from e
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# end function definition
|
pyxecm_api/app.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
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 logging
|
|
10
|
+
import os
|
|
11
|
+
import threading
|
|
12
|
+
from collections.abc import AsyncGenerator
|
|
13
|
+
from contextlib import asynccontextmanager
|
|
14
|
+
from datetime import UTC, datetime
|
|
15
|
+
from importlib.metadata import version
|
|
16
|
+
|
|
17
|
+
import uvicorn
|
|
18
|
+
from fastapi import FastAPI
|
|
19
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
20
|
+
from fastapi.staticfiles import StaticFiles
|
|
21
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
22
|
+
from prometheus_fastapi_instrumentator import Instrumentator
|
|
23
|
+
from pyxecm.helper.otel_config import tracer
|
|
24
|
+
from pyxecm_maintenance_page import run_maintenance_page
|
|
25
|
+
|
|
26
|
+
from .agents.app import agent_routers
|
|
27
|
+
from .agents.functions import register_all
|
|
28
|
+
from .agents.otcm_knowledgegraph.functions import build_graph
|
|
29
|
+
from .auth.router import router as auth_router
|
|
30
|
+
from .common.functions import PAYLOAD_LIST
|
|
31
|
+
from .common.metrics import payload_logs_by_payload, payload_logs_total
|
|
32
|
+
from .common.router import router as common_router
|
|
33
|
+
from .settings import api_settings
|
|
34
|
+
from .terminal.router import router as terminal_router
|
|
35
|
+
from .v1_csai.router import router as v1_csai_router
|
|
36
|
+
from .v1_maintenance.router import router as v1_maintenance_router
|
|
37
|
+
from .v1_otcs.router import router as v1_otcs_router
|
|
38
|
+
from .v1_payload.functions import import_payload
|
|
39
|
+
from .v1_payload.router import router as v1_payload_router
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger("CustomizerAPI")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Check if Logfile and folder exists and is unique
|
|
45
|
+
if os.path.isfile(os.path.join(api_settings.logfolder, api_settings.logfile)):
|
|
46
|
+
customizer_start_time = datetime.now(UTC).strftime(
|
|
47
|
+
"%Y-%m-%d_%H-%M",
|
|
48
|
+
)
|
|
49
|
+
api_settings.logfile = f"customizer_{customizer_start_time}.log"
|
|
50
|
+
elif not os.path.exists(api_settings.logfolder):
|
|
51
|
+
os.makedirs(api_settings.logfolder)
|
|
52
|
+
|
|
53
|
+
handlers = [logging.FileHandler(os.path.join(api_settings.logfolder, api_settings.logfile))]
|
|
54
|
+
if api_settings.log_payload_processing:
|
|
55
|
+
handlers.append(logging.StreamHandler())
|
|
56
|
+
|
|
57
|
+
logging.basicConfig(
|
|
58
|
+
format="%(asctime)s %(levelname)s [%(name)s] [%(threadName)s] %(message)s",
|
|
59
|
+
datefmt="%d-%b-%Y %H:%M:%S",
|
|
60
|
+
level=api_settings.loglevel,
|
|
61
|
+
handlers=handlers,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@asynccontextmanager
|
|
66
|
+
async def lifespan(
|
|
67
|
+
app: FastAPI, # noqa: ARG001
|
|
68
|
+
) -> AsyncGenerator:
|
|
69
|
+
"""Lifespan Method for FASTAPI to handle the startup and shutdown process.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
app (FastAPI):
|
|
73
|
+
The application.
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
logger.debug("Settings -> %s", api_settings)
|
|
78
|
+
|
|
79
|
+
with tracer.start_as_current_span("import_payloads"):
|
|
80
|
+
if api_settings.import_payload:
|
|
81
|
+
logger.info("Importing filesystem payloads...")
|
|
82
|
+
|
|
83
|
+
# Base Payload
|
|
84
|
+
import_payload(payload=api_settings.payload)
|
|
85
|
+
|
|
86
|
+
# External Payload
|
|
87
|
+
import_payload(payload_dir=api_settings.payload_dir, dependencies=True)
|
|
88
|
+
|
|
89
|
+
# Optional Payload
|
|
90
|
+
import_payload(payload_dir=api_settings.payload_dir_optional)
|
|
91
|
+
|
|
92
|
+
logger.info("Starting maintenance_page thread...")
|
|
93
|
+
if api_settings.maintenance_page:
|
|
94
|
+
run_maintenance_page()
|
|
95
|
+
|
|
96
|
+
if api_settings.csai_studio_integration:
|
|
97
|
+
logger.info("Registering Content Aviator tools...")
|
|
98
|
+
register_all()
|
|
99
|
+
threading.Thread(name="KnowledgeGraph", target=build_graph).start()
|
|
100
|
+
|
|
101
|
+
yield
|
|
102
|
+
logger.info("Shutdown")
|
|
103
|
+
PAYLOAD_LIST.stop_payload_processing()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
app = FastAPI(
|
|
107
|
+
docs_url="/api",
|
|
108
|
+
title=api_settings.title,
|
|
109
|
+
description=api_settings.description,
|
|
110
|
+
openapi_url=api_settings.openapi_url,
|
|
111
|
+
root_path=api_settings.root_path,
|
|
112
|
+
lifespan=lifespan,
|
|
113
|
+
version=version("pyxecm"),
|
|
114
|
+
openapi_tags=[
|
|
115
|
+
{
|
|
116
|
+
"name": "auth",
|
|
117
|
+
"description": "Authentication Endpoint - Users are authenticated against Opentext Directory Services",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "payload",
|
|
121
|
+
"description": "Get status and manipulate payload objects ",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"name": "maintenance",
|
|
125
|
+
"description": "Enable, disable or alter the maintenance mode.",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
FastAPIInstrumentor.instrument_app(app)
|
|
131
|
+
|
|
132
|
+
## Add Middlewares
|
|
133
|
+
app.add_middleware(
|
|
134
|
+
CORSMiddleware,
|
|
135
|
+
allow_origins=api_settings.trusted_origins,
|
|
136
|
+
allow_credentials=True,
|
|
137
|
+
allow_methods=["*"],
|
|
138
|
+
allow_headers=["*"],
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
## Add all Routers
|
|
142
|
+
app.include_router(router=common_router)
|
|
143
|
+
app.include_router(router=auth_router)
|
|
144
|
+
app.include_router(router=v1_maintenance_router)
|
|
145
|
+
app.include_router(router=v1_otcs_router)
|
|
146
|
+
app.include_router(router=v1_payload_router)
|
|
147
|
+
|
|
148
|
+
if api_settings.ws_terminal:
|
|
149
|
+
app.include_router(router=terminal_router)
|
|
150
|
+
|
|
151
|
+
if api_settings.csai:
|
|
152
|
+
app.mount(
|
|
153
|
+
"/api/v1/csai/lib",
|
|
154
|
+
StaticFiles(packages=["pyxecm_api.v1_csai"]),
|
|
155
|
+
name="csai-statics",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
app.include_router(router=v1_csai_router)
|
|
159
|
+
for agent_router in agent_routers:
|
|
160
|
+
app.include_router(prefix="/agents", router=agent_router)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
## Add Prometheus Instrumentator for /metrics,
|
|
164
|
+
if api_settings.metrics:
|
|
165
|
+
# Add Prometheus Instrumentator for /metricsf
|
|
166
|
+
instrumentator = Instrumentator().instrument(app).expose(app)
|
|
167
|
+
instrumentator.add(payload_logs_by_payload(PAYLOAD_LIST))
|
|
168
|
+
instrumentator.add(payload_logs_total(PAYLOAD_LIST))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
## Start the API Server
|
|
172
|
+
def run_api() -> None:
|
|
173
|
+
"""Start the FASTAPI Webserver."""
|
|
174
|
+
|
|
175
|
+
# Check if Temp and Log dir exists
|
|
176
|
+
if not os.path.exists(api_settings.temp_dir):
|
|
177
|
+
os.makedirs(api_settings.temp_dir)
|
|
178
|
+
if not os.path.exists(api_settings.logfolder):
|
|
179
|
+
os.makedirs(api_settings.logfolder)
|
|
180
|
+
|
|
181
|
+
# Check if Logfile and exists and is unique
|
|
182
|
+
if os.path.isfile(os.path.join(api_settings.logfolder, api_settings.logfile)):
|
|
183
|
+
customizer_start_time = datetime.now(UTC).strftime(
|
|
184
|
+
"%Y-%m-%d_%H-%M",
|
|
185
|
+
)
|
|
186
|
+
api_settings.logfile = f"customizer_{customizer_start_time}.log"
|
|
187
|
+
|
|
188
|
+
# Configure Logging for uvicorn
|
|
189
|
+
log_config = uvicorn.config.LOGGING_CONFIG
|
|
190
|
+
|
|
191
|
+
# Stdout
|
|
192
|
+
log_config["formatters"]["pyxecm"] = {
|
|
193
|
+
"()": "uvicorn.logging.DefaultFormatter",
|
|
194
|
+
"fmt": "%(levelprefix)s [%(name)s] [%(threadName)s] %(message)s",
|
|
195
|
+
"use_colors": True,
|
|
196
|
+
}
|
|
197
|
+
log_config["handlers"]["pyxecm"] = {
|
|
198
|
+
"formatter": "pyxecm",
|
|
199
|
+
"class": "logging.StreamHandler",
|
|
200
|
+
"stream": "ext://sys.stdout",
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
log_config["loggers"]["pyxecm"] = {
|
|
204
|
+
"handlers": ["pyxecm"],
|
|
205
|
+
"level": api_settings.loglevel,
|
|
206
|
+
"propagate": False,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
logger.info("Starting processing thread...")
|
|
210
|
+
PAYLOAD_LIST.run_payload_processing(concurrent=api_settings.concurrent_payloads)
|
|
211
|
+
|
|
212
|
+
uvicorn.run(
|
|
213
|
+
"pyxecm_api:app",
|
|
214
|
+
host=api_settings.bind_address,
|
|
215
|
+
port=api_settings.bind_port,
|
|
216
|
+
workers=api_settings.workers,
|
|
217
|
+
reload=api_settings.reload,
|
|
218
|
+
proxy_headers=True,
|
|
219
|
+
forwarded_allow_ips="*",
|
|
220
|
+
log_config=log_config,
|
|
221
|
+
)
|
|
@@ -7,8 +7,9 @@ import requests
|
|
|
7
7
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
8
8
|
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
|
|
10
|
+
from pyxecm_api.settings import api_settings
|
|
11
|
+
|
|
12
|
+
from .models import User
|
|
12
13
|
|
|
13
14
|
router = APIRouter()
|
|
14
15
|
|
|
@@ -19,6 +20,7 @@ oauth2_scheme = OAuth2PasswordBearer(
|
|
|
19
20
|
scheme_name="OTDS Authentication",
|
|
20
21
|
)
|
|
21
22
|
apikey_header = APIKeyHeader(name="x-api-key", auto_error=False, scheme_name="APIKey")
|
|
23
|
+
otcsticket = APIKeyHeader(name="otcsticket", auto_error=True, scheme_name="OTCSTicket")
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
def get_groups(response: dict, token: str) -> list:
|
|
@@ -97,3 +99,9 @@ async def get_authorized_user(current_user: Annotated[User, Depends(get_current_
|
|
|
97
99
|
detail=f"User {current_user.id} is not authorized",
|
|
98
100
|
)
|
|
99
101
|
return current_user
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def get_otcsticket(otcsticket: Annotated[str, Depends(otcsticket)]) -> User:
|
|
105
|
+
"""Check if the user is authorized (member of the Group otadmin@otds.admin)."""
|
|
106
|
+
|
|
107
|
+
return otcsticket
|
|
@@ -8,9 +8,10 @@ from fastapi import APIRouter, Depends, HTTPException
|
|
|
8
8
|
from fastapi.responses import JSONResponse
|
|
9
9
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|
10
10
|
|
|
11
|
-
from
|
|
12
|
-
|
|
13
|
-
from
|
|
11
|
+
from pyxecm_api.settings import api_settings
|
|
12
|
+
|
|
13
|
+
from .functions import get_current_user
|
|
14
|
+
from .models import User
|
|
14
15
|
|
|
15
16
|
router = APIRouter()
|
|
16
17
|
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
from typing import Annotated
|
|
5
6
|
|
|
6
|
-
from
|
|
7
|
-
from pyxecm.customizer.api.settings import CustomizerAPISettings, api_settings
|
|
8
|
-
from pyxecm.customizer.k8s import K8s
|
|
9
|
-
from pyxecm.customizer.settings import Settings
|
|
7
|
+
from fastapi import Depends
|
|
10
8
|
from pyxecm.otcs import OTCS
|
|
9
|
+
from pyxecm_customizer import K8s, PayloadList, Settings
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
from pyxecm_api.auth.functions import get_otcsticket
|
|
12
|
+
from pyxecm_api.settings import CustomizerAPISettings, api_settings
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("pyxecm_api")
|
|
13
15
|
|
|
14
16
|
# Create a LOCK dict for singleton logs collection
|
|
15
17
|
LOGS_LOCK = {}
|
|
@@ -37,7 +39,7 @@ def get_otcs_object() -> OTCS:
|
|
|
37
39
|
"""
|
|
38
40
|
settings = Settings()
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
otcs = OTCS(
|
|
41
43
|
protocol=settings.otcs.url_backend.scheme,
|
|
42
44
|
hostname=settings.otcs.url_backend.host,
|
|
43
45
|
port=settings.otcs.url_backend.port,
|
|
@@ -50,12 +52,40 @@ def get_otcs_object() -> OTCS:
|
|
|
50
52
|
support_path=settings.otcs.support_path,
|
|
51
53
|
download_dir=settings.otcs.download_dir,
|
|
52
54
|
feme_uri=settings.otcs.feme_uri,
|
|
53
|
-
logger=logger,
|
|
55
|
+
logger=logger.getChild("otcs"),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
otcs.authenticate()
|
|
59
|
+
|
|
60
|
+
return otcs
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_otcs_object_from_otcsticket(otcsticket: Annotated[str, Depends(get_otcsticket)]) -> OTCS:
|
|
64
|
+
"""Get an instance of a K8s object.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
K8s: Return a K8s object
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
settings = Settings()
|
|
71
|
+
|
|
72
|
+
otcs = OTCS(
|
|
73
|
+
protocol=settings.otcs.url_backend.scheme,
|
|
74
|
+
hostname=settings.otcs.url_backend.host,
|
|
75
|
+
port=settings.otcs.url_backend.port,
|
|
76
|
+
public_url=str(settings.otcs.url),
|
|
77
|
+
user_partition=settings.otcs.partition,
|
|
78
|
+
resource_name=settings.otcs.resource_name,
|
|
79
|
+
base_path=settings.otcs.base_path,
|
|
80
|
+
support_path=settings.otcs.support_path,
|
|
81
|
+
download_dir=settings.otcs.download_dir,
|
|
82
|
+
feme_uri=settings.otcs.feme_uri,
|
|
83
|
+
logger=logger.getChild("otcs"),
|
|
54
84
|
)
|
|
55
85
|
|
|
56
|
-
|
|
86
|
+
otcs._otcs_ticket = otcsticket # noqa: SLF001
|
|
57
87
|
|
|
58
|
-
return
|
|
88
|
+
return otcs
|
|
59
89
|
|
|
60
90
|
|
|
61
91
|
def get_settings() -> CustomizerAPISettings:
|
|
@@ -4,8 +4,7 @@ from collections.abc import Callable
|
|
|
4
4
|
|
|
5
5
|
from prometheus_client import Gauge
|
|
6
6
|
from prometheus_fastapi_instrumentator.metrics import Info
|
|
7
|
-
|
|
8
|
-
from pyxecm.customizer.api.common.payload_list import PayloadList
|
|
7
|
+
from pyxecm_customizer.payload_list import PayloadList
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
## By Payload
|