Skip to content

Commit 356203c

Browse files
committed
added Brief (v2): Unified Service Client Architecture
added Fast_API__Client__Requests refactored Fast_API__Service__Registry to support multiple configs
1 parent 0e36de2 commit 356203c

19 files changed

Lines changed: 1577 additions & 455 deletions

docs/dev-briefs/v0.32.2__fast-api-registry-refactor/v0.32.5__brief__unified-service-client-architecture.md renamed to docs/dev-briefs/v0.32.2__fast-api-registry-refactor/v0.32.5__brief_(v1)__unified-service-client-architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Development Brief: Unified Service Client Architecture
1+
# Development Brief (v1): Unified Service Client Architecture
22

33
**Version**: v0.32.5
44
**Date**: January 2025

docs/dev-briefs/v0.32.2__fast-api-registry-refactor/v0.33.1__brief_(v2)__unified-service-client-architecture.md.md

Lines changed: 791 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# ═══════════════════════════════════════════════════════════════════════════════
2+
# Fast_API__Client__Requests
3+
# Generic transport layer for service clients
4+
# Handles IN_MEMORY (TestClient) and REMOTE (requests) modes transparently
5+
# ═══════════════════════════════════════════════════════════════════════════════
6+
7+
import requests
8+
9+
from typing import Any, Dict, Type
10+
from starlette.testclient import TestClient
11+
from osbot_fast_api.services.registry.Fast_API__Service__Registry import fast_api__service__registry
12+
from osbot_fast_api.services.schemas.registry.Fast_API__Service__Registry__Client__Config import Fast_API__Service__Registry__Client__Config
13+
from osbot_fast_api.services.schemas.registry.enums.Enum__Fast_API__Service__Registry__Client__Mode import Enum__Fast_API__Service__Registry__Client__Mode
14+
from osbot_utils.type_safe.Type_Safe import Type_Safe
15+
from osbot_utils.decorators.methods.cache_on_self import cache_on_self
16+
17+
# todo: service_type should have a specific base class
18+
class Fast_API__Client__Requests(Type_Safe): # Generic transport for all service clients
19+
service_type : Type[Type_Safe] = None # Subclass sets this to client type
20+
21+
@cache_on_self
22+
def config(self) -> Fast_API__Service__Registry__Client__Config: # Cached config lookup from registry
23+
config = fast_api__service__registry.config(self.service_type)
24+
if config is None:
25+
raise ValueError(f"{self.service_type.__name__} not registered in service registry")
26+
return config
27+
28+
@cache_on_self
29+
def test_client(self) -> TestClient: # TestClient for IN_MEMORY mode
30+
if self.config().fast_api_app is None:
31+
raise ValueError("IN_MEMORY mode requires fast_api_app in config")
32+
return TestClient(self.config().fast_api_app)
33+
34+
@cache_on_self
35+
def session(self) -> requests.Session: # requests.Session for REMOTE mode
36+
session = requests.Session()
37+
if self.config().api_key_value:
38+
session.headers['Authorization'] = f'Bearer {self.config().api_key_value}'
39+
return session
40+
41+
def execute(self, method : str , # HTTP method (GET, POST, etc)
42+
path : str , # Endpoint path
43+
body : Any = None, # Request body
44+
headers : Dict = None # Additional headers
45+
): # Execute request based on mode
46+
request_headers = {**self.auth_headers(), **(headers or {})}
47+
48+
if self.config().mode == Enum__Fast_API__Service__Registry__Client__Mode.IN_MEMORY:
49+
return self.execute_in_memory(method, path, body, request_headers)
50+
elif self.config().mode == Enum__Fast_API__Service__Registry__Client__Mode.REMOTE:
51+
return self.execute_remote(method, path, body, request_headers)
52+
else:
53+
raise ValueError("Client mode not configured")
54+
55+
def execute_in_memory(self, method: str, path: str, body: Any, headers: Dict):
56+
method_func = getattr(self.test_client(), method.lower())
57+
if body:
58+
if type(body) is bytes:
59+
headers["Content-Type"] = "application/octet-stream"
60+
return method_func(path, data=body, headers=headers)
61+
else:
62+
return method_func(path, json=body, headers=headers)
63+
return method_func(path, headers=headers)
64+
65+
def execute_remote(self, method: str, path: str, body: Any, headers: Dict):
66+
url = f"{self.config().base_url}{path}"
67+
method_func = getattr(self.session(), method.lower())
68+
if body:
69+
if type(body) is bytes:
70+
headers["Content-Type"] = "application/octet-stream"
71+
return method_func(url, data=body, headers=headers)
72+
else:
73+
return method_func(url, json=body, headers=headers)
74+
return method_func(url, headers=headers)
75+
76+
def auth_headers(self) -> Dict[str, str]: # Get auth headers from config
77+
headers = {}
78+
if self.config().api_key_name and self.config().api_key_value:
79+
headers[str(self.config().api_key_name)] = str(self.config().api_key_value)
80+
return headers
Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,85 @@
11
# ═══════════════════════════════════════════════════════════════════════════════
22
# Fast_API__Service__Registry
3-
# Central registry for service client discovery across deployment modes
4-
# Enables zero-code-change switching between IN_MEMORY and REMOTE modes
3+
# Central registry for service client configuration
4+
# Stores configs keyed by client type - clients are stateless facades
5+
# Supports save/restore and context managers for test isolation and hot-swapping
56
# ═══════════════════════════════════════════════════════════════════════════════
6-
from typing import Type
7-
from osbot_fast_api.services.registry.Fast_API__Service__Registry__Client__Base import Fast_API__Service__Registry__Client__Base
8-
from osbot_fast_api.services.schemas.registry.collections.Dict__Fast_API__Service__Clients_By_Type import Dict__Fast_API__Service__Clients_By_Type
9-
from osbot_fast_api.services.schemas.registry.collections.List__Fast_API__Service__Client_Types import List__Fast_API__Service__Client_Types
10-
from osbot_utils.type_safe.Type_Safe import Type_Safe
11-
from osbot_utils.type_safe.type_safe_core.decorators.type_safe import type_safe
127

