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.
Files changed (52) hide show
  1. agent_api_server/__init__.py +0 -0
  2. agent_api_server/api/__init__.py +0 -0
  3. agent_api_server/api/v1/__init__.py +0 -0
  4. agent_api_server/api/v1/api.py +25 -0
  5. agent_api_server/api/v1/config.py +57 -0
  6. agent_api_server/api/v1/graph.py +59 -0
  7. agent_api_server/api/v1/schema.py +57 -0
  8. agent_api_server/api/v1/thread.py +563 -0
  9. agent_api_server/cache/__init__.py +0 -0
  10. agent_api_server/cache/redis_cache.py +385 -0
  11. agent_api_server/callback_handler.py +18 -0
  12. agent_api_server/client/css/styles.css +1202 -0
  13. agent_api_server/client/favicon.ico +0 -0
  14. agent_api_server/client/index.html +102 -0
  15. agent_api_server/client/js/app.js +1499 -0
  16. agent_api_server/client/js/index.umd.js +824 -0
  17. agent_api_server/config_center/config_center.py +239 -0
  18. agent_api_server/configs/__init__.py +3 -0
  19. agent_api_server/configs/config.py +163 -0
  20. agent_api_server/dynamic_llm/__init__.py +0 -0
  21. agent_api_server/dynamic_llm/dynamic_llm.py +331 -0
  22. agent_api_server/listener.py +530 -0
  23. agent_api_server/log/__init__.py +0 -0
  24. agent_api_server/log/formatters.py +122 -0
  25. agent_api_server/log/logging.json +50 -0
  26. agent_api_server/mcp_convert/__init__.py +0 -0
  27. agent_api_server/mcp_convert/mcp_convert.py +375 -0
  28. agent_api_server/memeory/__init__.py +0 -0
  29. agent_api_server/memeory/postgres.py +233 -0
  30. agent_api_server/register/__init__.py +0 -0
  31. agent_api_server/register/register.py +65 -0
  32. agent_api_server/service.py +354 -0
  33. agent_api_server/service_hub/service_hub.py +233 -0
  34. agent_api_server/service_hub/service_hub_test.py +700 -0
  35. agent_api_server/shared/__init__.py +0 -0
  36. agent_api_server/shared/ase.py +54 -0
  37. agent_api_server/shared/base_model.py +103 -0
  38. agent_api_server/shared/common.py +110 -0
  39. agent_api_server/shared/decode_token.py +107 -0
  40. agent_api_server/shared/detect_message.py +410 -0
  41. agent_api_server/shared/get_model_info.py +491 -0
  42. agent_api_server/shared/message.py +419 -0
  43. agent_api_server/shared/util_func.py +372 -0
  44. agent_api_server/sso_service/__init__.py +1 -0
  45. agent_api_server/sso_service/sdk/__init__.py +1 -0
  46. agent_api_server/sso_service/sdk/client.py +224 -0
  47. agent_api_server/sso_service/sdk/credential.py +11 -0
  48. agent_api_server/sso_service/sdk/encoding.py +22 -0
  49. agent_api_server/sso_service/sso_service.py +177 -0
  50. agent_api_server-2.1.7.dist-info/METADATA +130 -0
  51. agent_api_server-2.1.7.dist-info/RECORD +52 -0
  52. 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"))