Skip to content

Commit 8b28cd2

Browse files
committed
added new Fast_API__Routes__Paths class which outputs a json and html version of the current paths
1 parent e5ead0c commit 8b28cd2

14 files changed

Lines changed: 172 additions & 57 deletions

osbot_fast_api/api/Fast_API.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
from osbot_utils.type_safe.primitives.safe_str.git.Safe_Str__Version import Safe_Str__Version
66
from osbot_utils.type_safe.primitives.safe_str.identifiers.Random_Guid import Random_Guid
77
from starlette.staticfiles import StaticFiles
8-
from osbot_fast_api.api.Fast_API__Offline_Docs import Fast_API__Offline_Docs, FILE_PATH__STATIC__DOCS, \
9-
URL__STATIC__DOCS, NAME__STATIC__DOCS
8+
from osbot_fast_api.api.Fast_API__Offline_Docs import Fast_API__Offline_Docs, FILE_PATH__STATIC__DOCS, URL__STATIC__DOCS, NAME__STATIC__DOCS
109
from osbot_fast_api.api.events.Fast_API__Http_Events import Fast_API__Http_Events
10+
from osbot_fast_api.api.routes.Routes__Config import Routes__Config
1111
from osbot_fast_api.schemas.Safe_Str__Fast_API__Name import Safe_Str__Fast_API__Name
1212
from osbot_fast_api.schemas.Safe_Str__Fast_API__Route__Prefix import Safe_Str__Fast_API__Route__Prefix
1313
from osbot_fast_api.utils.Version import version__osbot_fast_api
@@ -166,12 +166,11 @@ def setup_middlewares(self): # overwrite to add more middlewares
166166
def setup_routes (self): return self # overwrite to add rules
167167

168168
def setup_default_routes(self):
169-
from osbot_fast_api.api.routes.Routes_Config import Routes_Config
170169

171170
if self.default_routes:
172171
self.setup_add_root_route()
173172
self.setup_offline_docs ()
174-
self.add_routes(Routes_Config)
173+
self.add_routes(Routes__Config)
175174

176175
def setup_add_root_route(self):
177176
from starlette.responses import RedirectResponse

osbot_fast_api/api/Fast_API__Offline_Docs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from osbot_utils.utils.Files import path_combine, file_not_exists, file_create_bytes, parent_folder, folder_create
2-
from osbot_utils.utils.Http import GET, GET_bytes
2+
from osbot_utils.utils.Http import GET_bytes
33

44
import osbot_fast_api
55
from fastapi import FastAPI

osbot_fast_api/api/events/Fast_API__Http_Events.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import types
22
from collections import deque
3-
from osbot_utils.type_safe.Type_Safe import Type_Safe
3+
from osbot_utils.type_safe.Type_Safe import Type_Safe
44
from osbot_utils.helpers.trace.Trace_Call__Config import Trace_Call__Config
55

66

osbot_fast_api/api/routes/Routes_Config.py

Lines changed: 0 additions & 27 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from fastapi.responses import HTMLResponse
2+
from osbot_fast_api.api.routes.Fast_API__Routes import Fast_API__Routes
3+
from osbot_fast_api.utils.Fast_API__Routes__Paths import Fast_API__Routes__Paths
4+
from osbot_fast_api.utils.Fast_API__Server_Info import fast_api__server_info
5+
from osbot_fast_api.utils.Version import version__osbot_fast_api
6+
7+
8+
class Routes__Config(Fast_API__Routes):
9+
tag = 'config'
10+
11+
def info(self):
12+
return fast_api__server_info.json()
13+
14+
def status(self):
15+
return {'status':'ok'}
16+
17+
def version(self):
18+
return {'version': version__osbot_fast_api}
19+
20+
def routes__json(self):
21+
return Fast_API__Routes__Paths(app=self.app).routes_tree()
22+
23+
def routes__html(self):
24+
html_content = Fast_API__Routes__Paths(app=self.app).routes_html()
25+
return HTMLResponse(content=html_content)
26+
27+
def setup_routes(self):
28+
self.add_route_get(self.info )
29+
self.add_route_get(self.status )
30+
self.add_route_get(self.version )
31+
self.add_route_get(self.routes__json)
32+
self.add_route_get(self.routes__html)