8+
from contextlib import contextmanager
9+
from osbot_fast_api.services.schemas.registry.Fast_API__Service__Registry__Client__Config import Fast_API__Service__Registry__Client__Config
10+
from osbot_fast_api.services.schemas.registry.collections.Dict__Fast_API__Service__Configs_By_Type import Dict__Fast_API__Service__Configs_By_Type
11+
from osbot_fast_api.services.schemas.registry.collections.List__Fast_API__Service__Configs_Stack import List__Fast_API__Service__Configs_Stack
12+
from osbot_utils.type_safe.Type_Safe import Type_Safe
13+
from osbot_utils.type_safe.type_safe_core.decorators.type_safe import type_safe
1314

14-
class Fast_API__Service__Registry(Type_Safe): # Instance-based registry for service clients
15-
clients : Dict__Fast_API__Service__Clients_By_Type # Type_Safe auto-creates this
1615

17-
@type_safe
18-
def register(self, client: Fast_API__Service__Registry__Client__Base) -> None: # Register a client instance
19-
self.clients[type(client)] = client # Index by actual type
16+
class Fast_API__Service__Registry(Type_Safe): # Config store keyed by client type
17+
configs : Dict__Fast_API__Service__Configs_By_Type # Current configs (auto-created)
18+
configs_stack : List__Fast_API__Service__Configs_Stack # Stack for save/restore (auto-created)
19+
20+
# ───────────────────────────────────────────────────────────────────────────
21+
# Core Registry Operations
22+
# ───────────────────────────────────────────────────────────────────────────
2023

