1+ # ═══════════════════════════════════════════════════════════════════════════════
2+ # Routes__Service__Registry - REST API for service registry introspection
3+ # Provides endpoints for viewing registered services, health checks, and diagnostics
4+ #
5+ # Path pattern: /registry/...
6+ # ═══════════════════════════════════════════════════════════════════════════════
7+
8+ from typing import List , Optional
9+ from osbot_fast_api .api .decorators .route_path import route_path
10+ from osbot_fast_api .api .routes .Fast_API__Routes import Fast_API__Routes
11+ from osbot_fast_api .schemas .core_routes .registry .Schema__Registry__Responses import Schema__Registry__Status , Schema__Registry__Health__Summary , Schema__Registry__Service__Health
12+ from osbot_fast_api .schemas .core_routes .registry .Schema__Registry__Service__Info import Schema__Registry__Service__Info
13+ from osbot_fast_api .services .registry .Fast_API__Service__Registry import Fast_API__Service__Registry
14+ from osbot_fast_api .services .registry .Fast_API__Service__Registry import fast_api__service__registry
15+ from osbot_utils .type_safe .type_safe_core .collections .Type_Safe__List import Type_Safe__List
16+
17+ TAG__ROUTES_REGISTRY = 'registry'
18+
19+ ROUTES_PATHS__REGISTRY = [
20+ f'/{ TAG__ROUTES_REGISTRY } /status' ,
21+ f'/{ TAG__ROUTES_REGISTRY } /services' ,
22+ f'/{ TAG__ROUTES_REGISTRY } /services/{{service_name}}' ,
23+ f'/{ TAG__ROUTES_REGISTRY } /health' ,
24+ f'/{ TAG__ROUTES_REGISTRY } /health/{{service_name}}' ,
25+ ]
26+
27+
28+ # todo: refactor the business logic in this file into a services class
29+ class Routes__Service__Registry (Fast_API__Routes ): # Service registry introspection routes
30+ tag : str = TAG__ROUTES_REGISTRY # Route tag
31+ registry : Fast_API__Service__Registry = None # Registry to inspect (defaults to global)
32+
33+ def __init__ (self , ** kwargs ):
34+ super ().__init__ (** kwargs )
35+ if self .registry is None :
36+ self .registry = fast_api__service__registry
37+
38+ # ═══════════════════════════════════════════════════════════════════════════════
39+ # Status Endpoint
40+ # ═══════════════════════════════════════════════════════════════════════════════
41+
42+ def status (self ) -> Schema__Registry__Status : # GET /registry/status
43+ """Get overall registry status."""
44+ service_names = [self ._get_service_name (client_type )
45+ for client_type in self .registry .configs .keys ()]
46+
47+ return Schema__Registry__Status (
48+ registered_count = len (self .registry .configs ) ,
49+ stack_depth = self .registry .configs__stack_size (),
50+ services = service_names
51+ )
52+
53+ # ═══════════════════════════════════════════════════════════════════════════════
54+ # Services Endpoints
55+ # ═══════════════════════════════════════════════════════════════════════════════
56+
57+ #def services(self) -> List[Schema__Registry__Service__Info]: # GET /registry/services : List all registered services with their configuration (sanitized).
58+ def services (self ) -> List : # todo: figure why the Schema__Registry__Service__Info class is creating issues with pydantic serialisation
59+ results = Type_Safe__List (expected_type = Schema__Registry__Service__Info )
60+ for client_type , config in self .registry .configs .items ():
61+ info = self ._build_service_info (client_type , config )
62+ results .append (info .json ())
63+ return results .json ()
64+
65+ @route_path ('/services/{service_name}' )
66+ def service__get (self , # GET /registry/services/{service_name} | Get configuration for a specific service (sanitized).
67+ service_name : str
68+ ) -> Schema__Registry__Service__Info :
69+ client_type = self ._find_client_type (service_name )
70+ if client_type is None :
71+ return None
72+
73+ config = self .registry .config (client_type )
74+ return self ._build_service_info (client_type , config )
75+
76+ # ═══════════════════════════════════════════════════════════════════════════════
77+ # Health Endpoints
78+ # ═══════════════════════════════════════════════════════════════════════════════
79+
80+ def health (self ) -> Schema__Registry__Health__Summary : # GET /registry/health
81+ """Health check all registered services."""
82+ results = []
83+ healthy_count = 0
84+
85+ for client_type in self .registry .configs .keys ():
86+ health = self ._check_service_health (client_type )
87+ results .append (health )
88+ if health .healthy :
89+ healthy_count += 1
90+
91+ return Schema__Registry__Health__Summary (
92+ total_services = len (results ) ,
93+ healthy_count = healthy_count ,
94+ unhealthy_count = len (results ) - healthy_count ,
95+ services = results
96+ )
97+
98+ @route_path ('/health/{service_name}' )
99+ def health__service (self , service_name : str # GET /registry/health/{service_name}
100+ ) -> Schema__Registry__Service__Health :
101+ """Health check a specific service."""
102+ client_type = self ._find_client_type (service_name )
103+ if client_type is None :
104+ return Schema__Registry__Service__Health (
105+ service_name = service_name ,
106+ healthy = False ,
107+ mode = 'NOT_REGISTERED' ,
108+ error = f"Service '{ service_name } ' not found in registry"
109+ )
110+
111+ return self ._check_service_health (client_type )
112+
113+ # ═══════════════════════════════════════════════════════════════════════════════
114+ # Helper Methods
115+ # ═══════════════════════════════════════════════════════════════════════════════
116+
117+ def _get_service_name (self , client_type : type ) -> str : # Extract service name from type
118+ return client_type .__name__
119+
120+ def _get_service_module (self , client_type : type ) -> str : # Extract module from type
121+ return client_type .__module__
122+
123+ def _find_client_type (self , service_name : str ) -> Optional [type ]: # Find client type by name
124+ for client_type in self .registry .configs .keys ():
125+ if client_type .__name__ == service_name :
126+ return client_type
127+ return None
128+
129+ def _build_service_info (self , client_type : type , config # Build sanitized service info
130+ ) -> Schema__Registry__Service__Info :
131+ mode_str = config .mode .value if config .mode else 'UNKNOWN'
132+
133+ # Sanitize base_url (hide sensitive path components if needed)
134+ base_url = None
135+ if config .base_url :
136+ base_url = str (config .base_url )
137+
138+ return Schema__Registry__Service__Info (
139+ service_name = self ._get_service_name (client_type ) ,
140+ service_module = self ._get_service_module (client_type ),
141+ mode = mode_str ,
142+ base_url = base_url ,
143+ has_api_key = bool (config .api_key_name and config .api_key_value ),
144+ has_fast_api = config .fast_api_app is not None ,
145+ )
146+
147+ def _check_service_health (self , client_type : type # Check health for a service
148+ ) -> Schema__Registry__Service__Health :
149+ config = self .registry .config (client_type )
150+ service_name = self ._get_service_name (client_type )
151+ mode_str = config .mode .value if config .mode else 'UNKNOWN'
152+
153+ try :
154+ # Create client instance and call health()
155+ client = client_type ()
156+
157+ # Check if client has health method
158+ if hasattr (client , 'health' ) and callable (client .health ):
159+ healthy = client .health ()
160+ return Schema__Registry__Service__Health (
161+ service_name = service_name ,
162+ healthy = healthy ,
163+ mode = mode_str
164+ )
165+ else :
166+ return Schema__Registry__Service__Health (
167+ service_name = service_name ,
168+ healthy = True , # Assume healthy if no health method
169+ mode = mode_str ,
170+ error = "Service has no health() method"
171+ )
172+ except Exception as e :
173+ return Schema__Registry__Service__Health (
174+ service_name = service_name ,
175+ healthy = False ,
176+ mode = mode_str ,
177+ error = str (e )
178+ )
179+
180+ # ═══════════════════════════════════════════════════════════════════════════════
181+ # Route Setup
182+ # ═══════════════════════════════════════════════════════════════════════════════
183+
184+ def setup_routes (self ): # Configure all routes
185+ self .add_route_get (self .status )
186+ self .add_route_get (self .services )
187+ self .add_route_get (self .service__get )
188+ self .add_route_get (self .health )
189+ self .add_route_get (self .health__service )
190+ return self
0 commit comments