Skip to content

Commit b076dbd

Browse files
committed
Merge dev into main
2 parents 7c1287c + 0704005 commit b076dbd

16 files changed

Lines changed: 1072 additions & 5 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# OSBot-Fast-API
22

3-
![Current Release](https://img.shields.io/badge/release-v0.34.0-blue)
3+
![Current Release](https://img.shields.io/badge/release-v0.34.2-blue)
44
![Python](https://img.shields.io/badge/python-3.8+-green)
55
![FastAPI](https://img.shields.io/badge/FastAPI-0.100+-red)
66
![Type-Safe](https://img.shields.io/badge/Type--Safe-✓-brightgreen)

docs/dev-briefs/v0.32.2__fast-api-registry-refactor/debrief__registry_save_restore_feature.md renamed to docs/dev-briefs/v0.32.2__fast-api-registry-refactor/v0.33.3__debrief__registry_save_restore_feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Technical Debrief: Registry Config Stack & Context Managers
22

3+
**Version**: v0.33.3
34
**Date**: January 2025
45
**Component**: `Fast_API__Service__Registry`
56
**Package**: `osbot-fast-api`

docs/dev-briefs/v0.32.2__fast-api-registry-refactor/v0.34.0__debrief__unified-service-client-architecture.md

Lines changed: 563 additions & 0 deletions
Large diffs are not rendered by default.

osbot_fast_api/api/Fast_API.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ def add_route_delete(self, function):
8080
self.route_helper().add_route_delete(self.app(), function)
8181
return self
8282

83-
def add_routes(self, class_routes):
84-
class_routes(app=self.app()).setup()
83+
def add_routes(self, class_routes, **kwargs):
84+
class_routes(app=self.app(), **kwargs).setup()
8585
return self
8686

8787
@cache_on_self

osbot_fast_api/core_routes/__init__.py

Whitespace-only changes.
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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

osbot_fast_api/core_routes/registry/__init__.py

Whitespace-only changes.

osbot_fast_api/schemas/__init__.py

Whitespace-only changes.

osbot_fast_api/schemas/core_routes/__init__.py

Whitespace-only changes.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# ═══════════════════════════════════════════════════════════════════════════════
2+
# Schema__Registry__* - Response schemas for service registry routes
3+
# ═══════════════════════════════════════════════════════════════════════════════
4+
5+
from typing import List, Optional
6+
from osbot_utils.type_safe.Type_Safe import Type_Safe
7+
8+
9+
class Schema__Registry__Service__Health(Type_Safe): # Health status for a service
10+
service_name : str # Service name
11+
healthy : bool # Health check result
12+
mode : str # Current mode
13+
error : str = None # Error message if unhealthy
14+
15+
16+
class Schema__Registry__Status(Type_Safe): # Overall registry status
17+
registered_count : int # Number of registered services
18+
stack_depth : int # Config stack depth (for save/restore)
19+
services : List[str] # List of registered service names
20+
21+
22+
class Schema__Registry__Health__Summary(Type_Safe): # Health summary for all services
23+
total_services : int # Total registered
24+
healthy_count : int # Number healthy
25+
unhealthy_count : int # Number unhealthy
26+
services : List[Schema__Registry__Service__Health] # Individual results

0 commit comments

Comments
 (0)