2124
@type_safe
22-
def client(self, client_type: Type[Fast_API__Service__Registry__Client__Base]
23-
) -> Fast_API__Service__Registry__Client__Base: # Retrieve client by type
24-
if client_type not in self.clients:
25-
return None # Not registered = None
26-
return self.clients[client_type] # Return registered client
25+
def register(self , # Register config for a client type
26+
client_type : type ,
27+
config : Fast_API__Service__Registry__Client__Config
28+
) -> None:
29+
self.configs[client_type] = config
30+
31+
def config(self, client_type: type) -> Fast_API__Service__Registry__Client__Config:
32+
if client_type not in self.configs: # Retrieve config by client type
33+
return None
34+
return self.configs[client_type]
35+
36+
def is_registered(self, client_type: type) -> bool:
37+
return client_type in self.configs
38+
39+
def clear(self) -> None: # Reset configs (not stack)
40+
self.configs.clear()
41+
42+
# ───────────────────────────────────────────────────────────────────────────
43+
# Save / Restore - For setUp/tearDown patterns
44+
# ───────────────────────────────────────────────────────────────────────────
45+
46+
def configs__save(self) -> None: # Save current configs to stack
47+
snapshot = Dict__Fast_API__Service__Configs_By_Type()
48+
snapshot.update(self.configs)
49+
self.configs_stack.append(snapshot)
50+
51+
def configs__restore(self) -> None: # Restore configs from stack
52+
if len(self.configs_stack) > 0:
53+
saved = self.configs_stack.pop()
54+
self.configs.clear()
55+
self.configs.update(saved)
56+
57+
def configs__stack_size(self) -> int: # Check stack depth
58+
return len(self.configs_stack)
2759

28-
def is_registered(self, client_type: type) -> bool: # Check if type is registered
29-
return client_type in self.clients
60+
# ───────────────────────────────────────────────────────────────────────────
61+
# Context Managers - For clean test isolation and hot-swapping
62+
# ───────────────────────────────────────────────────────────────────────────
3063

31-
def clear(self) -> None: # Reset registry for test isolation
32-
self.clients.clear()
64+
@contextmanager
65+
def with_registry(self, registry: 'Fast_API__Service__Registry'): # Temporarily use another registry's configs
66+
self.configs__save()
67+
self.configs.clear()
68+
self.configs.update(registry.configs)
69+
try:
70+
yield self
71+
finally:
72+
self.configs__restore()
3373

34-
def registered_types(self) -> List__Fast_API__Service__Client_Types: # List all registered client types
35-
return List__Fast_API__Service__Client_Types(list(self.clients.keys()))
74+
@contextmanager
75+
def with_config(self, client_type: type, # Temporarily override single client's config
76+
config : Fast_API__Service__Registry__Client__Config):
77+
self.configs__save()
78+
self.configs[client_type] = config
79+
try:
80+
yield self
81+
finally:
82+
self.configs__restore()
3683

3784

38-
fast_api__service__registry = Fast_API__Service__Registry() # Singleton instance for convenience
85+
fast_api__service__registry = Fast_API__Service__Registry() # Singleton instance for convenience

osbot_fast_api/services/registry/Fast_API__Service__Registry__Client__Base.py

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,9 @@
33
# Abstract base class that all service clients must inherit
44
# Provides the contract for registration with Fast_API__Service__Registry
55
# ═══════════════════════════════════════════════════════════════════════════════
6-
from osbot_fast_api.services.schemas.registry.Fast_API__Service__Registry__Client__Config import Fast_API__Service__Registry__Client__Config
7-
from osbot_fast_api.services.schemas.registry.collections.List__Fast_API__Registry__Env_Vars import List__Fast_API__Registry__Env_Vars
8-
from osbot_utils.type_safe.Type_Safe import Type_Safe
96

10-
class Fast_API__Service__Registry__Client__Base(Type_Safe): # Base class for all service clients
11-
config : Fast_API__Service__Registry__Client__Config # Configuration for this client
7+
from osbot_utils.type_safe.Type_Safe import Type_Safe
128