osbot_fast_api/schemas/Safe_Str__Fast_API__Route__Tag.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import re
2-
32
from osbot_utils.type_safe.primitives.safe_str.Safe_Str import Safe_Str
43

54
TYPE_SAFE_STR__FASTAPI__ROUTE__REGEX = re.compile(r'[^a-zA-Z0-9\-_/{}.]')
Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
11
import re
22

3-
REGEX__SAFE__STR__FAST_API__TITLE = re.compile(r'[^a-zA-Z0-9 _()-]')
3+
REGEX__SAFE__STR__FAST_API__TITLE = re.compile(r'[^a-zA-Z0-9 _()-]')
4+
5+
6+
EXPECTED_ROUTES_METHODS = ['info', 'redirect_to_docs', 'routes__html', 'routes__json', 'status', 'version' ]
7+
EXPECTED_ROUTES_PATHS = ['/' ,
8+
'/config/info' ,
9+
'/config/routes/html',
10+
'/config/routes/json',
11+
'/config/status' ,
12+
'/config/version' ]
13+
EXPECTED_DEFAULT_ROUTES = ['/docs', '/openapi.json', '/redoc', '/static-docs' ]
14+
15+
16+
ROUTES__CONFIG = [{ 'http_methods': ['GET' ], 'http_path': '/config/info' , 'method_name': 'info' },
17+
{ 'http_methods': ['GET' ], 'http_path': '/config/status' , 'method_name': 'status' },
18+
{ 'http_methods': ['GET' ], 'http_path': '/config/version' , 'method_name': 'version' },
19+
{ 'http_methods': ['GET' ], 'http_path': '/config/routes/json', 'method_name': 'routes__json'},
20+
{ 'http_methods': ['GET' ], 'http_path': '/config/routes/html', 'method_name': 'routes__html'}]
21+
ROUTES__STATIC_DOCS = [{'http_methods': ['GET', 'HEAD'], 'http_path': '/static-docs' , 'method_name': 'static-docs' }]
22+
ROUTES_PATHS__CONFIG = ['/config/status', '/config/version']
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from typing import Dict, Any, List
2+
from fastapi import FastAPI
3+
from osbot_utils.type_safe.Type_Safe import Type_Safe
4+
5+
6+
class Fast_API__Routes__Paths(Type_Safe):
7+
app: FastAPI
8+
9+
def routes_tree(self) -> Dict[str, Any]: # Returns a hierarchical view of all routes and mounts
10+
routes_data = { 'title' : self.app.title ,
11+
'version' : self.app.version ,
12+
'description': self.app.description ,
13+
'routes' : [] ,
14+
'mounts' : [] }
15+
16+
for route in self.app.routes: # Process regular routes
17+
if hasattr(route, 'path') and hasattr(route, 'methods'):
18+
route_info = {
19+
'path': route.path,
20+
'methods': list(route.methods) if route.methods else [],
21+
'name': route.name,
22+
#'tags': getattr(route, 'tags', [])
23+
}
24+
routes_data['routes'].append(route_info)
25+
26+
27+
elif hasattr(route, 'path') and hasattr(route, 'app'): # Handle mounts
28+
mount_info = {
29+
'path': route.path,
30+
'type': type(route.app).__name__,
31+
'routes': self.get_mount_routes(route.app)
32+
}
33+
routes_data['mounts'].append(mount_info)
34+
35+
return routes_data
36+
37+
def get_mount_routes(self, mounted_app) -> List[Dict]: # Extract routes from a mounted application
38+
routes = []
39+
if hasattr(mounted_app, 'routes'):
40+
for route in mounted_app.routes:
41+
if hasattr(route, 'path'):
42+
routes.append({
43+
'path': route.path,
44+
'methods': list(getattr(route, 'methods', []))
45+
})
46+
return routes
47+
48+
def routes_html(self) -> str: # Returns an HTML page with all routes
49+
routes_data = self.routes_tree()
50+
51+
html_content = f"""
52+
<!DOCTYPE html>
53+
<html>
54+
<head>
55+
<title>{routes_data['title']} - Routes Overview</title>
56+
<style>
57+
body {{ font-family: Arial, sans-serif; margin: 20px; }}
58+
h1 {{ color: #333; }}
59+
.route {{ margin: 10px 0; padding: 10px; border-left: 3px solid #4CAF50; }}
60+
.mount {{ margin: 10px 0; padding: 10px; border-left: 3px solid #2196F3; }}
61+
.method {{
62+
display: inline-block;
63+
padding: 2px 8px;
64+
margin-right: 5px;
65+
border-radius: 3px;
66+
font-size: 12px;
67+
font-weight: bold;
68+
}}
69+
.GET {{ background: #61affe; color: white; }}
70+
.POST {{ background: #49cc90; color: white; }}
71+
.PUT {{ background: #fca130; color: white; }}
72+
.DELETE {{ background: #f93e3e; color: white; }}
73+
.path {{ font-family: monospace; }}
74+
</style>
75+
</head>
76+
<body>
77+
<h1>{routes_data['title']} API Routes</h1>
78+
<p>Version: {routes_data['version']}</p>
79+
80+
<h2>Routes</h2>
81+
{"".join(self.format_route_html(r) for r in routes_data['routes'])}
82+
83+
<h2>Mounted Applications</h2>
84+
{"".join(self.format_mount_html(m) for m in routes_data['mounts'])}
85+
</body>
86+
</html>
87+
"""
88+
return html_content
89+
90+
def format_route_html(self, route):
91+
methods_html = "".join(f'<span class="method {m}">{m}</span>' for m in route['methods'])
92+
return f'<div class="route">{methods_html} <span class="path">{route["path"]}</span></div>'
93+
94+
def format_mount_html(self, mount):
95+
return f'<div class="mount"><strong>{mount["path"]}</strong> ({mount["type"]})</div>'
96+

