Skip to content

Commit 86fd3ec

Browse files
committed
major new feature: new Admin UI :)
1 parent dfca758 commit 86fd3ec

26 files changed

Lines changed: 6025 additions & 2 deletions

osbot_fast_api/admin_ui/__init__.py

Whitespace-only changes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from osbot_utils.type_safe.Type_Safe import Type_Safe
2+
from osbot_fast_api.schemas.Safe_Str__Fast_API__Route__Prefix import Safe_Str__Fast_API__Route__Prefix
3+
4+
class Admin_UI__Config(Type_Safe): # Configuration for Admin UI
5+
enabled : bool = True
6+
base_path : Safe_Str__Fast_API__Route__Prefix = '/admin'
7+
require_auth : bool = True
8+
show_dashboard : bool = True
9+
show_cookies : bool = True
10+
show_routes : bool = True
11+
show_docs : bool = True
12+
allow_api_testing: bool = True
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import osbot_fast_api
2+
from osbot_utils.type_safe.primitives.safe_str.text.Safe_Str__Text import Safe_Str__Text
3+
from osbot_utils.utils.Files import path_combine
4+
from osbot_fast_api.admin_ui.api.Admin_UI__Config import Admin_UI__Config
5+
from osbot_fast_api.admin_ui.api.routes.Routes__Admin__Config import Routes__Admin__Config
6+
from osbot_fast_api.admin_ui.api.routes.Routes__Admin__Cookies import Routes__Admin__Cookies
7+
from osbot_fast_api.admin_ui.api.routes.Routes__Admin__Docs import Routes__Admin__Docs
8+
from osbot_fast_api.admin_ui.api.routes.Routes__Admin__Info import Routes__Admin__Info
9+
from osbot_fast_api.admin_ui.api.routes.Routes__Admin__Static import Routes__Admin__Static
10+
from osbot_fast_api.api.Fast_API import Fast_API
11+
from osbot_fast_api.schemas.Safe_Str__Fast_API__Name import Safe_Str__Fast_API__Name
12+
from osbot_fast_api.schemas.Safe_Str__Fast_API__Route__Prefix import Safe_Str__Fast_API__Route__Prefix
13+
14+
15+
class Admin_UI__Fast_API(Fast_API): # Admin UI for FastAPI applications.
16+
add_admin_ui : bool = False # in the admin ui we can't add an admin ui :)
17+
admin_config : Admin_UI__Config
18+
base_path : Safe_Str__Fast_API__Route__Prefix = '/admin'
19+
description : Safe_Str__Text = 'Administration interface for FastAPI applications'
20+
docs_offline : bool = True
21+
default_routes: bool = False
22+
name : Safe_Str__Fast_API__Name = 'FastAPI Admin UI'
23+
parent_app : Fast_API
24+
25+
def __init__(self, **kwargs):
26+
super().__init__(**kwargs)
27+
if self.admin_config.base_path: # todo we should move this into a method that applies all changes we might have in admin_config
28+
self.base_path = self.admin_config.base_path
29+
30+
def path_static_folder(self): # Override to serve admin UI static files
31+
return path_combine(osbot_fast_api.path, 'admin_ui/static') # todo move string to consts__Fast_API__Admin_UI
32+
33+
def setup_routes(self): # Set up all admin UI routes"""
34+
Routes__Admin__Info .parent_app = self.parent_app # Pass parent_app reference to routes that need it
35+
Routes__Admin__Config .parent_app = self.parent_app # todo: see if there is a better way to do this, since these routes (like Routes__Admin__Info) have access to the .app() object
36+
Routes__Admin__Cookies .parent_app = self.parent_app
37+
Routes__Admin__Docs .parent_app = self.parent_app
38+
39+
if self.admin_config.show_dashboard: self.add_routes(Routes__Admin__Info ) # Add API routes (depending on config)
40+
if self.admin_config.show_routes: self.add_routes(Routes__Admin__Config )
41+
if self.admin_config.show_cookies: self.add_routes(Routes__Admin__Cookies)
42+
if self.admin_config.show_docs: self.add_routes(Routes__Admin__Docs )
43+
44+
self.add_routes(Routes__Admin__Static) # Add static file serving for the UI
45+
return self

