agent-api-server 2.1.7__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.
- agent_api_server/__init__.py +0 -0
- agent_api_server/api/__init__.py +0 -0
- agent_api_server/api/v1/__init__.py +0 -0
- agent_api_server/api/v1/api.py +25 -0
- agent_api_server/api/v1/config.py +57 -0
- agent_api_server/api/v1/graph.py +59 -0
- agent_api_server/api/v1/schema.py +57 -0
- agent_api_server/api/v1/thread.py +563 -0
- agent_api_server/cache/__init__.py +0 -0
- agent_api_server/cache/redis_cache.py +385 -0
- agent_api_server/callback_handler.py +18 -0
- agent_api_server/client/css/styles.css +1202 -0
- agent_api_server/client/favicon.ico +0 -0
- agent_api_server/client/index.html +102 -0
- agent_api_server/client/js/app.js +1499 -0
- agent_api_server/client/js/index.umd.js +824 -0
- agent_api_server/config_center/config_center.py +239 -0
- agent_api_server/configs/__init__.py +3 -0
- agent_api_server/configs/config.py +163 -0
- agent_api_server/dynamic_llm/__init__.py +0 -0
- agent_api_server/dynamic_llm/dynamic_llm.py +331 -0
- agent_api_server/listener.py +530 -0
- agent_api_server/log/__init__.py +0 -0
- agent_api_server/log/formatters.py +122 -0
- agent_api_server/log/logging.json +50 -0
- agent_api_server/mcp_convert/__init__.py +0 -0
- agent_api_server/mcp_convert/mcp_convert.py +375 -0
- agent_api_server/memeory/__init__.py +0 -0
- agent_api_server/memeory/postgres.py +233 -0
- agent_api_server/register/__init__.py +0 -0
- agent_api_server/register/register.py +65 -0
- agent_api_server/service.py +354 -0
- agent_api_server/service_hub/service_hub.py +233 -0
- agent_api_server/service_hub/service_hub_test.py +700 -0
- agent_api_server/shared/__init__.py +0 -0
- agent_api_server/shared/ase.py +54 -0
- agent_api_server/shared/base_model.py +103 -0
- agent_api_server/shared/common.py +110 -0
- agent_api_server/shared/decode_token.py +107 -0
- agent_api_server/shared/detect_message.py +410 -0
- agent_api_server/shared/get_model_info.py +491 -0
- agent_api_server/shared/message.py +419 -0
- agent_api_server/shared/util_func.py +372 -0
- agent_api_server/sso_service/__init__.py +1 -0
- agent_api_server/sso_service/sdk/__init__.py +1 -0
- agent_api_server/sso_service/sdk/client.py +224 -0
- agent_api_server/sso_service/sdk/credential.py +11 -0
- agent_api_server/sso_service/sdk/encoding.py +22 -0
- agent_api_server/sso_service/sso_service.py +177 -0
- agent_api_server-2.1.7.dist-info/METADATA +130 -0
- agent_api_server-2.1.7.dist-info/RECORD +52 -0
- agent_api_server-2.1.7.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import multiprocessing
|
|
3
|
+
import os
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
from agent_api_server.config_center.config_center import AsyncConfigCenterClient
|
|
7
|
+
from agent_api_server.configs import global_config
|
|
8
|
+
import logging
|
|
9
|
+
from fastapi import HTTPException
|
|
10
|
+
from fastapi import FastAPI
|
|
11
|
+
from agent_api_server.api.v1.api import api_router
|
|
12
|
+
from pydantic_settings import BaseSettings
|
|
13
|
+
from typing import List, Any
|
|
14
|
+
from fastapi.staticfiles import StaticFiles
|
|
15
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from agent_api_server.listener import ListenerType, create_listener
|
|
18
|
+
from agent_api_server.mcp_convert.mcp_convert import create_mcp_tool_from_agent
|
|
19
|
+
from agent_api_server.service_hub.service_hub import ServiceHubCredentialLoader, CredentialNotFoundError, \
|
|
20
|
+
InvalidCredentialError, ServiceHubError
|
|
21
|
+
from agent_api_server.sso_service import SSOConfig, SSOService
|
|
22
|
+
from fastapi.responses import Response, JSONResponse
|
|
23
|
+
from fastapi.requests import Request
|
|
24
|
+
from agent_api_server.register.register import AgentRegistry
|
|
25
|
+
from typing import AsyncIterator
|
|
26
|
+
from agent_api_server.cache.redis_cache import AsyncRedisThreadStorage
|
|
27
|
+
from agent_api_server.memeory.postgres import AsyncPostgresCheckpointer
|
|
28
|
+
from contextlib import asynccontextmanager
|
|
29
|
+
from agent_api_server.shared.util_func import parse_agent_config
|
|
30
|
+
|
|
31
|
+
WELCOME_ART = """
|
|
32
|
+
╦ ┌─┐┌┐┌┌─┐╔═╗┬─┐┌─┐┌─┐┬ ┬
|
|
33
|
+
║ ├─┤││││ ┬║ ╦├┬┘├─┤├─┘├─┤
|
|
34
|
+
╩═╝┴ ┴┘└┘└─┘╚═╝┴└─┴ ┴┴ ┴ ┴
|
|
35
|
+
|
|
36
|
+
🚀 API: http://{host}:{port}
|
|
37
|
+
📚 API Docs: http://{host}:{port}/docs
|
|
38
|
+
💻 client demo: http://{host}:{port}/site
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
class Settings(BaseSettings):
|
|
44
|
+
port: int = global_config.SERVER_PORT
|
|
45
|
+
mcp_port: int = global_config.MCP_SERVER_PORT
|
|
46
|
+
host: str = "0.0.0.0"
|
|
47
|
+
workers: int = global_config.SERVER_WORKER_AMOUNT
|
|
48
|
+
reload: bool = False
|
|
49
|
+
api_path: str = "/api/v1"
|
|
50
|
+
cors_origins: List[str] = ["*"]
|
|
51
|
+
|
|
52
|
+
settings = Settings()
|
|
53
|
+
|
|
54
|
+
@asynccontextmanager
|
|
55
|
+
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
56
|
+
cache = AsyncRedisThreadStorage.get_worker_instance()
|
|
57
|
+
checkpointer = AsyncPostgresCheckpointer.get_worker_instance()
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
await checkpointer.initialize()
|
|
61
|
+
logger.info(f"PostgresSQL connection pool initialized in worker {os.getpid()}")
|
|
62
|
+
|
|
63
|
+
await cache.initialize()
|
|
64
|
+
logger.info(f"Redis connection pool initialized in worker {os.getpid()}")
|
|
65
|
+
yield
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.error(f"Failed to initialize connection pool in worker {os.getpid()}")
|
|
68
|
+
raise
|
|
69
|
+
finally:
|
|
70
|
+
try:
|
|
71
|
+
await AsyncPostgresCheckpointer.close_worker_instance()
|
|
72
|
+
logger.info(f"PostgresSQL connection pool closed in worker {os.getpid()}")
|
|
73
|
+
|
|
74
|
+
await AsyncRedisThreadStorage.close_worker_instance()
|
|
75
|
+
logger.info(f"Redis connection pool closed in worker {os.getpid()}")
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(f"Error closing connection pool in worker {os.getpid()}, error message is {str(e)}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
app = FastAPI(lifespan=lifespan)
|
|
81
|
+
|
|
82
|
+
CLIENT_DIR = Path(__file__).parent / "client"
|
|
83
|
+
app.mount(
|
|
84
|
+
"/site",
|
|
85
|
+
StaticFiles(directory=str(CLIENT_DIR), html=True),
|
|
86
|
+
name="client"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
app.add_middleware(
|
|
90
|
+
CORSMiddleware,
|
|
91
|
+
allow_origins=settings.cors_origins,
|
|
92
|
+
allow_credentials=True,
|
|
93
|
+
allow_methods=["*"],
|
|
94
|
+
allow_headers=["*"],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@app.exception_handler(Exception)
|
|
98
|
+
async def universal_handler(request: Request, exc: Exception):
|
|
99
|
+
if "h11_impl" in str(exc):
|
|
100
|
+
return Response(status_code=204)
|
|
101
|
+
|
|
102
|
+
logger.error(
|
|
103
|
+
f"Unhandled Exception: {exc}",
|
|
104
|
+
exc_info=True,
|
|
105
|
+
extra={"path": request.url.path, "client": request.client}
|
|
106
|
+
)
|
|
107
|
+
return JSONResponse(
|
|
108
|
+
status_code=500,
|
|
109
|
+
content={"error": "internal_error", "message": f"{exc}"}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
app.include_router(api_router, prefix=settings.api_path)
|
|
113
|
+
|
|
114
|
+
def print_welcome(host: str, port: int):
|
|
115
|
+
welcome_msg = WELCOME_ART.format(host=host, port=port)
|
|
116
|
+
logger.info(welcome_msg)
|
|
117
|
+
|
|
118
|
+
async def load_agent_config():
|
|
119
|
+
try:
|
|
120
|
+
return await parse_agent_config()
|
|
121
|
+
except HTTPException as e:
|
|
122
|
+
raise e
|
|
123
|
+
except Exception as e:
|
|
124
|
+
raise e
|
|
125
|
+
|
|
126
|
+
def create_fastapi_app() -> FastAPI:
|
|
127
|
+
return app
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def init_fastapi_server():
|
|
131
|
+
print_welcome(host=settings.host, port=settings.port)
|
|
132
|
+
|
|
133
|
+
agents: list[dict[str, Any]]
|
|
134
|
+
try:
|
|
135
|
+
agents = await load_agent_config()
|
|
136
|
+
logger.info(f"Agent config loaded: {agents}")
|
|
137
|
+
|
|
138
|
+
if os.getenv("ENSAAS_SERVICES"):
|
|
139
|
+
logger.info(f"ENSAAS_SERVICES is not empty, Get POSTGRES_URL and REDIS_URL from ENSAAS_SERVICES")
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
credentials = ServiceHubCredentialLoader.load_credentials()
|
|
143
|
+
|
|
144
|
+
logger.info(f"Get credentials from ENSAAS_SERVICES key, credentials is {credentials}, let's parse POSTGRES_URL, REDIS_URL and Blob information from it!")
|
|
145
|
+
|
|
146
|
+
global_config.POSTGRES_URL = credentials.to_postgres_dsn()
|
|
147
|
+
global_config.REDIS_URL = credentials.to_redis_dsn()
|
|
148
|
+
blob_credential = credentials.get_blobstore_s3_config()
|
|
149
|
+
|
|
150
|
+
if blob_credential != {}:
|
|
151
|
+
global_config.STORAGE_ENDPOINT = blob_credential.get('endpoint', '')
|
|
152
|
+
global_config.ACCESS_KEY = blob_credential.get('access_key', '')
|
|
153
|
+
global_config.SECRET_KEY = blob_credential.get('secret_key', '')
|
|
154
|
+
|
|
155
|
+
os.environ['STORAGE_ENDPOINT'] = global_config.STORAGE_ENDPOINT
|
|
156
|
+
os.environ['ACCESS_KEY'] = global_config.ACCESS_KEY
|
|
157
|
+
os.environ['SECRET_KEY'] = global_config.SECRET_KEY
|
|
158
|
+
|
|
159
|
+
logger.info(
|
|
160
|
+
f"Get POSTGRES_URL {global_config.POSTGRES_URL}, REDIS_URL {global_config.REDIS_URL} "
|
|
161
|
+
f"Get STORAGE_ENDPOINT {global_config.STORAGE_ENDPOINT}, ACCESS_KEY {global_config.ACCESS_KEY}, SECRET_KEY {global_config.SECRET_KEY}"
|
|
162
|
+
f"from ENSAAS_SERVICES key is ok!")
|
|
163
|
+
else:
|
|
164
|
+
logger.info(
|
|
165
|
+
f"Get POSTGRES_URL {global_config.POSTGRES_URL}, REDIS_URL {global_config.REDIS_URL} "
|
|
166
|
+
f"from ENSAAS_SERVICES key is ok!")
|
|
167
|
+
|
|
168
|
+
except CredentialNotFoundError as e:
|
|
169
|
+
logger.error(f"Required credential not found: {e}")
|
|
170
|
+
raise
|
|
171
|
+
except InvalidCredentialError as e:
|
|
172
|
+
logger.error(f"Invalid credential format: {e}")
|
|
173
|
+
raise
|
|
174
|
+
except ServiceHubError as e:
|
|
175
|
+
logger.error(f"Failed to load credentials: {e}")
|
|
176
|
+
raise
|
|
177
|
+
|
|
178
|
+
if global_config.ENABLE_CONFIG_CENTER:
|
|
179
|
+
try:
|
|
180
|
+
config_client = await AsyncConfigCenterClient.get_instance()
|
|
181
|
+
|
|
182
|
+
if os.getenv("ENSAAS_SERVICES"):
|
|
183
|
+
with_secret = True
|
|
184
|
+
else:
|
|
185
|
+
with_secret = False
|
|
186
|
+
|
|
187
|
+
await config_client.register_to_config_center(agents=agents, with_secret=with_secret)
|
|
188
|
+
service_config = await config_client.service_config()
|
|
189
|
+
global_config.MODEL_MANAGER_SERVICE_URL = service_config["MODEL_MANAGER_SERVICE_URL"]
|
|
190
|
+
os.environ['MODEL_MANAGER_SERVICE_URL'] = global_config.MODEL_MANAGER_SERVICE_URL
|
|
191
|
+
|
|
192
|
+
if service_config["SSO_URL"] is not None and service_config["SSO_URL"] != "":
|
|
193
|
+
global_config.SSO_URL = service_config["SSO_URL"]
|
|
194
|
+
|
|
195
|
+
mcp_gateway_url = await config_client.get_service_internal_url_with_router_path(
|
|
196
|
+
service_name='MCPGateway',
|
|
197
|
+
router_path='MCPGateway')
|
|
198
|
+
global_config.MCP_GATEWAY_URL = mcp_gateway_url
|
|
199
|
+
os.environ['MCP_GATEWAY_URL'] = global_config.MCP_GATEWAY_URL
|
|
200
|
+
|
|
201
|
+
logger.info(
|
|
202
|
+
f"Got service config from config center: "
|
|
203
|
+
f"MODEL_MANAGER_SERVICE_URL={os.environ['MODEL_MANAGER_SERVICE_URL']}, "
|
|
204
|
+
f"SSO_URL={global_config.SSO_URL}, "
|
|
205
|
+
f"MCP_GATEWAY_URL={global_config.MCP_GATEWAY_URL}")
|
|
206
|
+
|
|
207
|
+
if not with_secret:
|
|
208
|
+
logger.info("ENABLE_CONFIG_CENTER is True and SECRET_NAME is empty, get db and public information from config center")
|
|
209
|
+
|
|
210
|
+
public_config = await config_client.public_config()
|
|
211
|
+
os.environ['cluster'] = public_config.get('cluster', '')
|
|
212
|
+
os.environ['datacenter'] = public_config.get('datacenter', '')
|
|
213
|
+
os.environ['workspace'] = public_config.get('workspace', '')
|
|
214
|
+
os.environ['namespace'] = public_config.get('namespace', '')
|
|
215
|
+
logger.info(f"Got public config from config center: {public_config}")
|
|
216
|
+
|
|
217
|
+
db_credentials = await config_client.database_credential()
|
|
218
|
+
global_config.POSTGRES_URL = db_credentials["POSTGRES_URL"]
|
|
219
|
+
global_config.REDIS_URL = db_credentials["REDIS_URL"]
|
|
220
|
+
logger.info(
|
|
221
|
+
f"Got database credentials from config center: POSTGRES_URL={global_config.POSTGRES_URL}, REDIS_URL={global_config.REDIS_URL}")
|
|
222
|
+
|
|
223
|
+
config_client.start_periodic_registration(agents, with_secret)
|
|
224
|
+
logger.info("Started periodic registration to config center successfully")
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Failed to get configuration from config center: {e}")
|
|
228
|
+
raise
|
|
229
|
+
else:
|
|
230
|
+
logger.info("ENABLE_CONFIG_CENTER is False, do not get service and db information from config center")
|
|
231
|
+
|
|
232
|
+
if global_config.POSTGRES_URL != "":
|
|
233
|
+
checkpointer = AsyncPostgresCheckpointer.get_worker_instance()
|
|
234
|
+
try:
|
|
235
|
+
await checkpointer.initialize()
|
|
236
|
+
await checkpointer.initialize_schema_with_retry()
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.error(f"Init schema to postgres database with an un-expected error: {e}")
|
|
239
|
+
raise
|
|
240
|
+
finally:
|
|
241
|
+
logger.info("Init schema to postgres database with successfully and close postgres connection instance")
|
|
242
|
+
await checkpointer.close_worker_instance()
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
sso_service = SSOService(sso_config=SSOConfig(
|
|
246
|
+
sso_address=global_config.SSO_URL,
|
|
247
|
+
client_id=global_config.CLIENT_ID,
|
|
248
|
+
client_secret=global_config.CLIENT_SECRET,
|
|
249
|
+
))
|
|
250
|
+
client_id, client_secret = sso_service.get_client_id_and_secret()
|
|
251
|
+
logger.debug(f"Client ID: {client_id}, Client Secret: {client_secret}")
|
|
252
|
+
|
|
253
|
+
global_config.CLIENT_TOKEN = sso_service.generate_client_token(client_id, client_secret)
|
|
254
|
+
os.environ['CLIENT_TOKEN'] = global_config.CLIENT_TOKEN
|
|
255
|
+
logger.info(f"Generated Client Token: {os.environ['CLIENT_TOKEN']}")
|
|
256
|
+
except Exception as e:
|
|
257
|
+
logger.error(f"Failed during agent registration or listener setup: {e}")
|
|
258
|
+
raise
|
|
259
|
+
|
|
260
|
+
if not global_config.AGENT_AUTO_REGISTRATION:
|
|
261
|
+
logger.warning(
|
|
262
|
+
"Agent auto-registration is disabled by configuration (AGENT_AUTO_REGISTRATION=False).\n"
|
|
263
|
+
"IMPORTANT: Automatic registration to Model Manager is prohibited.\n"
|
|
264
|
+
"Required action:\n"
|
|
265
|
+
"1. You must manually register this agent to Model Manager\n"
|
|
266
|
+
"2. Use the Model Manager SDK to complete registration\n"
|
|
267
|
+
)
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
for agent in agents:
|
|
272
|
+
agent["agent_url"] = global_config.SERVICE_EXTERNAL_URL
|
|
273
|
+
|
|
274
|
+
agent_registry = AgentRegistry(
|
|
275
|
+
base_url=global_config.MODEL_MANAGER_SERVICE_URL,
|
|
276
|
+
client_token=global_config.CLIENT_TOKEN
|
|
277
|
+
)
|
|
278
|
+
await agent_registry.register_all(agents=agents)
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
listener_type = determine_listener_type()
|
|
282
|
+
logger.info(f"Using {listener_type.name} listener based on configuration")
|
|
283
|
+
except ValueError as e:
|
|
284
|
+
logger.error(str(e))
|
|
285
|
+
raise
|
|
286
|
+
|
|
287
|
+
for agent in agents:
|
|
288
|
+
listener = create_listener(
|
|
289
|
+
agent["agent_name"],
|
|
290
|
+
global_config.CLIENT_TOKEN,
|
|
291
|
+
listener_type
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
thread = threading.Thread(
|
|
295
|
+
target=listener.run,
|
|
296
|
+
daemon=True,
|
|
297
|
+
name=f"RedisListener-{agent['agent_name']}"
|
|
298
|
+
)
|
|
299
|
+
thread.start()
|
|
300
|
+
logger.info(f"Started {listener_type.name} listener for agent: {agent['agent_name']}")
|
|
301
|
+
|
|
302
|
+
except Exception as e:
|
|
303
|
+
logger.error(f"Failed during agent registration or listener setup: {e}")
|
|
304
|
+
raise
|
|
305
|
+
|
|
306
|
+
except ValueError as e:
|
|
307
|
+
logger.error(f"Failed to start agent server with a ValueError: {e}")
|
|
308
|
+
raise
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error(f"Failed to start agent server with an Exception error: {e}")
|
|
311
|
+
raise
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def determine_listener_type() -> ListenerType:
|
|
315
|
+
if hasattr(global_config, 'MODEL_MANAGER_NATS_URL') and global_config.MODEL_MANAGER_NATS_URL:
|
|
316
|
+
return ListenerType.NATS
|
|
317
|
+
elif hasattr(global_config, 'MODEL_MANAGER_REDIS_URL') and global_config.MODEL_MANAGER_REDIS_URL:
|
|
318
|
+
return ListenerType.REDIS
|
|
319
|
+
else:
|
|
320
|
+
raise ValueError(
|
|
321
|
+
"No valid message broker configured. "
|
|
322
|
+
"Please set either MODEL_MANAGER_NATS_URL or MODEL_MANAGER_REDIS_URL"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def run_mcp_server(log_cfg):
|
|
326
|
+
try:
|
|
327
|
+
mcp = asyncio.run(create_mcp_tool_from_agent())
|
|
328
|
+
mcp.run(
|
|
329
|
+
transport="streamable-http",
|
|
330
|
+
show_banner=False,
|
|
331
|
+
uvicorn_config={"log_config": log_cfg},
|
|
332
|
+
host=settings.host,
|
|
333
|
+
port=settings.mcp_port,
|
|
334
|
+
)
|
|
335
|
+
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
336
|
+
logging.info("MCP server received shutdown signal")
|
|
337
|
+
except Exception as e:
|
|
338
|
+
logging.error(f"MCP server failed: {e}")
|
|
339
|
+
raise
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def start_mcp_server_process(log_cfg):
|
|
343
|
+
if not global_config.ENABLE_MCP_SERVER:
|
|
344
|
+
logger.info("MCP server is disabled by configuration (ENABLE_MCP_SERVER=False)")
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
process = multiprocessing.Process(
|
|
348
|
+
target=run_mcp_server,
|
|
349
|
+
args=(log_cfg,),
|
|
350
|
+
name="MCP-Server",
|
|
351
|
+
)
|
|
352
|
+
process.start()
|
|
353
|
+
logger.info(f"MCP server process started (PID: {process.pid})")
|
|
354
|
+
return process
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, List, Union
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class ServiceHubError(Exception):
|
|
10
|
+
"""Base exception for ServiceHub credential parsing errors"""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CredentialNotFoundError(ServiceHubError):
|
|
15
|
+
"""Raised when a specific credential type is not found"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InvalidCredentialError(ServiceHubError):
|
|
20
|
+
"""Raised when credential data is invalid"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class PostgresCredential:
|
|
26
|
+
async_flag: bool
|
|
27
|
+
binding_name: str
|
|
28
|
+
database: str
|
|
29
|
+
external_hosts: str
|
|
30
|
+
host: str
|
|
31
|
+
internal_hosts: str
|
|
32
|
+
password: str
|
|
33
|
+
port: int
|
|
34
|
+
uri: str
|
|
35
|
+
username: str
|
|
36
|
+
instance_name: str = ""
|
|
37
|
+
label: str = ""
|
|
38
|
+
plan: str = ""
|
|
39
|
+
service_instance_id: str = ""
|
|
40
|
+
subscription_id: str = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class RedisCredential:
|
|
45
|
+
binding_name: str
|
|
46
|
+
external_hosts: str
|
|
47
|
+
host: str
|
|
48
|
+
internal_hosts: str
|
|
49
|
+
password: str
|
|
50
|
+
port: int
|
|
51
|
+
uri: str = ""
|
|
52
|
+
username: str = ""
|
|
53
|
+
instance_name: str = ""
|
|
54
|
+
label: str = ""
|
|
55
|
+
plan: str = ""
|
|
56
|
+
service_instance_id: str = ""
|
|
57
|
+
subscription_id: str = ""
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class BlobstoreCredential:
|
|
61
|
+
"""Blobstore (S3-compatible) connection credentials"""
|
|
62
|
+
binding_name: str
|
|
63
|
+
access_key: str
|
|
64
|
+
secret_key: str
|
|
65
|
+
endpoint: str
|
|
66
|
+
external_hosts: str
|
|
67
|
+
internal_hosts: str
|
|
68
|
+
signature_version: str
|
|
69
|
+
type: str
|
|
70
|
+
instance_name: str = ""
|
|
71
|
+
label: str = ""
|
|
72
|
+
plan: str = ""
|
|
73
|
+
service_instance_id: str = ""
|
|
74
|
+
subscription_id: str = ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class ServiceHubCredentials:
|
|
79
|
+
postgres: List[PostgresCredential] = field(default_factory=list)
|
|
80
|
+
redis: List[RedisCredential] = field(default_factory=list)
|
|
81
|
+
blobstore: List[BlobstoreCredential] = field(default_factory=list)
|
|
82
|
+
|
|
83
|
+
def to_postgres_dsn(self) -> str:
|
|
84
|
+
if not self.postgres:
|
|
85
|
+
raise CredentialNotFoundError("PostgreSQL credentials not found")
|
|
86
|
+
|
|
87
|
+
cred = self.postgres[0]
|
|
88
|
+
return f"postgresql://{cred.username}:{cred.password}@{cred.internal_hosts}/{cred.database}"
|
|
89
|
+
|
|
90
|
+
def to_redis_dsn(self) -> str:
|
|
91
|
+
if not self.redis:
|
|
92
|
+
raise CredentialNotFoundError("Redis credentials not found")
|
|
93
|
+
|
|
94
|
+
cred = self.redis[0]
|
|
95
|
+
|
|
96
|
+
if not cred.password and not cred.username:
|
|
97
|
+
return f"redis://{cred.internal_hosts}:{cred.port}"
|
|
98
|
+
|
|
99
|
+
if cred.password and not cred.username:
|
|
100
|
+
return f"redis://:{cred.password}@{cred.internal_hosts}:{cred.port}"
|
|
101
|
+
|
|
102
|
+
if cred.username and not cred.password:
|
|
103
|
+
return f"redis://{cred.username}@{cred.internal_hosts}:{cred.port}"
|
|
104
|
+
|
|
105
|
+
if cred.username and cred.password:
|
|
106
|
+
return f"redis://{cred.username}:{cred.password}@{cred.internal_hosts}:{cred.port}"
|
|
107
|
+
|
|
108
|
+
def get_blobstore_s3_config(self) -> Dict[str, str]:
|
|
109
|
+
"""Get Blobstore credentials as S3-compatible configuration dictionary"""
|
|
110
|
+
if not self.blobstore:
|
|
111
|
+
logger.warning("Blobstore credentials not found")
|
|
112
|
+
return {}
|
|
113
|
+
|
|
114
|
+
cred = self.blobstore[0]
|
|
115
|
+
return {
|
|
116
|
+
"access_key": cred.access_key,
|
|
117
|
+
"secret_key": cred.secret_key,
|
|
118
|
+
"endpoint": cred.endpoint,
|
|
119
|
+
"external_hosts": cred.external_hosts,
|
|
120
|
+
"internal_hosts": cred.internal_hosts,
|
|
121
|
+
"signature_version": cred.signature_version,
|
|
122
|
+
"type": cred.type
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
def get_service_instance_id(self) -> str:
|
|
126
|
+
"""Get the first available service instance ID"""
|
|
127
|
+
for cred_list in [
|
|
128
|
+
self.postgres,
|
|
129
|
+
self.redis,
|
|
130
|
+
self.blobstore,
|
|
131
|
+
]:
|
|
132
|
+
if cred_list and cred_list[0].service_instance_id:
|
|
133
|
+
return cred_list[0].service_instance_id
|
|
134
|
+
raise CredentialNotFoundError("No service instance ID found")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ServiceHubCredentialParser:
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _parse_postgres(data: Dict) -> PostgresCredential:
|
|
140
|
+
return PostgresCredential(
|
|
141
|
+
async_flag=data.get("async", False),
|
|
142
|
+
binding_name=data.get("binding_name", ""),
|
|
143
|
+
database=data.get("credentials", {}).get("database", ""),
|
|
144
|
+
external_hosts=data.get("credentials", {}).get("externalHosts", ""),
|
|
145
|
+
host=data.get("credentials", {}).get("host", ""),
|
|
146
|
+
internal_hosts=data.get("credentials", {}).get("internalHosts", ""),
|
|
147
|
+
password=data.get("credentials", {}).get("password", ""),
|
|
148
|
+
port=data.get("credentials", {}).get("port", 0),
|
|
149
|
+
uri=data.get("credentials", {}).get("uri", ""),
|
|
150
|
+
username=data.get("credentials", {}).get("username", ""),
|
|
151
|
+
instance_name=data.get("instance_name", ""),
|
|
152
|
+
label=data.get("label", ""),
|
|
153
|
+
plan=data.get("plan", ""),
|
|
154
|
+
service_instance_id=data.get("serviceInstanceId", ""),
|
|
155
|
+
subscription_id=data.get("subscriptionId", ""),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _parse_redis(data: Dict) -> RedisCredential:
|
|
160
|
+
return RedisCredential(
|
|
161
|
+
binding_name=data.get("binding_name", ""),
|
|
162
|
+
external_hosts=data.get("credentials", {}).get("externalHosts", ""),
|
|
163
|
+
host=data.get("credentials", {}).get("host", ""),
|
|
164
|
+
internal_hosts=data.get("credentials", {}).get("internalHosts", ""),
|
|
165
|
+
password=data.get("credentials", {}).get("password", ""),
|
|
166
|
+
port=data.get("credentials", {}).get("port", 0),
|
|
167
|
+
uri=data.get("credentials", {}).get("uri", ""),
|
|
168
|
+
username=data.get("credentials", {}).get("username", ""),
|
|
169
|
+
instance_name=data.get("instance_name", ""),
|
|
170
|
+
label=data.get("label", ""),
|
|
171
|
+
plan=data.get("plan", ""),
|
|
172
|
+
service_instance_id=data.get("serviceInstanceId", ""),
|
|
173
|
+
subscription_id=data.get("subscriptionId", ""),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _parse_blobstore(data: Dict) -> BlobstoreCredential:
|
|
178
|
+
return BlobstoreCredential(
|
|
179
|
+
binding_name=data.get("binding_name", ""),
|
|
180
|
+
access_key=data.get("credentials", {}).get("accessKey", ""),
|
|
181
|
+
secret_key=data.get("credentials", {}).get("secretKey", ""),
|
|
182
|
+
endpoint=data.get("credentials", {}).get("endpoint", ""),
|
|
183
|
+
external_hosts=data.get("credentials", {}).get("externalHosts", ""),
|
|
184
|
+
internal_hosts=data.get("credentials", {}).get("internalHosts", ""),
|
|
185
|
+
signature_version=data.get("credentials", {}).get("signature-version", ""),
|
|
186
|
+
type=data.get("credentials", {}).get("type", ""),
|
|
187
|
+
instance_name=data.get("instance_name", ""),
|
|
188
|
+
label=data.get("label", ""),
|
|
189
|
+
plan=data.get("plan", ""),
|
|
190
|
+
service_instance_id=data.get("serviceInstanceId", ""),
|
|
191
|
+
subscription_id=data.get("subscriptionId", ""),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def parse(data: Union[str, bytes, Dict]) -> ServiceHubCredentials:
|
|
196
|
+
"""Parse ServiceHub credentials from JSON data"""
|
|
197
|
+
if isinstance(data, (str, bytes)):
|
|
198
|
+
try:
|
|
199
|
+
if isinstance(data, bytes):
|
|
200
|
+
data = data.decode('utf-8')
|
|
201
|
+
data = json.loads(data)
|
|
202
|
+
except json.JSONDecodeError as e:
|
|
203
|
+
raise InvalidCredentialError(f"Invalid JSON data: {e}") from e
|
|
204
|
+
|
|
205
|
+
if not isinstance(data, dict):
|
|
206
|
+
raise InvalidCredentialError("Expected dictionary data")
|
|
207
|
+
|
|
208
|
+
credentials = ServiceHubCredentials()
|
|
209
|
+
|
|
210
|
+
if "postgresql" in data:
|
|
211
|
+
credentials.postgres = [
|
|
212
|
+
ServiceHubCredentialParser._parse_postgres(item)
|
|
213
|
+
for item in data["postgresql"]
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
if "redis" in data:
|
|
217
|
+
credentials.redis = [
|
|
218
|
+
ServiceHubCredentialParser._parse_redis(item)
|
|
219
|
+
for item in data["redis"]
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
if "blobstore" in data:
|
|
223
|
+
credentials.blobstore = [
|
|
224
|
+
ServiceHubCredentialParser._parse_blobstore(item)
|
|
225
|
+
for item in data["blobstore"]
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
return credentials
|
|
229
|
+
|
|
230
|
+
class ServiceHubCredentialLoader:
|
|
231
|
+
@staticmethod
|
|
232
|
+
def load_credentials() -> ServiceHubCredentials:
|
|
233
|
+
return ServiceHubCredentialParser.parse(os.getenv("ENSAAS_SERVICES"))
|