tests/unit/api/routes/http_shell/test_Http_Shell__Client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def test__fast_api_server(self):
5656
del options_headers['date']
5757

5858
assert response_shell_invoke.json() == expected_result
59-
assert self.fast_api.routes_paths() == ['/', '/config/info', '/config/status', '/config/version', '/http-shell-server']
59+
assert self.fast_api.routes_paths() == ['/', '/config/info', '/config/routes/html', '/config/routes/json', '/config/status', '/config/version', '/http-shell-server']
6060
assert self.fast_api_server.port > 19999
6161
assert self.fast_api_server.is_port_open() is True
6262
assert response_options.json() == { "detail" : "Method Not Allowed" }

tests/unit/api/routes/test_Router_Config.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
from unittest import TestCase
22
from fastapi import FastAPI
33
from starlette.testclient import TestClient
4-
from osbot_fast_api.api.routes.Routes_Config import Routes_Config
4+
from osbot_fast_api.api.routes.Routes__Config import Routes__Config
55
from osbot_fast_api.utils.Version import Version
66

77

8-
class test_Routes_Config(TestCase):
8+
class test_Routes__Config(TestCase):
99

1010
def setUp(self):
1111
self.app = FastAPI()
12-
self.routes_config = Routes_Config(app=self.app).setup()
12+
self.routes_config = Routes__Config(app=self.app).setup()
1313
self.client = TestClient(self.app)
1414

1515
def test__init__(self):
16-
assert type(self.routes_config) is Routes_Config
16+
assert type(self.routes_config) is Routes__Config
1717
assert self.routes_config.tag == 'config'
1818
assert self.routes_config.app == self.app
1919
assert self.routes_config.router is not None
@@ -29,7 +29,9 @@ def test_client__version(self):
2929
assert response.json() == {'version': Version().value()}
3030

3131
def test_routes(self):
32-
expected_routes = [{'http_methods': ['GET'], 'http_path': '/info' , 'method_name': 'info' },
33-
{'http_methods': ['GET'], 'http_path': '/status' , 'method_name': 'status' },
34-
{'http_methods': ['GET'], 'http_path': '/version', 'method_name': 'version'}]
32+
expected_routes = [{'http_methods': ['GET'], 'http_path': '/info' , 'method_name': 'info' },
33+
{'http_methods': ['GET'], 'http_path': '/status' , 'method_name': 'status' },
34+
{'http_methods': ['GET'], 'http_path': '/version' , 'method_name': 'version' },
35+
{'http_methods': ['GET'], 'http_path': '/routes/json', 'method_name': 'routes__json'},
36+
{'http_methods': ['GET'], 'http_path': '/routes/html', 'method_name': 'routes__html'}]
3537
assert self.routes_config.routes() == expected_routes

0 commit comments

Comments
 (0)