fastapi-voyager 0.10.5__py3-none-any.whl → 0.11.2__py3-none-any.whl
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/filter.py +91 -1
- fastapi_voyager/render.py +55 -39
- fastapi_voyager/server.py +18 -3
- fastapi_voyager/type.py +3 -1
- fastapi_voyager/version.py +1 -1
- fastapi_voyager/voyager.py +32 -6
- fastapi_voyager/web/index.html +22 -3
- fastapi_voyager/web/vue-main.js +10 -1
- {fastapi_voyager-0.10.5.dist-info → fastapi_voyager-0.11.2.dist-info}/METADATA +40 -27
- {fastapi_voyager-0.10.5.dist-info → fastapi_voyager-0.11.2.dist-info}/RECORD +13 -13
- {fastapi_voyager-0.10.5.dist-info → fastapi_voyager-0.11.2.dist-info}/WHEEL +0 -0
- {fastapi_voyager-0.10.5.dist-info → fastapi_voyager-0.11.2.dist-info}/entry_points.txt +0 -0
- {fastapi_voyager-0.10.5.dist-info → fastapi_voyager-0.11.2.dist-info}/licenses/LICENSE +0 -0
fastapi_voyager/filter.py
CHANGED
|
@@ -192,4 +192,94 @@ def filter_subgraph_by_module_prefix(
|
|
|
192
192
|
|
|
193
193
|
filtered_links = tag_route_links + merged_links + module_prefix_links
|
|
194
194
|
|
|
195
|
-
return tags, routes, filtered_nodes, filtered_links
|
|
195
|
+
return tags, routes, filtered_nodes, filtered_links
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def filter_subgraph_from_tag_to_schema_by_module_prefix(
|
|
199
|
+
*,
|
|
200
|
+
tags: list[Tag],
|
|
201
|
+
routes: list[Route],
|
|
202
|
+
links: list[Link],
|
|
203
|
+
nodes: list[SchemaNode],
|
|
204
|
+
module_prefix: str
|
|
205
|
+
) -> Tuple[list[Tag], list[Route], list[SchemaNode], list[Link]]:
|
|
206
|
+
"""Collapse schema graph so routes link directly to nodes whose module matches ``module_prefix``.
|
|
207
|
+
|
|
208
|
+
The routine keeps tag→route links untouched, prunes schema nodes whose module does not start
|
|
209
|
+
with ``module_prefix``, and merges the remaining schema relationships so each route connects
|
|
210
|
+
directly to the surviving schema nodes. Traversal stops once a qualifying node is reached and
|
|
211
|
+
guards against cycles in the schema graph.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
if not module_prefix:
|
|
215
|
+
# empty prefix keeps existing graph structure, so simply reuse incoming data
|
|
216
|
+
return tags, routes, nodes, [lk for lk in links if lk.type in ("tag_route", "route_to_schema")]
|
|
217
|
+
|
|
218
|
+
route_links = [lk for lk in links if lk.type == "route_to_schema"]
|
|
219
|
+
schema_links = [lk for lk in links if lk.type in {"schema", "parent", "subset"}]
|
|
220
|
+
tag_route_links = [lk for lk in links if lk.type == "tag_route"]
|
|
221
|
+
|
|
222
|
+
node_lookup: dict[str, SchemaNode] = {node.id: node for node in (nodes + routes)}
|
|
223
|
+
|
|
224
|
+
filtered_nodes = [node for node in nodes if node_lookup[node.id].module.startswith(module_prefix)]
|
|
225
|
+
filtered_node_ids = {node.id for node in filtered_nodes}
|
|
226
|
+
|
|
227
|
+
adjacency: dict[str, list[str]] = {}
|
|
228
|
+
for link in (schema_links + route_links):
|
|
229
|
+
if link.source_origin not in node_lookup or link.target_origin not in node_lookup:
|
|
230
|
+
continue
|
|
231
|
+
adjacency.setdefault(link.source_origin, [])
|
|
232
|
+
if link.target_origin not in adjacency[link.source_origin]:
|
|
233
|
+
adjacency[link.source_origin].append(link.target_origin)
|
|
234
|
+
|
|
235
|
+
merged_links: list[Link] = []
|
|
236
|
+
seen_pairs: set[tuple[str, str]] = set()
|
|
237
|
+
|
|
238
|
+
for link in tag_route_links:
|
|
239
|
+
# print(link)
|
|
240
|
+
tag_id = link.source_origin
|
|
241
|
+
start_node_id = link.target_origin
|
|
242
|
+
if tag_id is None or start_node_id is None:
|
|
243
|
+
continue
|
|
244
|
+
if start_node_id not in node_lookup:
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
visited: set[str] = set()
|
|
248
|
+
queue: deque[str] = deque([start_node_id])
|
|
249
|
+
|
|
250
|
+
while queue:
|
|
251
|
+
current = queue.popleft()
|
|
252
|
+
if current in visited:
|
|
253
|
+
continue
|
|
254
|
+
visited.add(current)
|
|
255
|
+
|
|
256
|
+
if current in filtered_node_ids:
|
|
257
|
+
key = (tag_id, current)
|
|
258
|
+
if key not in seen_pairs:
|
|
259
|
+
seen_pairs.add(key)
|
|
260
|
+
merged_links.append(
|
|
261
|
+
Link(
|
|
262
|
+
source=link.source,
|
|
263
|
+
source_origin=tag_id,
|
|
264
|
+
target=f"{current}::{PK}",
|
|
265
|
+
target_origin=current,
|
|
266
|
+
type="tag_to_schema",
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
# stop traversing past a qualifying node
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
for next_node in adjacency.get(current, () ):
|
|
273
|
+
if next_node not in visited:
|
|
274
|
+
queue.append(next_node)
|
|
275
|
+
|
|
276
|
+
module_prefix_links = [
|
|
277
|
+
lk
|
|
278
|
+
for lk in links
|
|
279
|
+
if (lk.source_origin or "").startswith(module_prefix)
|
|
280
|
+
and (lk.target_origin or "").startswith(module_prefix)
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
filtered_links = merged_links + module_prefix_links
|
|
284
|
+
|
|
285
|
+
return tags, [], filtered_nodes, filtered_links # route is skipped
|
fastapi_voyager/render.py
CHANGED
|
@@ -10,10 +10,12 @@ class Renderer:
|
|
|
10
10
|
show_fields: FieldType = 'single',
|
|
11
11
|
module_color: dict[str, str] | None = None,
|
|
12
12
|
schema: str | None = None,
|
|
13
|
+
show_module: bool = True
|
|
13
14
|
) -> None:
|
|
14
15
|
self.show_fields = show_fields if show_fields in ('single', 'object', 'all') else 'single'
|
|
15
16
|
self.module_color = module_color or {}
|
|
16
17
|
self.schema = schema
|
|
18
|
+
self.show_module = show_module
|
|
17
19
|
|
|
18
20
|
def render_schema_label(self, node: SchemaNode) -> str:
|
|
19
21
|
has_base_fields = any(f.from_base for f in node.fields)
|
|
@@ -59,12 +61,19 @@ class Renderer:
|
|
|
59
61
|
return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid, dashed", dir="back", minlen=3, taillabel = "< inherit >", color = "purple", tailport="n"];"""
|
|
60
62
|
elif link.type == 'subset':
|
|
61
63
|
return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid, dashed", dir="back", minlen=3, taillabel = "< subset >", color = "orange", tailport="n"];"""
|
|
64
|
+
elif link.type == 'tag_to_schema':
|
|
65
|
+
return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid", minlen=3];"""
|
|
62
66
|
else:
|
|
63
67
|
raise ValueError(f'Unknown link type: {link.type}')
|
|
64
68
|
|
|
65
|
-
def render_module_schema_content(self,
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
def render_module_schema_content(self, nodes: list[SchemaNode]) -> str:
|
|
70
|
+
def render_node(node: SchemaNode) -> str:
|
|
71
|
+
return f'''
|
|
72
|
+
"{node.id}" [
|
|
73
|
+
label = {self.render_schema_label(node)}
|
|
74
|
+
shape = "plain"
|
|
75
|
+
margin="0.5,0.1"
|
|
76
|
+
];'''
|
|
68
77
|
def render_module_schema(mod: ModuleNode) -> str:
|
|
69
78
|
color: Optional[str] = None
|
|
70
79
|
|
|
@@ -74,14 +83,7 @@ class Renderer:
|
|
|
74
83
|
color = self.module_color[k]
|
|
75
84
|
break
|
|
76
85
|
|
|
77
|
-
inner_nodes = [
|
|
78
|
-
f'''
|
|
79
|
-
"{node.id}" [
|
|
80
|
-
label = {self.render_schema_label(node)}
|
|
81
|
-
shape = "plain"
|
|
82
|
-
margin="0.5,0.1"
|
|
83
|
-
];''' for node in mod.schema_nodes
|
|
84
|
-
]
|
|
86
|
+
inner_nodes = [ render_node(node) for node in mod.schema_nodes ]
|
|
85
87
|
inner_nodes_str = '\n'.join(inner_nodes)
|
|
86
88
|
child_str = '\n'.join(render_module_schema(m) for m in mod.modules)
|
|
87
89
|
return f'''
|
|
@@ -96,34 +98,49 @@ class Renderer:
|
|
|
96
98
|
{inner_nodes_str}
|
|
97
99
|
{child_str}
|
|
98
100
|
}}'''
|
|
99
|
-
|
|
101
|
+
if self.show_module:
|
|
102
|
+
module_schemas = build_module_schema_tree(nodes)
|
|
103
|
+
module_color_flag = set(self.module_color.keys())
|
|
104
|
+
return '\n'.join(render_module_schema(m) for m in module_schemas)
|
|
105
|
+
else:
|
|
106
|
+
return '\n'.join(render_node(n) for n in nodes)
|
|
100
107
|
|
|
101
|
-
def
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
f'''
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
108
|
+
def render_module_route_content(self, routes: list[Route]) -> str:
|
|
109
|
+
def render_route(route: Route) -> str:
|
|
110
|
+
response_schema = route.response_schema[:25] + '..' if len(route.response_schema) > 25 else route.response_schema
|
|
111
|
+
return f'''
|
|
112
|
+
"{route.id}" [
|
|
113
|
+
label = " {route.name} | {response_schema} "
|
|
114
|
+
margin="0.5,0.1"
|
|
115
|
+
shape = "record"
|
|
116
|
+
];'''
|
|
117
|
+
|
|
118
|
+
def render_module_route(mod: ModuleRoute) -> str:
|
|
119
|
+
# Inner route nodes, same style as flat route_str
|
|
120
|
+
inner_nodes = [
|
|
121
|
+
render_route(r) for r in mod.routes
|
|
122
|
+
]
|
|
123
|
+
inner_nodes_str = '\n'.join(inner_nodes)
|
|
124
|
+
child_str = '\n'.join(render_module_route(m) for m in mod.modules)
|
|
125
|
+
return f'''
|
|
126
|
+
subgraph cluster_route_module_{mod.fullname.replace('.', '_')} {{
|
|
127
|
+
tooltip="{mod.fullname}"
|
|
128
|
+
color = "#666"
|
|
129
|
+
style="rounded"
|
|
130
|
+
label = " {mod.name}"
|
|
131
|
+
labeljust = "l"
|
|
132
|
+
{inner_nodes_str}
|
|
133
|
+
{child_str}
|
|
134
|
+
}}'''
|
|
135
|
+
if self.show_module:
|
|
136
|
+
module_routes = build_module_route_tree(routes)
|
|
137
|
+
module_routes_str = '\n'.join(render_module_route(m) for m in module_routes)
|
|
138
|
+
return module_routes_str
|
|
139
|
+
else:
|
|
140
|
+
return '\n'.join(render_route(r) for r in routes)
|
|
141
|
+
|
|
123
142
|
|
|
124
143
|
def render_dot(self, tags: list[Tag], routes: list[Route], nodes: list[SchemaNode], links: list[Link], spline_line=False) -> str:
|
|
125
|
-
module_schemas = build_module_schema_tree(nodes)
|
|
126
|
-
module_routes = build_module_route_tree(routes)
|
|
127
144
|
|
|
128
145
|
tag_str = '\n'.join([
|
|
129
146
|
f'''
|
|
@@ -134,9 +151,8 @@ class Renderer:
|
|
|
134
151
|
];''' for t in tags
|
|
135
152
|
])
|
|
136
153
|
|
|
137
|
-
|
|
138
|
-
module_schemas_str = self.render_module_schema_content(
|
|
139
|
-
module_routes_str = '\n'.join(self.render_module_route(m) for m in module_routes)
|
|
154
|
+
module_routes_str = self.render_module_route_content(routes)
|
|
155
|
+
module_schemas_str = self.render_module_schema_content(nodes)
|
|
140
156
|
link_str = '\n'.join(self.render_link(link) for link in links)
|
|
141
157
|
|
|
142
158
|
dot_str = f'''
|
fastapi_voyager/server.py
CHANGED
|
@@ -22,6 +22,7 @@ class OptionParam(BaseModel):
|
|
|
22
22
|
dot: str
|
|
23
23
|
enable_brief_mode: bool
|
|
24
24
|
version: str
|
|
25
|
+
swagger_url: Optional[str] = None
|
|
25
26
|
|
|
26
27
|
class Payload(BaseModel):
|
|
27
28
|
tags: Optional[list[str]] = None
|
|
@@ -32,10 +33,13 @@ class Payload(BaseModel):
|
|
|
32
33
|
show_meta: bool = False
|
|
33
34
|
brief: bool = False
|
|
34
35
|
hide_primitive_route: bool = False
|
|
36
|
+
show_module: bool = True
|
|
37
|
+
|
|
35
38
|
|
|
36
39
|
def create_route(
|
|
37
40
|
target_app: FastAPI,
|
|
38
41
|
module_color: dict[str, str] | None = None,
|
|
42
|
+
swagger_url: Optional[str] = None,
|
|
39
43
|
module_prefix: Optional[str] = None,
|
|
40
44
|
):
|
|
41
45
|
"""
|
|
@@ -56,7 +60,13 @@ def create_route(
|
|
|
56
60
|
schemas = voyager.nodes[:]
|
|
57
61
|
schemas.sort(key=lambda s: s.name)
|
|
58
62
|
|
|
59
|
-
return OptionParam(
|
|
63
|
+
return OptionParam(
|
|
64
|
+
tags=tags,
|
|
65
|
+
schemas=schemas,
|
|
66
|
+
dot=dot,
|
|
67
|
+
enable_brief_mode=bool(module_prefix),
|
|
68
|
+
version=__version__,
|
|
69
|
+
swagger_url=swagger_url)
|
|
60
70
|
|
|
61
71
|
@router.post("/dot", response_class=PlainTextResponse)
|
|
62
72
|
def get_filtered_dot(payload: Payload) -> str:
|
|
@@ -68,10 +78,14 @@ def create_route(
|
|
|
68
78
|
module_color=module_color,
|
|
69
79
|
route_name=payload.route_name,
|
|
70
80
|
hide_primitive_route=payload.hide_primitive_route,
|
|
81
|
+
show_module=payload.show_module,
|
|
71
82
|
)
|
|
72
83
|
voyager.analysis(target_app)
|
|
73
84
|
if payload.brief:
|
|
74
|
-
|
|
85
|
+
if payload.tags:
|
|
86
|
+
return voyager.render_tag_level_brief_dot(module_prefix=module_prefix)
|
|
87
|
+
else:
|
|
88
|
+
return voyager.render_overall_brief_dot(module_prefix=module_prefix)
|
|
75
89
|
else:
|
|
76
90
|
return voyager.render_dot()
|
|
77
91
|
|
|
@@ -196,8 +210,9 @@ def create_voyager(
|
|
|
196
210
|
module_color: dict[str, str] | None = None,
|
|
197
211
|
gzip_minimum_size: int | None = 500,
|
|
198
212
|
module_prefix: Optional[str] = None,
|
|
213
|
+
swagger_url: Optional[str] = None,
|
|
199
214
|
) -> FastAPI:
|
|
200
|
-
router = create_route(target_app, module_color=module_color, module_prefix=module_prefix)
|
|
215
|
+
router = create_route(target_app, module_color=module_color, module_prefix=module_prefix, swagger_url=swagger_url)
|
|
201
216
|
|
|
202
217
|
app = FastAPI(title="fastapi-voyager demo server")
|
|
203
218
|
if gzip_minimum_size is not None and gzip_minimum_size >= 0:
|
fastapi_voyager/type.py
CHANGED
|
@@ -22,6 +22,7 @@ class Tag(NodeBase):
|
|
|
22
22
|
@dataclass
|
|
23
23
|
class Route(NodeBase):
|
|
24
24
|
module: str
|
|
25
|
+
unique_id: str = ''
|
|
25
26
|
response_schema: str = ''
|
|
26
27
|
is_primitive: bool = True
|
|
27
28
|
|
|
@@ -51,7 +52,8 @@ class ModuleNode:
|
|
|
51
52
|
# - subset: schema -> schema (subset)
|
|
52
53
|
# - parent: schema -> schema (inheritance)
|
|
53
54
|
# - schema: schema -> schema (field reference)
|
|
54
|
-
|
|
55
|
+
# - tag_to_schema: tag -> schema (only happens in module prefix filtering, aka brief mode)
|
|
56
|
+
LinkType = Literal['schema', 'parent', 'tag_route', 'subset', 'route_to_schema', 'tag_to_schema']
|
|
55
57
|
|
|
56
58
|
@dataclass
|
|
57
59
|
class Link:
|
fastapi_voyager/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.11.2"
|
fastapi_voyager/voyager.py
CHANGED
|
@@ -6,15 +6,13 @@ from fastapi_voyager.type_helper import (
|
|
|
6
6
|
get_bases_fields,
|
|
7
7
|
is_inheritance_of_pydantic_base,
|
|
8
8
|
get_pydantic_fields,
|
|
9
|
-
get_vscode_link,
|
|
10
|
-
get_source,
|
|
11
9
|
get_type_name,
|
|
12
10
|
update_forward_refs,
|
|
13
11
|
is_non_pydantic_type
|
|
14
12
|
)
|
|
15
13
|
from pydantic import BaseModel
|
|
16
14
|
from fastapi_voyager.type import Route, SchemaNode, Link, Tag, LinkType, FieldType, PK, CoreData
|
|
17
|
-
from fastapi_voyager.filter import filter_graph, filter_subgraph_by_module_prefix
|
|
15
|
+
from fastapi_voyager.filter import filter_graph, filter_subgraph_from_tag_to_schema_by_module_prefix, filter_subgraph_by_module_prefix
|
|
18
16
|
from fastapi_voyager.render import Renderer
|
|
19
17
|
import pydantic_resolve.constant as const
|
|
20
18
|
|
|
@@ -29,6 +27,7 @@ class Voyager:
|
|
|
29
27
|
module_color: dict[str, str] | None = None,
|
|
30
28
|
route_name: str | None = None,
|
|
31
29
|
hide_primitive_route: bool = False,
|
|
30
|
+
show_module: bool = True
|
|
32
31
|
):
|
|
33
32
|
|
|
34
33
|
self.routes: list[Route] = []
|
|
@@ -50,6 +49,7 @@ class Voyager:
|
|
|
50
49
|
self.module_color = module_color or {}
|
|
51
50
|
self.route_name = route_name
|
|
52
51
|
self.hide_primitive_route = hide_primitive_route
|
|
52
|
+
self.show_module = show_module
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def _get_available_route(self, app: FastAPI):
|
|
@@ -123,6 +123,7 @@ class Voyager:
|
|
|
123
123
|
id=route_id,
|
|
124
124
|
name=route_name,
|
|
125
125
|
module=route_module,
|
|
126
|
+
unique_id=route.unique_id,
|
|
126
127
|
response_schema=get_type_name(route.response_model),
|
|
127
128
|
is_primitive=is_primitive_response
|
|
128
129
|
)
|
|
@@ -302,12 +303,13 @@ class Voyager:
|
|
|
302
303
|
node_set=self.node_set,
|
|
303
304
|
)
|
|
304
305
|
|
|
305
|
-
renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema)
|
|
306
|
+
renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema, show_module=self.show_module)
|
|
306
307
|
|
|
307
308
|
_tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
|
|
308
309
|
return renderer.render_dot(_tags, _routes, _nodes, _links)
|
|
309
310
|
|
|
310
|
-
|
|
311
|
+
|
|
312
|
+
def render_tag_level_brief_dot(self, module_prefix: str | None = None):
|
|
311
313
|
_tags, _routes, _nodes, _links = filter_graph(
|
|
312
314
|
schema=self.schema,
|
|
313
315
|
schema_field=self.schema_field,
|
|
@@ -326,7 +328,31 @@ class Voyager:
|
|
|
326
328
|
links=_links,
|
|
327
329
|
)
|
|
328
330
|
|
|
329
|
-
renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema)
|
|
331
|
+
renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema, show_module=self.show_module)
|
|
332
|
+
|
|
333
|
+
_tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
|
|
334
|
+
return renderer.render_dot(_tags, _routes, _nodes, _links, True)
|
|
335
|
+
|
|
336
|
+
def render_overall_brief_dot(self, module_prefix: str | None = None):
|
|
337
|
+
_tags, _routes, _nodes, _links = filter_graph(
|
|
338
|
+
schema=self.schema,
|
|
339
|
+
schema_field=self.schema_field,
|
|
340
|
+
tags=self.tags,
|
|
341
|
+
routes=self.routes,
|
|
342
|
+
nodes=self.nodes,
|
|
343
|
+
links=self.links,
|
|
344
|
+
node_set=self.node_set,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
_tags, _routes, _nodes, _links = filter_subgraph_from_tag_to_schema_by_module_prefix(
|
|
348
|
+
module_prefix=module_prefix,
|
|
349
|
+
tags=_tags,
|
|
350
|
+
routes=_routes,
|
|
351
|
+
nodes=_nodes,
|
|
352
|
+
links=_links,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema, show_module=self.show_module)
|
|
330
356
|
|
|
331
357
|
_tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
|
|
332
358
|
return renderer.render_dot(_tags, _routes, _nodes, _links, True)
|
fastapi_voyager/web/index.html
CHANGED
|
@@ -148,6 +148,7 @@
|
|
|
148
148
|
side="right"
|
|
149
149
|
style="border-left: 1px solid #888;"
|
|
150
150
|
bordered
|
|
151
|
+
overlay
|
|
151
152
|
>
|
|
152
153
|
<!-- 可拖拽的调整栏 -->
|
|
153
154
|
<div
|
|
@@ -218,6 +219,9 @@
|
|
|
218
219
|
:name="state.tag == tag.name ? 'folder' : 'folder_open'"
|
|
219
220
|
></q-icon>
|
|
220
221
|
<span>{{ tag.name }} <q-chip style="position:relative; top: -1px;" class="q-ml-sm" dense>{{ tag.routes.length }}</q-chip></span>
|
|
222
|
+
<a target="_blank" class="q-ml-sm" v-if="state.swaggerUrl" :href="state.swaggerUrl + '#/' + tag.name">
|
|
223
|
+
<q-icon size="small" name="link"></q-icon>
|
|
224
|
+
</a>
|
|
221
225
|
</div>
|
|
222
226
|
</template>
|
|
223
227
|
<q-list separator style="overflow: auto; max-height: 60vh;">
|
|
@@ -237,6 +241,9 @@
|
|
|
237
241
|
name="data_object"
|
|
238
242
|
></q-icon>
|
|
239
243
|
{{ route.name }}
|
|
244
|
+
<a target="_blank" class="q-ml-sm" v-if="state.swaggerUrl" :href="state.swaggerUrl + '#/' + tag.name + '/' + route.unique_id">
|
|
245
|
+
<q-icon size="small" name="link"></q-icon>
|
|
246
|
+
</a>
|
|
240
247
|
</span>
|
|
241
248
|
</q-item-section>
|
|
242
249
|
</q-item>
|
|
@@ -259,32 +266,44 @@
|
|
|
259
266
|
<div style="position: relative; width: 100%; height: 100%;">
|
|
260
267
|
<div id="graph" class="adjust-fit"></div>
|
|
261
268
|
<div style="position: absolute; left: 8px; bottom: 8px; z-index: 10; background: rgba(255,255,255,0.85); border-radius: 4px; padding: 2px 8px;">
|
|
262
|
-
<div>
|
|
269
|
+
<div class="q-mt-sm">
|
|
263
270
|
<q-toggle
|
|
264
271
|
v-model="state.focus"
|
|
265
272
|
v-show="schemaCodeName"
|
|
266
273
|
@update:model-value="val => onFocusChange(val)"
|
|
267
274
|
label="Focus"
|
|
275
|
+
dense
|
|
268
276
|
title="pick a schema and toggle focus on to display related nodes only"
|
|
269
277
|
/>
|
|
270
278
|
</div>
|
|
271
|
-
<div>
|
|
279
|
+
<div class="q-mt-sm">
|
|
272
280
|
<q-toggle
|
|
273
281
|
v-if="state.enableBriefMode"
|
|
282
|
+
dense
|
|
274
283
|
v-model="state.brief"
|
|
275
284
|
label="Brief Mode"
|
|
276
285
|
@update:model-value="(val) => toggleBrief(val)"
|
|
277
286
|
title="skip middle classes, config module_prefix to enable it"
|
|
278
287
|
/>
|
|
279
288
|
</div>
|
|
280
|
-
<div>
|
|
289
|
+
<div class="q-mt-sm">
|
|
281
290
|
<q-toggle
|
|
282
291
|
v-model="state.hidePrimitiveRoute"
|
|
283
292
|
@update:model-value="(val) => toggleHidePrimitiveRoute(val)"
|
|
284
293
|
label="Hide Primitive"
|
|
294
|
+
dense
|
|
285
295
|
title="hide routes return primitive"
|
|
286
296
|
/>
|
|
287
297
|
</div>
|
|
298
|
+
<div class="q-mt-sm">
|
|
299
|
+
<q-toggle
|
|
300
|
+
v-model="state.showModule"
|
|
301
|
+
@update:model-value="(val) => toggleShowModule(val)"
|
|
302
|
+
label="Show Module Cluster"
|
|
303
|
+
dense
|
|
304
|
+
title="show module cluster"
|
|
305
|
+
/>
|
|
306
|
+
</div>
|
|
288
307
|
</div>
|
|
289
308
|
</div>
|
|
290
309
|
</template>
|
fastapi_voyager/web/vue-main.js
CHANGED
|
@@ -24,6 +24,7 @@ const app = createApp({
|
|
|
24
24
|
focus: false,
|
|
25
25
|
hidePrimitiveRoute: false,
|
|
26
26
|
generating: false,
|
|
27
|
+
swaggerUrl: null,
|
|
27
28
|
rawTags: [], // [{ name, routes: [{ id, name }] }]
|
|
28
29
|
rawSchemas: new Set(), // [{ name, id }]
|
|
29
30
|
rawSchemasFull: {}, // full schemas dict: { [schema.id]: schema }
|
|
@@ -33,6 +34,7 @@ const app = createApp({
|
|
|
33
34
|
detailDrawer: false,
|
|
34
35
|
drawerWidth: 300, // drawer 宽度
|
|
35
36
|
version: "", // version from backend
|
|
37
|
+
showModule: true
|
|
36
38
|
});
|
|
37
39
|
|
|
38
40
|
const showDetail = ref(false);
|
|
@@ -78,6 +80,7 @@ const app = createApp({
|
|
|
78
80
|
}, {});
|
|
79
81
|
state.enableBriefMode = data.enable_brief_mode || false;
|
|
80
82
|
state.version = data.version || "";
|
|
83
|
+
state.swaggerUrl = data.swagger_url || null
|
|
81
84
|
|
|
82
85
|
// default route options placeholder
|
|
83
86
|
} catch (e) {
|
|
@@ -110,6 +113,7 @@ const app = createApp({
|
|
|
110
113
|
show_fields: state.showFields,
|
|
111
114
|
brief: state.brief,
|
|
112
115
|
hide_primitive_route: state.hidePrimitiveRoute,
|
|
116
|
+
show_module: state.showModule,
|
|
113
117
|
};
|
|
114
118
|
const res = await fetch("dot", {
|
|
115
119
|
method: "POST",
|
|
@@ -225,7 +229,6 @@ const app = createApp({
|
|
|
225
229
|
state.routeId = "";
|
|
226
230
|
state.schemaId = null;
|
|
227
231
|
// state.showFields = "object";
|
|
228
|
-
state.brief = false;
|
|
229
232
|
state.focus = false;
|
|
230
233
|
schemaCodeName.value = "";
|
|
231
234
|
onGenerate();
|
|
@@ -260,6 +263,11 @@ const app = createApp({
|
|
|
260
263
|
onGenerate();
|
|
261
264
|
}
|
|
262
265
|
|
|
266
|
+
function toggleShowModule(val) {
|
|
267
|
+
state.showModule = val;
|
|
268
|
+
onGenerate()
|
|
269
|
+
}
|
|
270
|
+
|
|
263
271
|
function toggleShowField(field) {
|
|
264
272
|
state.showFields = field;
|
|
265
273
|
onGenerate(false);
|
|
@@ -336,6 +344,7 @@ const app = createApp({
|
|
|
336
344
|
toggleShowField,
|
|
337
345
|
startDragDrawer,
|
|
338
346
|
onFocusChange,
|
|
347
|
+
toggleShowModule
|
|
339
348
|
};
|
|
340
349
|
},
|
|
341
350
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-voyager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.2
|
|
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
|
|
@@ -16,6 +16,8 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
21
|
Requires-Python: >=3.10
|
|
20
22
|
Requires-Dist: fastapi>=0.110
|
|
21
23
|
Requires-Dist: pydantic-resolve>=1.13.2
|
|
@@ -32,7 +34,14 @@ Description-Content-Type: text/markdown
|
|
|
32
34
|
|
|
33
35
|
> This repo is still in early stage, it supports pydantic v2 only
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
FastAPI can help you:
|
|
38
|
+
|
|
39
|
+
- design your API
|
|
40
|
+
- inspect your API
|
|
41
|
+
- refactor your API
|
|
42
|
+
|
|
43
|
+
interactively !!
|
|
44
|
+
|
|
36
45
|
|
|
37
46
|
[visit online demo](https://www.newsyeah.fun/voyager/) of project: [composition oriented development pattern](https://github.com/allmonday/composition-oriented-development-pattern)
|
|
38
47
|
|
|
@@ -51,6 +60,22 @@ uv add fastapi-voyager
|
|
|
51
60
|
voyager -m path.to.your.app.module --server
|
|
52
61
|
```
|
|
53
62
|
|
|
63
|
+
## Mount into project
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from fastapi import FastAPI
|
|
67
|
+
from fastapi_voyager import create_voyager
|
|
68
|
+
from tests.demo import app
|
|
69
|
+
|
|
70
|
+
app.mount('/voyager', create_voyager(
|
|
71
|
+
app,
|
|
72
|
+
module_color={"tests.service": "red"},
|
|
73
|
+
module_prefix="tests.service"),
|
|
74
|
+
swagger_url="/docs")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
more about [sub application](https://fastapi.tiangolo.com/advanced/sub-applications/?h=sub)
|
|
78
|
+
|
|
54
79
|
|
|
55
80
|
## Feature
|
|
56
81
|
|
|
@@ -99,20 +124,6 @@ click a node to highlight it's upperstream and downstream nodes. figure out the
|
|
|
99
124
|
<img width="882" height="445" alt="image" src="https://github.com/user-attachments/assets/158560ef-63ca-4991-9b7d-587be4fa04e4" />
|
|
100
125
|
|
|
101
126
|
|
|
102
|
-
## Mount to target project
|
|
103
|
-
|
|
104
|
-
```python
|
|
105
|
-
from fastapi import FastAPI
|
|
106
|
-
from fastapi_voyager import create_voyager
|
|
107
|
-
from tests.demo import app
|
|
108
|
-
|
|
109
|
-
app.mount('/voyager', create_voyager(
|
|
110
|
-
app,
|
|
111
|
-
module_color={"tests.service": "red"},
|
|
112
|
-
module_prefix="tests.service"))
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
more about [sub application](https://fastapi.tiangolo.com/advanced/sub-applications/?h=sub)
|
|
116
127
|
|
|
117
128
|
|
|
118
129
|
## Command Line Usage
|
|
@@ -244,6 +255,7 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
|
|
|
244
255
|
- [x] fix focus in brief-mode
|
|
245
256
|
- [x] ui: adjust focus position
|
|
246
257
|
- [x] refactor naming
|
|
258
|
+
- [x] fix layout issue when rendering huge graph
|
|
247
259
|
- 0.10.4
|
|
248
260
|
- [x] fix: when focus is on, should ensure changes from other params not broken.
|
|
249
261
|
- 0.10.5
|
|
@@ -251,19 +263,20 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
|
|
|
251
263
|
|
|
252
264
|
|
|
253
265
|
#### 0.11
|
|
254
|
-
-
|
|
255
|
-
- [
|
|
256
|
-
- [
|
|
257
|
-
- [
|
|
258
|
-
|
|
259
|
-
- [
|
|
260
|
-
-
|
|
261
|
-
- [ ]
|
|
262
|
-
- [ ]
|
|
263
|
-
- [ ]
|
|
264
|
-
- [ ]
|
|
266
|
+
- 0.11.1
|
|
267
|
+
- [x] support opening route in swagger
|
|
268
|
+
- [x] config docs path
|
|
269
|
+
- [x] provide option to hide routes in brief mode (auto hide in full graph mode)
|
|
270
|
+
- 0.11.2
|
|
271
|
+
- [x] enable/disable module cluster (to save space)
|
|
272
|
+
- 0.11.3
|
|
273
|
+
- [ ] add loading for field detail panel
|
|
274
|
+
- [ ] logging information
|
|
275
|
+
- [ ] sort field name
|
|
276
|
+
- [ ] set max limit for fields
|
|
265
277
|
|
|
266
278
|
#### 0.12
|
|
279
|
+
- [ ] add tests
|
|
267
280
|
- [ ] integration with pydantic-resolve
|
|
268
281
|
- [ ] show hint for resolve, post fields
|
|
269
282
|
- [ ] display loader as edges
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
fastapi_voyager/__init__.py,sha256=tZy0Nkj8kTaMgbvHy-mGxVcFGVX0Km-36dnzsAIG2uk,230
|
|
2
2
|
fastapi_voyager/cli.py,sha256=kQb4g6JEGZR99e5r8LyFFEeb_-uT-n_gp_sDoYG3R7k,11118
|
|
3
|
-
fastapi_voyager/filter.py,sha256=
|
|
3
|
+
fastapi_voyager/filter.py,sha256=GY2J9Vfsf_wbFwC-0t74-Lf-OlO77PnhEXD_rmgkfSw,11574
|
|
4
4
|
fastapi_voyager/module.py,sha256=Z2QHNmiLk6ZAJlm2nSmO875Q33TweSg8UxZSzIpU9zY,3499
|
|
5
|
-
fastapi_voyager/render.py,sha256=
|
|
6
|
-
fastapi_voyager/server.py,sha256=
|
|
7
|
-
fastapi_voyager/type.py,sha256=
|
|
5
|
+
fastapi_voyager/render.py,sha256=vdwqIync2wsP8gMPY0v_XjRhdPBtbKyRT8yTBa_Ep3Y,8744
|
|
6
|
+
fastapi_voyager/server.py,sha256=TOzGEyzE_lviWMcKuSYHDaoArMApENIfvzHdAzOuKY4,6679
|
|
7
|
+
fastapi_voyager/type.py,sha256=VmcTB1G-LOT70EWCzi4LU_FUkSGWUIBJX15T_J5HnOo,1764
|
|
8
8
|
fastapi_voyager/type_helper.py,sha256=hjBC4E0tgBpQDlYxGg74uK07SXjsrAgictEETJfIpYM,9231
|
|
9
|
-
fastapi_voyager/version.py,sha256=
|
|
10
|
-
fastapi_voyager/voyager.py,sha256=
|
|
9
|
+
fastapi_voyager/version.py,sha256=0HoMD0zNdXsQi-ZwAZecr3qNO9tE7hFLleGl1vf67AA,49
|
|
10
|
+
fastapi_voyager/voyager.py,sha256=hNal25S5Hi_ZRe-gnmdUKt8tnRd-BRCrzaybAEJ_1HI,13498
|
|
11
11
|
fastapi_voyager/web/graph-ui.js,sha256=DTedkpZNbtufexONVkJ8mOwF_-VnvxoReYHtox6IKR4,5842
|
|
12
12
|
fastapi_voyager/web/graphviz.svg.css,sha256=zDCjjpT0Idufu5YOiZI76PL70-avP3vTyzGPh9M85Do,1563
|
|
13
13
|
fastapi_voyager/web/graphviz.svg.js,sha256=lvAdbjHc-lMSk4GQp-iqYA2PCFX4RKnW7dFaoe0LUHs,16005
|
|
14
|
-
fastapi_voyager/web/index.html,sha256=
|
|
14
|
+
fastapi_voyager/web/index.html,sha256=9zp6id31DB_cWdAoCkO5NkeHCMTnNaz2VOdJO24__eM,17837
|
|
15
15
|
fastapi_voyager/web/quasar.min.css,sha256=F5jQe7X2XT54VlvAaa2V3GsBFdVD-vxDZeaPLf6U9CU,203145
|
|
16
16
|
fastapi_voyager/web/quasar.min.js,sha256=h0ftyPMW_CRiyzeVfQqiup0vrVt4_QWojpqmpnpn07E,502974
|
|
17
|
-
fastapi_voyager/web/vue-main.js,sha256=
|
|
17
|
+
fastapi_voyager/web/vue-main.js,sha256=sUjLmn06jYy1guwxAcouHwkcjxGD2UgZgkiiIEDbk04,10663
|
|
18
18
|
fastapi_voyager/web/component/render-graph.js,sha256=e8Xgh2Kl-nYU0P1gstEmAepCgFnk2J6UdxW8TlMafGs,2322
|
|
19
19
|
fastapi_voyager/web/component/route-code-display.js,sha256=8NJPPjNRUC21gjpY8XYEQs4RBbhX1pCiqEhJp39ku6k,3678
|
|
20
20
|
fastapi_voyager/web/component/schema-code-display.js,sha256=UgFotzvqSuhnPXNOr6w_r1fV2_savRiCdokEvferutE,6244
|
|
@@ -26,8 +26,8 @@ fastapi_voyager/web/icon/favicon-16x16.png,sha256=JC07jEzfIYxBIoQn_FHXvyHuxESdhW
|
|
|
26
26
|
fastapi_voyager/web/icon/favicon-32x32.png,sha256=C7v1h58cfWOsiLp9yOIZtlx-dLasBcq3NqpHVGRmpt4,1859
|
|
27
27
|
fastapi_voyager/web/icon/favicon.ico,sha256=tZolYIXkkBcFiYl1A8ksaXN2VjGamzcSdes838dLvNc,15406
|
|
28
28
|
fastapi_voyager/web/icon/site.webmanifest,sha256=ep4Hzh9zhmiZF2At3Fp1dQrYQuYF_3ZPZxc1KcGBvwQ,263
|
|
29
|
-
fastapi_voyager-0.
|
|
30
|
-
fastapi_voyager-0.
|
|
31
|
-
fastapi_voyager-0.
|
|
32
|
-
fastapi_voyager-0.
|
|
33
|
-
fastapi_voyager-0.
|
|
29
|
+
fastapi_voyager-0.11.2.dist-info/METADATA,sha256=ImBnVBoy2rqVkdOrd037aIgj9V8pRc0cN7dl5aXv3dM,9917
|
|
30
|
+
fastapi_voyager-0.11.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
31
|
+
fastapi_voyager-0.11.2.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
|
|
32
|
+
fastapi_voyager-0.11.2.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
|
|
33
|
+
fastapi_voyager-0.11.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|