osbot_fast_api/admin_ui/api/__init__.py

Whitespace-only changes.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from typing import Dict, Any, List
2+
from osbot_fast_api.api.routes.Fast_API__Routes import Fast_API__Routes
3+
4+
5+
class Routes__Admin__Config(Fast_API__Routes): # API routes for configuration and route management
6+
tag = 'admin-config'
7+
parent_app = None # Will be set by Admin_UI__Fast_API
8+
9+
def api__routes(self) -> List[Dict[str, Any]]: # Get all application routes
10+
if not self.parent_app:
11+
return []
12+
13+
routes = self.parent_app.routes(include_default=False, expand_mounts=True)
14+
15+
# Enhance route information
16+
enhanced_routes = []
17+
for route in routes:
18+
enhanced = { "path" : route.get('http_path' ),
19+
"methods" : route.get('http_methods', []),
20+
"name" : route.get('method_name' ),
21+
"tag" : self._extract_tag_from_path(route.get('http_path', '')),
22+
"is_get" : 'GET' in route.get('http_methods', []),
23+
"is_post" : 'POST' in route.get('http_methods', []),
24+
"is_put" : 'PUT' in route.get('http_methods', []),
25+
"is_delete": 'DELETE' in route.get('http_methods', [])}
26+
enhanced_routes.append(enhanced)
27+
28+
return enhanced_routes
29+
30+
def api__routes_grouped(self) -> Dict[str, List[Dict[str, Any]]]: # Get routes grouped by prefix/tag
31+
routes = self.api__routes()
32+
33+
grouped = {}
34+
for route in routes:
35+
tag = route['tag']
36+
if tag not in grouped:
37+
grouped[tag] = []
38+
grouped[tag].append(route)
39+
40+
return grouped
41+
42+
def api__middlewares(self) -> List[Dict[str, Any]]: # Get middleware information
43+
if not self.parent_app:
44+
return []
45+
46+
return self.parent_app.user_middlewares()
47+
48+
def api__openapi_spec(self) -> Dict[str, Any]: # Get OpenAPI specification
49+
if not self.parent_app:
50+
return {}
51+
52+
return self.parent_app.open_api_json()
53+
54+
def _extract_tag_from_path(self, path: str) -> str: # Extract a tag/category from the route path
55+
if not path or path == '/':
56+
return 'root'
57+
58+
parts = path.strip('/').split('/')
59+
if parts:
60+
return parts[0]
61+
return 'other'
62+
63+
def setup_routes(self):
64+
self.add_route_get(self.api__routes )
65+
self.add_route_get(self.api__routes_grouped )
66+
self.add_route_get(self.api__middlewares )
67+
self.add_route_get(self.api__openapi_spec )
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
from typing import Dict, Any, List, Optional
2+
from fastapi import Request, Response
3+
from osbot_fast_api.api.routes.Fast_API__Routes import Fast_API__Routes
4+
from osbot_utils.type_safe.Type_Safe import Type_Safe
5+
from osbot_utils.utils.Misc import random_guid
6+
7+
8+
class Cookie__Config(Type_Safe): # Configuration for a cookie
9+
name : str
10+
description : str = ""
11+
required : bool = False
12+
secure : bool = True
13+
http_only : bool = True
14+
same_site : str = "strict"
15+
category : str = "general"
16+
validator : str = None # Optional validation pattern
17+
18+
19+
class Cookie__Value(Type_Safe): # Cookie value to set
20+
value : str
21+
expires_in : Optional[int] = None # Seconds until expiration
22+
23+
24+
class Cookie__Template(Type_Safe): # Template for common cookie configurations
25+
id : str
26+
name : str
27+
description : str
28+
cookies : List[Cookie__Config]
29+
30+
class Cookies__Templates(Type_Safe):
31+
cookies_templates : List[Cookie__Template]
32+
33+
class Routes__Admin__Cookies(Fast_API__Routes): # API routes for cookie management
34+
tag = 'admin-cookies'
35+
parent_app = None # Will be set by Admin_UI__Fast_API
36+
37+
# Predefined cookie templates
38+
COOKIE_TEMPLATES = [ # todo: move to .json file which can then be changed by the multiple implementations (especialy since by default we should not have have all this cookies set in the main Fast_API package)
39+
{
40+
"id": "openai",
41+
"name": "OpenAI Configuration",
42+
"description": "Cookies for OpenAI API integration",
43+
"cookies": [
44+
{
45+
"name": "openai-api-key",
46+
"description": "OpenAI API Key",
47+
"required": True,
48+
"category": "llm",
49+
"validator": "^sk-[a-zA-Z0-9]{48}$"
50+
},
51+
{
52+
"name": "openai-org-id",
53+
"description": "OpenAI Organization ID",
54+
"required": False,
55+
"category": "llm"
56+
}
57+
]
58+
},
59+
{
60+
"id": "anthropic",
61+
"name": "Anthropic Configuration",
62+
"description": "Cookies for Anthropic Claude API",
63+
"cookies": [
64+
{
65+
"name": "anthropic-api-key",
66+
"description": "Anthropic API Key",
67+
"required": True,
68+
"category": "llm",
69+
"validator": "^sk-ant-[a-zA-Z0-9-]{95}$"
70+
}
71+
]
72+
},
73+
{
74+
"id": "groq",
75+
"name": "Groq Configuration",
76+
"description": "Cookies for Groq API",
77+
"cookies": [
78+
{
79+
"name": "groq-api-key",
80+
"description": "Groq API Key",
81+
"required": True,
82+
"category": "llm"
83+
}
84+
]
85+
},
86+
{
87+
"id": "auth",
88+
"name": "Authentication",
89+
"description": "Authentication cookies",
90+
"cookies": [
91+
{
92+
"name": "auth-token",
93+
"description": "Authentication token",
94+
"required": False,
95+
"category": "auth"
96+
},
97+
{
98+
"name": "api-key",
99+
"description": "API Key for protected endpoints",
100+
"required": False,
101+
"category": "auth"
102+
}
103+
]
104+
}
105+
]
106+
107+
def api__cookies_list(self, request: Request) -> List[Dict[str, Any]]: # Get list of all cookies with their current values
108+
cookies = []
109+
110+
all_cookie_configs = {} # Get all cookie configurations from templates
111+
for template in self.COOKIE_TEMPLATES:
112+
for cookie_config in template['cookies']:
113+
if cookie_config['name'] not in all_cookie_configs:
114+
all_cookie_configs[cookie_config['name']] = cookie_config
115+
116+
for cookie_name, config in all_cookie_configs.items(): # Add current values from request
117+
current_value = request.cookies.get(cookie_name)
118+
cookies.append({ "name" : cookie_name ,
119+
"description" : config.get('description', '' ),
120+
"category" : config.get('category' , 'general' ),
121+
"required" : config.get('required' , False ),
122+
"has_value" : current_value is not None ,
123+
"value_length" : len(current_value) if current_value else 0,
124+
"is_valid" : self._validate_cookie_value(current_value, config.get('validator'))})
125+
126+
return cookies
127+
128+
def api__cookies_templates(self) -> List[Dict[str, Any]]: # Get available cookie templates
129+
return self.COOKIE_TEMPLATES # todo: see note above in COOKIE_TEMPLATES
130+
131+
def api__cookie_get(self, cookie_name: str, request: Request) -> Dict[str, Any]: # Get a specific cookie value
132+
value = request.cookies.get(cookie_name)
133+
134+
config = None # Find cookie config
135+
for template in self.COOKIE_TEMPLATES:
136+
for cookie_config in template['cookies']:
137+
if cookie_config['name'] == cookie_name:
138+
config = cookie_config
139+
break
140+
141+
return { "name" : cookie_name , # todo: convert this to a Schema_* class
142+
"value" : value ,
143+
"exists" : value is not None,
144+
"config" : config ,
145+
"is_valid": self._validate_cookie_value(value, config.get('validator') if config else None) }
146+
147+
def api__cookie_set__cookie_name(self, cookie_name : str ,
148+
cookie_value : Cookie__Value ,
149+
request : Request ,
150+
response : Response
151+
) -> Dict[str, Any]: # Set a cookie value
152+
config = None # Find cookie config
153+
for template in self.COOKIE_TEMPLATES:
154+
for cookie_config in template['cookies']:
155+
if cookie_config['name'] == cookie_name:
156+
config = cookie_config
157+
break
158+
159+
if not config: # Allow setting unknown cookies with defaults
160+
config = { "secure" : request.url.scheme == 'https', # todo: convert this to a Schema_* class
161+
"http_only" : True ,
162+
"same_site" : "strict" }
163+
164+
if config.get('validator') and not self._validate_cookie_value(cookie_value.value, config['validator']): # Validate if validator exists
165+
return { "success": False,
166+
"error": f"Value does not match required pattern: {config['validator']}" } # todo: convert this to a Schema_* class
167+
168+
# Set the cookie
169+
response.set_cookie(key = cookie_name ,
170+
value = cookie_value.value ,
171+
httponly = config.get('http_only', True ),
172+
secure = config.get('secure' , True ),
173+
samesite = config.get('same_site', 'strict' ),
174+
max_age = cookie_value.expires_in if cookie_value.expires_in else None)
175+
176+
return { "success" : True , # todo: convert this to a Schema_* class
177+
"name" : cookie_name , # we also need to standardise the return object/class
178+
"value_set": len(cookie_value.value) > 0 }
179+
180+
def api__cookie_delete(self, cookie_name: str, response: Response) -> Dict[str, Any]: # Delete a cookie
181+
response.delete_cookie(key=cookie_name)
182+
183+
return { "success" : True , # todo: convert this to a Schema_* class
184+
"name" : cookie_name,
185+
"deleted" : True }
186+
187+
def api__cookies_bulk_set(self, cookies_templates : Cookies__Templates, #cookies: List[Dict[str, str]],
188+
request : Request ,
189+
response : Response
190+
) -> Dict[str, Any]: # Set multiple cookies at once"""
191+
results = []
192+
193+
for cookie_data in cookies_templates:
194+
cookie_name = cookie_data.get('name')
195+
cookie_value = Cookie__Value(value=cookie_data.get('value', ''))
196+
197+
if cookie_name:
198+
result = self.api__cookie_set(cookie_name, cookie_value, request, response)
199+
results.append(result)
200+
201+
return {
202+
"success": all(r.get('success') for r in results),
203+
"results": results
204+
}
205+
206+
def api__generate_value(self, value_type: str = "uuid") -> Dict[str, str]:
207+
"""Generate a value for cookies (UUID, random string, etc.)"""
208+
if value_type == "uuid":
209+
return {"value": random_guid(), "type": "uuid"}
210+
elif value_type == "api_key":
211+
# Generate a mock API key format
212+
import random
213+
import string
214+
prefix = "sk-"
215+
random_part = ''.join(random.choices(string.ascii_letters + string.digits, k=48))
216+
return {"value": f"{prefix}{random_part}", "type": "api_key"}
217+
else:
218+
return {"value": random_guid(), "type": "default"}
219+
220+
def _validate_cookie_value(self, value: Optional[str], pattern: Optional[str]) -> bool:
221+
"""Validate a cookie value against a pattern"""
222+
if not pattern or not value:
223+
return True
224+
225+
try:
226+
import re
227+
return bool(re.match(pattern, value))
228+
except:
229+
return True
230+
231+
def setup_routes(self):
232+
self.add_route_get (self.api__cookies_list )
233+
self.add_route_get (self.api__cookies_templates )
234+
self.add_route_get (self.api__cookie_get )
235+
self.add_route_post (self.api__cookie_set__cookie_name )
236+
self.add_route_delete(self.api__cookie_delete )
237+
self.add_route_post (self.api__cookies_bulk_set )
238+
self.add_route_get (self.api__generate_value )

0 commit comments

Comments
 (0)