13-
def setup_from_env(self) -> 'Fast_API__Service__Registry__Client__Base': # Configure from environment variables
14-
raise NotImplementedError("Subclass must implement setup_from_env()")
159

16-
def requests(self): # Return the *__Requests transport object
17-
raise NotImplementedError("Subclass must implement requests()")
18-
19-
def health(self) -> bool: # Basic health check
20-
raise NotImplementedError("Subclass must implement health()")
21-
22-
@classmethod
23-
def env_vars(cls) -> List__Fast_API__Registry__Env_Vars: # Expected env vars for this client
24-
raise NotImplementedError("Subclass must implement env_vars()")
25-
26-
@classmethod
27-
def client_name(cls) -> str: # Human-readable name for errors
28-
return cls.__name__
10+
class Fast_API__Service__Registry__Client__Base(Type_Safe): # Base class for all service clients
11+
pass # Marker class - domain clients extend this

osbot_fast_api/services/schemas/registry/Fast_API__Service__Registry__Client__Config.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
# Configuration schema shared by all service clients
44
# ═══════════════════════════════════════════════════════════════════════════════
55

6-
from fastapi import FastAPI
7-
from osbot_utils.type_safe.Type_Safe import Type_Safe
8-
from osbot_fast_api.services.schemas.registry.enums.Enum__Fast_API__Service__Registry__Client__Mode import Enum__Fast_API__Service__Registry__Client__Mode
9-
from osbot_fast_api.services.schemas.registry.safe_str.Safe_Str__Fast_API__Auth__Key_Name import Safe_Str__Fast_API__Auth__Key_Name
10-
from osbot_fast_api.services.schemas.registry.safe_str.Safe_Str__Fast_API__Auth__Key_Value import Safe_Str__Fast_API__Auth__Key_Value
11-
from osbot_utils.type_safe.primitives.domains.web.safe_str.Safe_Str__Url import Safe_Str__Url
6+
from fastapi import FastAPI
7+
from osbot_utils.type_safe.Type_Safe import Type_Safe
8+
from osbot_fast_api.services.schemas.registry.enums.Enum__Fast_API__Service__Registry__Client__Mode import Enum__Fast_API__Service__Registry__Client__Mode
9+
from osbot_utils.type_safe.primitives.domains.http.safe_str.Safe_Str__Http__Header__Name import Safe_Str__Http__Header__Name
10+
from osbot_utils.type_safe.primitives.domains.http.safe_str.Safe_Str__Http__Header__Value import Safe_Str__Http__Header__Value
11+
from osbot_utils.type_safe.primitives.domains.web.safe_str.Safe_Str__Url import Safe_Str__Url
1212

1313

14-
class Fast_API__Service__Registry__Client__Config(Type_Safe): # Pure data - client configuration
15-
mode : Enum__Fast_API__Service__Registry__Client__Mode = None # IN_MEMORY or REMOTE (None = not configured)
16-
fast_api_app : FastAPI = None # FastAPI app for IN_MEMORY mode
17-
base_url : Safe_Str__Url = None # Base URL for REMOTE mode
18-
api_key_name : Safe_Str__Fast_API__Auth__Key_Name = None # HTTP header name for auth
19-
api_key_value : Safe_Str__Fast_API__Auth__Key_Value = None # HTTP header value for auth
14+
class Fast_API__Service__Registry__Client__Config(Type_Safe): # Pure data - client configuration
15+
mode : Enum__Fast_API__Service__Registry__Client__Mode = None # IN_MEMORY or REMOTE (None = not configured)
16+
fast_api_app : FastAPI = None # FastAPI app for IN_MEMORY mode
17+
base_url : Safe_Str__Url = None # Base URL for REMOTE mode
18+
api_key_name : Safe_Str__Http__Header__Name = None # HTTP header name for auth
19+
api_key_value : Safe_Str__Http__Header__Value = None # HTTP header value for auth

0 commit comments

Comments
 (0)