fastapi-voyager 0.5.2__tar.gz → 0.6.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/PKG-INFO +8 -1
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/README.md +7 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/cli.py +8 -2
- fastapi_voyager-0.6.1/src/fastapi_voyager/module.py +99 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/render.py +37 -17
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/server.py +26 -22
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/type.py +8 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/type_helper.py +2 -1
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/version.py +1 -1
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/voyager.py +2 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/component/render-graph.js +1 -1
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/component/schema-field-filter.js +1 -1
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/index.html +10 -10
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/vue-main.js +5 -5
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/tests/demo.py +11 -2
- fastapi_voyager-0.6.1/tests/programatic.py +8 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/tests/test_module.py +5 -5
- fastapi_voyager-0.5.2/src/fastapi_voyager/module.py +0 -64
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/.gitignore +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/.python-version +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/LICENSE +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/pyproject.toml +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/__init__.py +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/filter.py +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/graph-ui.js +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/quasar.min.css +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/quasar.min.js +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/tests/__init__.py +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/tests/demo_anno.py +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/tests/service/__init__.py +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/tests/service/schema.py +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/tests/test_analysis.py +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/tests/test_import.py +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/tests/test_type_helper.py +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/uv.lock +0 -0
- {fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/voyager.jpg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-voyager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Visualize FastAPI application's routing tree and dependencies
|
|
5
5
|
Project-URL: Homepage, https://github.com/allmonday/fastapi-voyager
|
|
6
6
|
Project-URL: Source, https://github.com/allmonday/fastapi-voyager
|
|
@@ -183,6 +183,13 @@ features
|
|
|
183
183
|
- [ ] abstract render module
|
|
184
184
|
- [ ] export voyager core data into json (for better debugging)
|
|
185
185
|
- [ ] add api to rebuild core data from json, and render it
|
|
186
|
+
- [ ] fix Generic case
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
class Paged(BaseModel, Generic[T]):
|
|
190
|
+
total: int
|
|
191
|
+
items: List[T]
|
|
192
|
+
```
|
|
186
193
|
|
|
187
194
|
bugs:
|
|
188
195
|
- [ ] fix duplicated link from class and parent class, it also break clicking highlight
|
|
@@ -156,6 +156,13 @@ features
|
|
|
156
156
|
- [ ] abstract render module
|
|
157
157
|
- [ ] export voyager core data into json (for better debugging)
|
|
158
158
|
- [ ] add api to rebuild core data from json, and render it
|
|
159
|
+
- [ ] fix Generic case
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
class Paged(BaseModel, Generic[T]):
|
|
163
|
+
total: int
|
|
164
|
+
items: List[T]
|
|
165
|
+
```
|
|
159
166
|
|
|
160
167
|
bugs:
|
|
161
168
|
- [ ] fix duplicated link from class and parent class, it also break clicking highlight
|
|
@@ -169,6 +169,12 @@ Examples:
|
|
|
169
169
|
default=8000,
|
|
170
170
|
help="Port for the preview server when --server is used (default: 8000)"
|
|
171
171
|
)
|
|
172
|
+
parser.add_argument(
|
|
173
|
+
"--host",
|
|
174
|
+
type=str,
|
|
175
|
+
default="127.0.0.1",
|
|
176
|
+
help="Host/IP for the preview server when --server is used (default: 127.0.0.1). Use 0.0.0.0 to listen on all interfaces."
|
|
177
|
+
)
|
|
172
178
|
|
|
173
179
|
parser.add_argument(
|
|
174
180
|
"--version", "-v",
|
|
@@ -267,8 +273,8 @@ Examples:
|
|
|
267
273
|
print("Error: uvicorn is required to run the server. Install via 'pip install uvicorn' or 'uv add uvicorn'.")
|
|
268
274
|
sys.exit(1)
|
|
269
275
|
app_server = viz_server.create_app_with_fastapi(app, module_color=module_color)
|
|
270
|
-
print(f"Starting preview server at http://
|
|
271
|
-
uvicorn.run(app_server, host=
|
|
276
|
+
print(f"Starting preview server at http://{args.host}:{args.port} ... (Ctrl+C to stop)")
|
|
277
|
+
uvicorn.run(app_server, host=args.host, port=args.port)
|
|
272
278
|
else:
|
|
273
279
|
# Generate and write dot file locally
|
|
274
280
|
generate_visualization(
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from typing import Callable, Type, TypeVar, Any
|
|
2
|
+
from fastapi_voyager.type import SchemaNode, ModuleNode, Route, ModuleRoute
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
N = TypeVar('N') # Node type: ModuleNode or ModuleRoute
|
|
6
|
+
I = TypeVar('I') # Item type: SchemaNode or Route
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _build_module_tree(
|
|
10
|
+
items: list[I],
|
|
11
|
+
*,
|
|
12
|
+
get_module_path: Callable[[I], str | None],
|
|
13
|
+
NodeClass: Type[N],
|
|
14
|
+
item_list_attr: str,
|
|
15
|
+
) -> list[N]:
|
|
16
|
+
"""
|
|
17
|
+
Generic builder that groups items by dotted module path into a tree of NodeClass.
|
|
18
|
+
|
|
19
|
+
NodeClass must accept kwargs: name, fullname, modules(list), and an item list via
|
|
20
|
+
item_list_attr (e.g., 'schema_nodes' or 'routes').
|
|
21
|
+
"""
|
|
22
|
+
# Map from top-level module name to node
|
|
23
|
+
top_modules: dict[str, N] = {}
|
|
24
|
+
# Items without module path
|
|
25
|
+
root_level_items: list[I] = []
|
|
26
|
+
|
|
27
|
+
def make_node(name: str, fullname: str) -> N:
|
|
28
|
+
kwargs: dict[str, Any] = {
|
|
29
|
+
'name': name,
|
|
30
|
+
'fullname': fullname,
|
|
31
|
+
'modules': [],
|
|
32
|
+
item_list_attr: [],
|
|
33
|
+
}
|
|
34
|
+
return NodeClass(**kwargs) # type: ignore[arg-type]
|
|
35
|
+
|
|
36
|
+
def get_or_create(child_name: str, parent: N) -> N:
|
|
37
|
+
for m in getattr(parent, 'modules'):
|
|
38
|
+
if m.name == child_name:
|
|
39
|
+
return m
|
|
40
|
+
parent_full = getattr(parent, 'fullname')
|
|
41
|
+
fullname = child_name if not parent_full or parent_full == "__root__" else f"{parent_full}.{child_name}"
|
|
42
|
+
new_node = make_node(child_name, fullname)
|
|
43
|
+
getattr(parent, 'modules').append(new_node)
|
|
44
|
+
return new_node
|
|
45
|
+
|
|
46
|
+
# Build the tree
|
|
47
|
+
for it in items:
|
|
48
|
+
module_path = get_module_path(it) or ""
|
|
49
|
+
if not module_path:
|
|
50
|
+
root_level_items.append(it)
|
|
51
|
+
continue
|
|
52
|
+
parts = module_path.split('.')
|
|
53
|
+
top_name = parts[0]
|
|
54
|
+
if top_name not in top_modules:
|
|
55
|
+
top_modules[top_name] = make_node(top_name, top_name)
|
|
56
|
+
current = top_modules[top_name]
|
|
57
|
+
for part in parts[1:]:
|
|
58
|
+
current = get_or_create(part, current)
|
|
59
|
+
getattr(current, item_list_attr).append(it)
|
|
60
|
+
|
|
61
|
+
result: list[N] = list(top_modules.values())
|
|
62
|
+
if root_level_items:
|
|
63
|
+
result.append(make_node("__root__", "__root__"))
|
|
64
|
+
setattr(result[-1], item_list_attr, root_level_items)
|
|
65
|
+
|
|
66
|
+
# Collapse linear chains: no items on node and exactly one child module
|
|
67
|
+
def collapse(node: N) -> None:
|
|
68
|
+
while len(getattr(node, 'modules')) == 1 and len(getattr(node, item_list_attr)) == 0:
|
|
69
|
+
child = getattr(node, 'modules')[0]
|
|
70
|
+
node.name = f"{node.name}.{child.name}"
|
|
71
|
+
node.fullname = child.fullname
|
|
72
|
+
setattr(node, item_list_attr, getattr(child, item_list_attr))
|
|
73
|
+
setattr(node, 'modules', getattr(child, 'modules'))
|
|
74
|
+
for m in getattr(node, 'modules'):
|
|
75
|
+
collapse(m)
|
|
76
|
+
|
|
77
|
+
for top in result:
|
|
78
|
+
collapse(top)
|
|
79
|
+
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
def build_module_schema_tree(schema_nodes: list[SchemaNode]) -> list[ModuleNode]:
|
|
83
|
+
"""Build a module tree for schema nodes, grouped by their module path."""
|
|
84
|
+
return _build_module_tree(
|
|
85
|
+
schema_nodes,
|
|
86
|
+
get_module_path=lambda sn: sn.module,
|
|
87
|
+
NodeClass=ModuleNode,
|
|
88
|
+
item_list_attr='schema_nodes',
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_module_route_tree(routes: list[Route]) -> list[ModuleRoute]:
|
|
93
|
+
"""Build a module tree for routes, grouped by their module path."""
|
|
94
|
+
return _build_module_tree(
|
|
95
|
+
routes,
|
|
96
|
+
get_module_path=lambda r: r.module,
|
|
97
|
+
NodeClass=ModuleRoute,
|
|
98
|
+
item_list_attr='routes',
|
|
99
|
+
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from fastapi_voyager.type import SchemaNode, ModuleNode, Link, Tag, Route, FieldType, PK
|
|
2
|
-
from fastapi_voyager.module import
|
|
1
|
+
from fastapi_voyager.type import SchemaNode, ModuleNode, Link, Tag, Route, FieldType, PK, ModuleRoute
|
|
2
|
+
from fastapi_voyager.module import build_module_schema_tree, build_module_route_tree
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class Renderer:
|
|
@@ -51,7 +51,7 @@ class Renderer:
|
|
|
51
51
|
if link.type == 'tag_route':
|
|
52
52
|
return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid", minlen=3];"""
|
|
53
53
|
elif link.type == 'route_to_schema':
|
|
54
|
-
return f"""{h(link.source)}:e -> {h(link.target)}:
|
|
54
|
+
return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid", dir="back", arrowtail="odot", minlen=3];"""
|
|
55
55
|
elif link.type == 'schema':
|
|
56
56
|
return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid", label = "", dir="back", minlen=3, arrowtail="odot"];"""
|
|
57
57
|
elif link.type == 'parent':
|
|
@@ -61,7 +61,7 @@ class Renderer:
|
|
|
61
61
|
else:
|
|
62
62
|
raise ValueError(f'Unknown link type: {link.type}')
|
|
63
63
|
|
|
64
|
-
def
|
|
64
|
+
def render_module_schema(self, mod: ModuleNode) -> str:
|
|
65
65
|
color = self.module_color.get(mod.fullname)
|
|
66
66
|
inner_nodes = [
|
|
67
67
|
f'''
|
|
@@ -72,7 +72,7 @@ class Renderer:
|
|
|
72
72
|
];''' for node in mod.schema_nodes
|
|
73
73
|
]
|
|
74
74
|
inner_nodes_str = '\n'.join(inner_nodes)
|
|
75
|
-
child_str = '\n'.join(self.
|
|
75
|
+
child_str = '\n'.join(self.render_module_schema(m) for m in mod.modules)
|
|
76
76
|
return f'''
|
|
77
77
|
subgraph cluster_module_{mod.fullname.replace('.', '_')} {{
|
|
78
78
|
tooltip="{mod.fullname}"
|
|
@@ -85,9 +85,36 @@ class Renderer:
|
|
|
85
85
|
{inner_nodes_str}
|
|
86
86
|
{child_str}
|
|
87
87
|
}}'''
|
|
88
|
+
|
|
89
|
+
def render_module_route(self, mod: ModuleRoute) -> str:
|
|
90
|
+
color = self.module_color.get(mod.fullname)
|
|
91
|
+
# Inner route nodes, same style as flat route_str
|
|
92
|
+
inner_nodes = [
|
|
93
|
+
f'''
|
|
94
|
+
"{r.id}" [
|
|
95
|
+
label = " {r.name} "
|
|
96
|
+
margin="0.5,0.1"
|
|
97
|
+
shape = "record"
|
|
98
|
+
];''' for r in mod.routes
|
|
99
|
+
]
|
|
100
|
+
inner_nodes_str = '\n'.join(inner_nodes)
|
|
101
|
+
child_str = '\n'.join(self.render_module_route(m) for m in mod.modules)
|
|
102
|
+
return f'''
|
|
103
|
+
subgraph cluster_route_module_{mod.fullname.replace('.', '_')} {{
|
|
104
|
+
tooltip="{mod.fullname}"
|
|
105
|
+
color = "#666"
|
|
106
|
+
style="rounded"
|
|
107
|
+
label = " {mod.name}"
|
|
108
|
+
labeljust = "l"
|
|
109
|
+
{(f'pencolor = "{color}"' if color else 'pencolor="#ccc"')}
|
|
110
|
+
{(f'penwidth = 3' if color else 'penwidth=""')}
|
|
111
|
+
{inner_nodes_str}
|
|
112
|
+
{child_str}
|
|
113
|
+
}}'''
|
|
88
114
|
|
|
89
115
|
def render_dot(self, tags: list[Tag], routes: list[Route], nodes: list[SchemaNode], links: list[Link]) -> str:
|
|
90
|
-
|
|
116
|
+
module_schemas = build_module_schema_tree(nodes)
|
|
117
|
+
module_routes = build_module_route_tree(routes)
|
|
91
118
|
|
|
92
119
|
tag_str = '\n'.join([
|
|
93
120
|
f'''
|
|
@@ -98,16 +125,9 @@ class Renderer:
|
|
|
98
125
|
];''' for t in tags
|
|
99
126
|
])
|
|
100
127
|
|
|
101
|
-
route_str = '\n'.join([
|
|
102
|
-
f'''
|
|
103
|
-
"{r.id}" [
|
|
104
|
-
label = " {r.name} "
|
|
105
|
-
margin="0.5,0.1"
|
|
106
|
-
shape = "record"
|
|
107
|
-
];''' for r in routes
|
|
108
|
-
])
|
|
109
128
|
|
|
110
|
-
|
|
129
|
+
module_schemas_str = '\n'.join(self.render_module_schema(m) for m in module_schemas)
|
|
130
|
+
module_routes_str = '\n'.join(self.render_module_route(m) for m in module_routes)
|
|
111
131
|
link_str = '\n'.join(self.render_link(link) for link in links)
|
|
112
132
|
|
|
113
133
|
dot_str = f'''
|
|
@@ -144,7 +164,7 @@ class Renderer:
|
|
|
144
164
|
label = " Routes"
|
|
145
165
|
labeljust = "l"
|
|
146
166
|
fontsize = "20"
|
|
147
|
-
{
|
|
167
|
+
{module_routes_str}
|
|
148
168
|
}}
|
|
149
169
|
|
|
150
170
|
subgraph cluster_schema {{
|
|
@@ -154,7 +174,7 @@ class Renderer:
|
|
|
154
174
|
label=" Schema"
|
|
155
175
|
labeljust="l"
|
|
156
176
|
fontsize="20"
|
|
157
|
-
{
|
|
177
|
+
{module_schemas_str}
|
|
158
178
|
}}
|
|
159
179
|
|
|
160
180
|
{link_str}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import Optional
|
|
3
|
-
from fastapi import FastAPI
|
|
3
|
+
from fastapi import FastAPI, APIRouter
|
|
4
4
|
from starlette.middleware.gzip import GZipMiddleware
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
from fastapi.responses import HTMLResponse, PlainTextResponse, JSONResponse
|
|
@@ -33,23 +33,13 @@ class Payload(BaseModel):
|
|
|
33
33
|
show_fields: str = 'object'
|
|
34
34
|
show_meta: bool = False
|
|
35
35
|
|
|
36
|
-
def
|
|
36
|
+
def create_route(
|
|
37
37
|
target_app: FastAPI,
|
|
38
38
|
module_color: dict[str, str] | None = None,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"""Create a FastAPI server that serves DOT computed via Analytics.
|
|
42
|
-
|
|
43
|
-
This avoids module-level globals by keeping state in closures.
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
app = FastAPI(title="fastapi-voyager demo server")
|
|
39
|
+
):
|
|
40
|
+
router = APIRouter(tags=['fastapi-voyager'])
|
|
47
41
|
|
|
48
|
-
|
|
49
|
-
if gzip_minimum_size is not None and gzip_minimum_size >= 0:
|
|
50
|
-
app.add_middleware(GZipMiddleware, minimum_size=gzip_minimum_size)
|
|
51
|
-
|
|
52
|
-
@app.get("/dot", response_model=OptionParam)
|
|
42
|
+
@router.get("/dot", response_model=OptionParam)
|
|
53
43
|
def get_dot() -> str:
|
|
54
44
|
voyager = Voyager(module_color=module_color, load_meta=True)
|
|
55
45
|
voyager.analysis(target_app)
|
|
@@ -71,7 +61,7 @@ def create_app_with_fastapi(
|
|
|
71
61
|
|
|
72
62
|
return OptionParam(tags=tags, schemas=schemas, dot=dot)
|
|
73
63
|
|
|
74
|
-
@
|
|
64
|
+
@router.post("/dot", response_class=PlainTextResponse)
|
|
75
65
|
def get_filtered_dot(payload: Payload) -> str:
|
|
76
66
|
voyager = Voyager(
|
|
77
67
|
include_tags=payload.tags,
|
|
@@ -85,7 +75,7 @@ def create_app_with_fastapi(
|
|
|
85
75
|
voyager.analysis(target_app)
|
|
86
76
|
return voyager.render_dot()
|
|
87
77
|
|
|
88
|
-
@
|
|
78
|
+
@router.post("/dot-core-data", response_model=CoreData)
|
|
89
79
|
def get_filtered_dot_core_data(payload: Payload) -> str:
|
|
90
80
|
voyager = Voyager(
|
|
91
81
|
include_tags=payload.tags,
|
|
@@ -98,13 +88,13 @@ def create_app_with_fastapi(
|
|
|
98
88
|
)
|
|
99
89
|
voyager.analysis(target_app)
|
|
100
90
|
return voyager.dump_core_data()
|
|
101
|
-
|
|
102
|
-
@
|
|
91
|
+
|
|
92
|
+
@router.post('/dot-render-core-data', response_class=PlainTextResponse)
|
|
103
93
|
def render_dot_from_core_data(core_data: CoreData) -> str:
|
|
104
94
|
renderer = Renderer(show_fields=core_data.show_fields, module_color=core_data.module_color, schema=core_data.schema)
|
|
105
95
|
return renderer.render_dot(core_data.tags, core_data.routes, core_data.nodes, core_data.links)
|
|
106
96
|
|
|
107
|
-
@
|
|
97
|
+
@router.get("/", response_class=HTMLResponse)
|
|
108
98
|
def index():
|
|
109
99
|
index_file = WEB_DIR / "index.html"
|
|
110
100
|
if index_file.exists():
|
|
@@ -120,8 +110,22 @@ def create_app_with_fastapi(
|
|
|
120
110
|
</html>
|
|
121
111
|
"""
|
|
122
112
|
|
|
123
|
-
|
|
124
|
-
|
|
113
|
+
return router
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def create_app_with_fastapi(
|
|
117
|
+
target_app: FastAPI,
|
|
118
|
+
module_color: dict[str, str] | None = None,
|
|
119
|
+
gzip_minimum_size: int | None = 500,
|
|
120
|
+
) -> FastAPI:
|
|
121
|
+
router = create_route(target_app, module_color=module_color)
|
|
122
|
+
|
|
123
|
+
app = FastAPI(title="fastapi-voyager demo server")
|
|
124
|
+
if gzip_minimum_size is not None and gzip_minimum_size >= 0:
|
|
125
|
+
app.add_middleware(GZipMiddleware, minimum_size=gzip_minimum_size)
|
|
126
|
+
|
|
127
|
+
app.mount("/fastapi-voyager-static", StaticFiles(directory=str(WEB_DIR)), name="static")
|
|
128
|
+
app.include_router(router)
|
|
125
129
|
|
|
126
130
|
return app
|
|
127
131
|
|
|
@@ -21,9 +21,17 @@ class Tag(NodeBase):
|
|
|
21
21
|
|
|
22
22
|
@dataclass
|
|
23
23
|
class Route(NodeBase):
|
|
24
|
+
module: str
|
|
24
25
|
source_code: str = ''
|
|
25
26
|
vscode_link: str = '' # optional vscode deep link
|
|
26
27
|
|
|
28
|
+
@dataclass
|
|
29
|
+
class ModuleRoute:
|
|
30
|
+
name: str
|
|
31
|
+
fullname: str
|
|
32
|
+
routes: list[Route]
|
|
33
|
+
modules: list['ModuleRoute']
|
|
34
|
+
|
|
27
35
|
@dataclass
|
|
28
36
|
class SchemaNode(NodeBase):
|
|
29
37
|
module: str
|
|
@@ -142,7 +142,7 @@ def get_type_name(anno):
|
|
|
142
142
|
|
|
143
143
|
|
|
144
144
|
def is_inheritance_of_pydantic_base(cls):
|
|
145
|
-
return
|
|
145
|
+
return safe_issubclass(cls, BaseModel) and cls is not BaseModel
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
def get_bases_fields(schemas: list[type[BaseModel]]) -> set[str]:
|
|
@@ -228,6 +228,7 @@ def safe_issubclass(kls, classinfo):
|
|
|
228
228
|
try:
|
|
229
229
|
return issubclass(kls, classinfo)
|
|
230
230
|
except TypeError:
|
|
231
|
+
print(kls.__name__, 'is not a class')
|
|
231
232
|
return False
|
|
232
233
|
|
|
233
234
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.6.1"
|
|
@@ -83,6 +83,7 @@ class Voyager:
|
|
|
83
83
|
# add route and create links
|
|
84
84
|
route_id = f'{route.endpoint.__name__}_{route.path.replace("/", "_")}'
|
|
85
85
|
route_name = route.endpoint.__name__
|
|
86
|
+
route_module = route.endpoint.__module__
|
|
86
87
|
|
|
87
88
|
# filter by route_name (route.id) if provided
|
|
88
89
|
if self.route_name is not None and route_id != self.route_name:
|
|
@@ -91,6 +92,7 @@ class Voyager:
|
|
|
91
92
|
route_obj = Route(
|
|
92
93
|
id=route_id,
|
|
93
94
|
name=route_name,
|
|
95
|
+
module=route_module,
|
|
94
96
|
vscode_link=get_vscode_link(route.endpoint) if self.load_meta else '',
|
|
95
97
|
source_code=inspect.getsource(route.endpoint) if self.load_meta else ''
|
|
96
98
|
)
|
{fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/component/render-graph.js
RENAMED
|
@@ -31,7 +31,7 @@ export default defineComponent({
|
|
|
31
31
|
if (!props.coreData) return;
|
|
32
32
|
loading.value = true;
|
|
33
33
|
try {
|
|
34
|
-
const res = await fetch("
|
|
34
|
+
const res = await fetch("dot-render-core-data", {
|
|
35
35
|
method: "POST",
|
|
36
36
|
headers: { "Content-Type": "application/json" },
|
|
37
37
|
body: JSON.stringify(props.coreData),
|
|
@@ -81,7 +81,7 @@ export default defineComponent({
|
|
|
81
81
|
schema_field: state.fieldName || null,
|
|
82
82
|
show_fields: state.showFields,
|
|
83
83
|
};
|
|
84
|
-
const res = await fetch("
|
|
84
|
+
const res = await fetch("dot", {
|
|
85
85
|
method: "POST",
|
|
86
86
|
headers: { "Content-Type": "application/json" },
|
|
87
87
|
body: JSON.stringify(payload),
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
<head>
|
|
3
3
|
<title>FastAPI Voyager</title>
|
|
4
4
|
<meta name="theme-color" content="#ffffff" />
|
|
5
|
-
<link rel="stylesheet" href="
|
|
6
|
-
<link rel="stylesheet" href="
|
|
5
|
+
<link rel="stylesheet" href="fastapi-voyager-static/graphviz.svg.css" />
|
|
6
|
+
<link rel="stylesheet" href="fastapi-voyager-static/quasar.min.css" />
|
|
7
7
|
<!-- App Icons / Favicons -->
|
|
8
|
-
<link rel="apple-touch-icon" sizes="180x180" href="
|
|
9
|
-
<link rel="icon" type="image/png" sizes="32x32" href="
|
|
10
|
-
<link rel="icon" type="image/png" sizes="16x16" href="
|
|
11
|
-
<link rel="icon" href="
|
|
12
|
-
<link rel="manifest" href="
|
|
8
|
+
<link rel="apple-touch-icon" sizes="180x180" href="fastapi-voyager-static/icon/apple-touch-icon.png" />
|
|
9
|
+
<link rel="icon" type="image/png" sizes="32x32" href="fastapi-voyager-static/icon/favicon-32x32.png" />
|
|
10
|
+
<link rel="icon" type="image/png" sizes="16x16" href="fastapi-voyager-static/icon/favicon-16x16.png" />
|
|
11
|
+
<link rel="icon" href="fastapi-voyager-static/icon/favicon.ico" sizes="any" />
|
|
12
|
+
<link rel="manifest" href="fastapi-voyager-static/icon/site.webmanifest" />
|
|
13
13
|
<link
|
|
14
14
|
href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
|
|
15
15
|
rel="stylesheet"
|
|
@@ -265,8 +265,8 @@
|
|
|
265
265
|
|
|
266
266
|
<!-- Add the following at the end of your body tag -->
|
|
267
267
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.5.22/vue.global.prod.min.js" integrity="sha512-Y9sKU0AwzWRxKSLd2i35LuDpUdHY/E9tJrKG0mxw0qYQ75VVgGYazIUQPwKhFK9vGO3jIgAtxLiSq8GQ7PDfUg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
268
|
-
<script src="
|
|
269
|
-
<script src="
|
|
268
|
+
<script src="fastapi-voyager-static/quasar.min.js"></script>
|
|
269
|
+
<script src="fastapi-voyager-static/graphviz.svg.js"></script>
|
|
270
270
|
<!-- highlight.js minimal ES module load (python only) -->
|
|
271
271
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" />
|
|
272
272
|
<script type="module">
|
|
@@ -275,6 +275,6 @@
|
|
|
275
275
|
hljs.registerLanguage('python', python);
|
|
276
276
|
window.hljs = hljs;
|
|
277
277
|
</script>
|
|
278
|
-
<script type="module" src="
|
|
278
|
+
<script type="module" src="fastapi-voyager-static/vue-main.js"></script>
|
|
279
279
|
</body>
|
|
280
280
|
</html>
|
|
@@ -37,8 +37,8 @@ const app = createApp({
|
|
|
37
37
|
const dumpJson = ref("");
|
|
38
38
|
const showImportDialog = ref(false);
|
|
39
39
|
const importJsonText = ref("");
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const showRenderGraph = ref(false);
|
|
41
|
+
const renderCoreData = ref(null);
|
|
42
42
|
const schemaName = ref(""); // used by detail dialog
|
|
43
43
|
const schemaFieldFilterSchema = ref(null); // external schemaName for schema-field-filter
|
|
44
44
|
const schemaCodeName = ref("");
|
|
@@ -94,7 +94,7 @@ const app = createApp({
|
|
|
94
94
|
async function loadInitial() {
|
|
95
95
|
state.initializing = true;
|
|
96
96
|
try {
|
|
97
|
-
const res = await fetch("
|
|
97
|
+
const res = await fetch("dot");
|
|
98
98
|
const data = await res.json();
|
|
99
99
|
state.rawTags = Array.isArray(data.tags) ? data.tags : [];
|
|
100
100
|
state.rawSchemasFull = Array.isArray(data.schemas) ? data.schemas : [];
|
|
@@ -134,7 +134,7 @@ const app = createApp({
|
|
|
134
134
|
show_fields: state.showFields,
|
|
135
135
|
};
|
|
136
136
|
|
|
137
|
-
const res = await fetch("
|
|
137
|
+
const res = await fetch("dot", {
|
|
138
138
|
method: "POST",
|
|
139
139
|
headers: { "Content-Type": "application/json" },
|
|
140
140
|
body: JSON.stringify(payload),
|
|
@@ -180,7 +180,7 @@ const app = createApp({
|
|
|
180
180
|
route_name: state.routeId || null,
|
|
181
181
|
show_fields: state.showFields,
|
|
182
182
|
};
|
|
183
|
-
const res = await fetch("
|
|
183
|
+
const res = await fetch("dot-core-data", {
|
|
184
184
|
method: "POST",
|
|
185
185
|
headers: { "Content-Type": "application/json" },
|
|
186
186
|
body: JSON.stringify(payload),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field
|
|
2
2
|
from fastapi import FastAPI
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import Optional, Generic, TypeVar
|
|
4
4
|
from pydantic_resolve import ensure_subset, Resolver
|
|
5
5
|
from tests.service.schema import Story, Task
|
|
6
6
|
import tests.service.schema as serv
|
|
@@ -56,10 +56,19 @@ async def get_page_info():
|
|
|
56
56
|
page_overall = PageOverallWrap(content="Page Overall Content", sprints=[]) # focus on schema only
|
|
57
57
|
return await Resolver().resolve(page_overall)
|
|
58
58
|
|
|
59
|
-
|
|
60
59
|
class PageStories(BaseModel):
|
|
61
60
|
stories: list[PageStory]
|
|
62
61
|
|
|
63
62
|
@app.get("/page_info/", tags=['for-page'], response_model=PageStories)
|
|
64
63
|
def get_page_stories():
|
|
64
|
+
return {} # no implementation
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
T = TypeVar('T')
|
|
68
|
+
class DataModel(BaseModel, Generic[T]):
|
|
69
|
+
data: T
|
|
70
|
+
id: int
|
|
71
|
+
|
|
72
|
+
@app.get("/page_test_1/", tags=['for-page'], response_model=DataModel[PageStory])
|
|
73
|
+
def get_page_test_1():
|
|
65
74
|
return {} # no implementation
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from fastapi_voyager.module import
|
|
1
|
+
from fastapi_voyager.module import build_module_schema_tree
|
|
2
2
|
from fastapi_voyager.type import SchemaNode
|
|
3
3
|
|
|
4
4
|
|
|
@@ -24,7 +24,7 @@ def test_build_module_tree_basic():
|
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
# Act
|
|
27
|
-
top_modules =
|
|
27
|
+
top_modules = build_module_schema_tree(schema_nodes)
|
|
28
28
|
from pprint import pprint
|
|
29
29
|
pprint(top_modules)
|
|
30
30
|
|
|
@@ -58,7 +58,7 @@ def test_build_module_tree_basic():
|
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def test_build_module_tree_empty_input():
|
|
61
|
-
top_modules =
|
|
61
|
+
top_modules = build_module_schema_tree([])
|
|
62
62
|
assert top_modules == []
|
|
63
63
|
|
|
64
64
|
|
|
@@ -70,7 +70,7 @@ def test_build_module_tree_root_level_nodes():
|
|
|
70
70
|
_sn("PkgA", "pkg", "PkgA"),
|
|
71
71
|
]
|
|
72
72
|
|
|
73
|
-
top_modules =
|
|
73
|
+
top_modules = build_module_schema_tree(schema_nodes)
|
|
74
74
|
names = sorted(m.name for m in top_modules)
|
|
75
75
|
assert names == ["__root__", "pkg"]
|
|
76
76
|
root = _find_top(top_modules, "__root__")
|
|
@@ -86,7 +86,7 @@ def test_collapse_single_child_empty_modules():
|
|
|
86
86
|
_sn("Deep", "a.b.c.d", "Deep"),
|
|
87
87
|
_sn("Peer", "a.b.x", "Peer"),
|
|
88
88
|
]
|
|
89
|
-
top_modules =
|
|
89
|
+
top_modules = build_module_schema_tree(schema_nodes)
|
|
90
90
|
print(top_modules)
|
|
91
91
|
# 'a' should have one child path 'b', but due to branching at x, only a.b collapses into a.b
|
|
92
92
|
# and below it, 'c.d' should collapse to 'c.d'. Final structure:
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
from fastapi_voyager.type import SchemaNode, ModuleNode
|
|
2
|
-
|
|
3
|
-
def build_module_tree(schema_nodes: list[SchemaNode]) -> list[ModuleNode]:
|
|
4
|
-
"""
|
|
5
|
-
1. the name of module_node comes from schema_node's module field
|
|
6
|
-
2. split the module_name with '.' to create a tree structure
|
|
7
|
-
3. group schema_nodes under the correct module_node
|
|
8
|
-
4. return the top-level module_node list
|
|
9
|
-
"""
|
|
10
|
-
# Map from top-level module name to ModuleNode
|
|
11
|
-
top_modules: dict[str, ModuleNode] = {}
|
|
12
|
-
# For nodes without module path, collect separately
|
|
13
|
-
root_level_nodes = []
|
|
14
|
-
|
|
15
|
-
def get_or_create(child_name: str, parent: ModuleNode) -> ModuleNode:
|
|
16
|
-
for m in parent.modules:
|
|
17
|
-
if m.name == child_name:
|
|
18
|
-
return m
|
|
19
|
-
# derive fullname from parent
|
|
20
|
-
parent_full = parent.fullname
|
|
21
|
-
fullname = child_name if not parent_full or parent_full == "__root__" else f"{parent_full}.{child_name}"
|
|
22
|
-
new_node = ModuleNode(name=child_name, fullname=fullname, schema_nodes=[], modules=[])
|
|
23
|
-
parent.modules.append(new_node)
|
|
24
|
-
return new_node
|
|
25
|
-
|
|
26
|
-
for sn in schema_nodes:
|
|
27
|
-
module_path = sn.module or ""
|
|
28
|
-
if not module_path:
|
|
29
|
-
root_level_nodes.append(sn)
|
|
30
|
-
continue
|
|
31
|
-
parts = module_path.split('.')
|
|
32
|
-
top_name = parts[0]
|
|
33
|
-
if top_name not in top_modules:
|
|
34
|
-
top_modules[top_name] = ModuleNode(name=top_name, fullname=top_name, schema_nodes=[], modules=[])
|
|
35
|
-
current = top_modules[top_name]
|
|
36
|
-
for part in parts[1:]:
|
|
37
|
-
current = get_or_create(part, current)
|
|
38
|
-
current.schema_nodes.append(sn)
|
|
39
|
-
|
|
40
|
-
# If there are root-level nodes, add a pseudo-module named "__root__"
|
|
41
|
-
result = list(top_modules.values())
|
|
42
|
-
if root_level_nodes:
|
|
43
|
-
result.append(ModuleNode(name="__root__", fullname="__root__", schema_nodes=root_level_nodes, modules=[]))
|
|
44
|
-
|
|
45
|
-
# Collapse pass: if a module has exactly one child module and no schema_nodes,
|
|
46
|
-
# merge it upward (A + B -> A.B). Repeat until fixed point.
|
|
47
|
-
def collapse(node: ModuleNode):
|
|
48
|
-
# Collapse chains at current node
|
|
49
|
-
while len(node.modules) == 1 and len(node.schema_nodes) == 0:
|
|
50
|
-
child = node.modules[0]
|
|
51
|
-
# Merge child's identity into current node
|
|
52
|
-
node.name = f"{node.name}.{child.name}"
|
|
53
|
-
# Prefer child's fullname which already reflects full path
|
|
54
|
-
node.fullname = child.fullname
|
|
55
|
-
node.schema_nodes = child.schema_nodes
|
|
56
|
-
node.modules = child.modules
|
|
57
|
-
# Recurse into children
|
|
58
|
-
for m in node.modules:
|
|
59
|
-
collapse(m)
|
|
60
|
-
|
|
61
|
-
for top in result:
|
|
62
|
-
collapse(top)
|
|
63
|
-
|
|
64
|
-
return result
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/icon/apple-touch-icon.png
RENAMED
|
File without changes
|
{fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/icon/favicon-16x16.png
RENAMED
|
File without changes
|
{fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/icon/favicon-32x32.png
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.5.2 → fastapi_voyager-0.6.1}/src/fastapi_voyager/web/icon/site.webmanifest
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|