fastapi-voyager 0.10.4__tar.gz → 0.11.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.10.4 → fastapi_voyager-0.11.1}/PKG-INFO +16 -8
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/README.md +15 -7
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/filter.py +91 -1
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/render.py +3 -1
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/server.py +16 -3
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/type.py +3 -1
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/version.py +1 -1
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/voyager.py +28 -4
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/component/schema-field-filter.js +5 -6
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/graph-ui.js +56 -25
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/index.html +8 -5
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/vue-main.js +55 -52
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/tests/programatic.py +1 -1
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/.gitignore +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/.python-version +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/LICENSE +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/pyproject.toml +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/__init__.py +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/cli.py +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/module.py +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/type_helper.py +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/component/render-graph.js +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/quasar.min.css +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/quasar.min.js +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/tests/__init__.py +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/tests/demo.py +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/tests/demo_anno.py +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/tests/service/__init__.py +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/tests/service/schema.py +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/tests/test_analysis.py +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/tests/test_filter.py +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/tests/test_generic.py +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/tests/test_import.py +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/tests/test_module.py +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/tests/test_type_helper.py +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/uv.lock +0 -0
- {fastapi_voyager-0.10.4 → fastapi_voyager-0.11.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.11.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
|
|
@@ -244,20 +244,28 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
|
|
|
244
244
|
- [x] fix focus in brief-mode
|
|
245
245
|
- [x] ui: adjust focus position
|
|
246
246
|
- [x] refactor naming
|
|
247
|
+
- [x] fix layout issue when rendering huge graph
|
|
247
248
|
- 0.10.4
|
|
248
249
|
- [x] fix: when focus is on, should ensure changes from other params not broken.
|
|
250
|
+
- 0.10.5
|
|
251
|
+
- [x] double click to show details, and highlight as tomato
|
|
249
252
|
|
|
250
253
|
|
|
251
254
|
#### 0.11
|
|
252
|
-
-
|
|
253
|
-
- [
|
|
254
|
-
- [
|
|
255
|
-
- [
|
|
256
|
-
|
|
257
|
-
- [ ]
|
|
258
|
-
- [ ]
|
|
255
|
+
- 0.11.1
|
|
256
|
+
- [x] support opening route in swagger
|
|
257
|
+
- [x] config docs path
|
|
258
|
+
- [x] provide option to hide routes in brief mode (auto hide in full graph mode)
|
|
259
|
+
- 0.11.2
|
|
260
|
+
- [ ] enable/disable module cluster (to save space)
|
|
261
|
+
- [ ] logging information
|
|
262
|
+
- [ ] sort field name
|
|
263
|
+
- [ ] set max limit for fields
|
|
264
|
+
- [ ] add info icon alone with schema node
|
|
265
|
+
- [ ] add loading for field detail panel
|
|
259
266
|
|
|
260
267
|
#### 0.12
|
|
268
|
+
- [ ] add tests
|
|
261
269
|
- [ ] integration with pydantic-resolve
|
|
262
270
|
- [ ] show hint for resolve, post fields
|
|
263
271
|
- [ ] display loader as edges
|
|
@@ -217,20 +217,28 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
|
|
|
217
217
|
- [x] fix focus in brief-mode
|
|
218
218
|
- [x] ui: adjust focus position
|
|
219
219
|
- [x] refactor naming
|
|
220
|
+
- [x] fix layout issue when rendering huge graph
|
|
220
221
|
- 0.10.4
|
|
221
222
|
- [x] fix: when focus is on, should ensure changes from other params not broken.
|
|
223
|
+
- 0.10.5
|
|
224
|
+
- [x] double click to show details, and highlight as tomato
|
|
222
225
|
|
|
223
226
|
|
|
224
227
|
#### 0.11
|
|
225
|
-
-
|
|
226
|
-
- [
|
|
227
|
-
- [
|
|
228
|
-
- [
|
|
229
|
-
|
|
230
|
-
- [ ]
|
|
231
|
-
- [ ]
|
|
228
|
+
- 0.11.1
|
|
229
|
+
- [x] support opening route in swagger
|
|
230
|
+
- [x] config docs path
|
|
231
|
+
- [x] provide option to hide routes in brief mode (auto hide in full graph mode)
|
|
232
|
+
- 0.11.2
|
|
233
|
+
- [ ] enable/disable module cluster (to save space)
|
|
234
|
+
- [ ] logging information
|
|
235
|
+
- [ ] sort field name
|
|
236
|
+
- [ ] set max limit for fields
|
|
237
|
+
- [ ] add info icon alone with schema node
|
|
238
|
+
- [ ] add loading for field detail panel
|
|
232
239
|
|
|
233
240
|
#### 0.12
|
|
241
|
+
- [ ] add tests
|
|
234
242
|
- [ ] integration with pydantic-resolve
|
|
235
243
|
- [ ] show hint for resolve, post fields
|
|
236
244
|
- [ ] display loader as edges
|
|
@@ -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
|
|
@@ -37,7 +37,7 @@ class Renderer:
|
|
|
37
37
|
fields_parts.append(field_str)
|
|
38
38
|
|
|
39
39
|
header_color = 'tomato' if node.id == self.schema else '#009485'
|
|
40
|
-
header = f"""<tr><td cellpadding="
|
|
40
|
+
header = f"""<tr><td cellpadding="6" bgcolor="{header_color}" align="center" colspan="1" port="{PK}"> <font color="white"> {node.name} </font></td> </tr>"""
|
|
41
41
|
field_content = ''.join(fields_parts) if fields_parts else ''
|
|
42
42
|
return f"""<<table border="1" cellborder="0" cellpadding="0" bgcolor="white"> {header} {field_content} </table>>"""
|
|
43
43
|
|
|
@@ -59,6 +59,8 @@ class Renderer:
|
|
|
59
59
|
return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid, dashed", dir="back", minlen=3, taillabel = "< inherit >", color = "purple", tailport="n"];"""
|
|
60
60
|
elif link.type == 'subset':
|
|
61
61
|
return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid, dashed", dir="back", minlen=3, taillabel = "< subset >", color = "orange", tailport="n"];"""
|
|
62
|
+
elif link.type == 'tag_to_schema':
|
|
63
|
+
return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid", dir="back", arrowtail="odot", minlen=3];"""
|
|
62
64
|
else:
|
|
63
65
|
raise ValueError(f'Unknown link type: {link.type}')
|
|
64
66
|
|
|
@@ -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
|
|
@@ -33,9 +34,11 @@ class Payload(BaseModel):
|
|
|
33
34
|
brief: bool = False
|
|
34
35
|
hide_primitive_route: bool = False
|
|
35
36
|
|
|
37
|
+
|
|
36
38
|
def create_route(
|
|
37
39
|
target_app: FastAPI,
|
|
38
40
|
module_color: dict[str, str] | None = None,
|
|
41
|
+
swagger_url: Optional[str] = None,
|
|
39
42
|
module_prefix: Optional[str] = None,
|
|
40
43
|
):
|
|
41
44
|
"""
|
|
@@ -56,7 +59,13 @@ def create_route(
|
|
|
56
59
|
schemas = voyager.nodes[:]
|
|
57
60
|
schemas.sort(key=lambda s: s.name)
|
|
58
61
|
|
|
59
|
-
return OptionParam(
|
|
62
|
+
return OptionParam(
|
|
63
|
+
tags=tags,
|
|
64
|
+
schemas=schemas,
|
|
65
|
+
dot=dot,
|
|
66
|
+
enable_brief_mode=bool(module_prefix),
|
|
67
|
+
version=__version__,
|
|
68
|
+
swagger_url=swagger_url)
|
|
60
69
|
|
|
61
70
|
@router.post("/dot", response_class=PlainTextResponse)
|
|
62
71
|
def get_filtered_dot(payload: Payload) -> str:
|
|
@@ -71,7 +80,10 @@ def create_route(
|
|
|
71
80
|
)
|
|
72
81
|
voyager.analysis(target_app)
|
|
73
82
|
if payload.brief:
|
|
74
|
-
|
|
83
|
+
if payload.tags:
|
|
84
|
+
return voyager.render_tag_level_brief_dot(module_prefix=module_prefix)
|
|
85
|
+
else:
|
|
86
|
+
return voyager.render_overall_brief_dot(module_prefix=module_prefix)
|
|
75
87
|
else:
|
|
76
88
|
return voyager.render_dot()
|
|
77
89
|
|
|
@@ -196,8 +208,9 @@ def create_voyager(
|
|
|
196
208
|
module_color: dict[str, str] | None = None,
|
|
197
209
|
gzip_minimum_size: int | None = 500,
|
|
198
210
|
module_prefix: Optional[str] = None,
|
|
211
|
+
swagger_url: Optional[str] = None,
|
|
199
212
|
) -> FastAPI:
|
|
200
|
-
router = create_route(target_app, module_color=module_color, module_prefix=module_prefix)
|
|
213
|
+
router = create_route(target_app, module_color=module_color, module_prefix=module_prefix, swagger_url=swagger_url)
|
|
201
214
|
|
|
202
215
|
app = FastAPI(title="fastapi-voyager demo server")
|
|
203
216
|
if gzip_minimum_size is not None and gzip_minimum_size >= 0:
|
|
@@ -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:
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.11.1"
|
|
@@ -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
|
|
|
@@ -123,6 +121,7 @@ class Voyager:
|
|
|
123
121
|
id=route_id,
|
|
124
122
|
name=route_name,
|
|
125
123
|
module=route_module,
|
|
124
|
+
unique_id=route.unique_id,
|
|
126
125
|
response_schema=get_type_name(route.response_model),
|
|
127
126
|
is_primitive=is_primitive_response
|
|
128
127
|
)
|
|
@@ -307,7 +306,8 @@ class Voyager:
|
|
|
307
306
|
_tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
|
|
308
307
|
return renderer.render_dot(_tags, _routes, _nodes, _links)
|
|
309
308
|
|
|
310
|
-
|
|
309
|
+
|
|
310
|
+
def render_tag_level_brief_dot(self, module_prefix: str | None = None):
|
|
311
311
|
_tags, _routes, _nodes, _links = filter_graph(
|
|
312
312
|
schema=self.schema,
|
|
313
313
|
schema_field=self.schema_field,
|
|
@@ -328,5 +328,29 @@ class Voyager:
|
|
|
328
328
|
|
|
329
329
|
renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema)
|
|
330
330
|
|
|
331
|
+
_tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
|
|
332
|
+
return renderer.render_dot(_tags, _routes, _nodes, _links, True)
|
|
333
|
+
|
|
334
|
+
def render_overall_brief_dot(self, module_prefix: str | None = None):
|
|
335
|
+
_tags, _routes, _nodes, _links = filter_graph(
|
|
336
|
+
schema=self.schema,
|
|
337
|
+
schema_field=self.schema_field,
|
|
338
|
+
tags=self.tags,
|
|
339
|
+
routes=self.routes,
|
|
340
|
+
nodes=self.nodes,
|
|
341
|
+
links=self.links,
|
|
342
|
+
node_set=self.node_set,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
_tags, _routes, _nodes, _links = filter_subgraph_from_tag_to_schema_by_module_prefix(
|
|
346
|
+
module_prefix=module_prefix,
|
|
347
|
+
tags=_tags,
|
|
348
|
+
routes=_routes,
|
|
349
|
+
nodes=_nodes,
|
|
350
|
+
links=_links,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema)
|
|
354
|
+
|
|
331
355
|
_tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
|
|
332
356
|
return renderer.render_dot(_tags, _routes, _nodes, _links, True)
|
|
@@ -42,11 +42,12 @@ export default defineComponent({
|
|
|
42
42
|
async function loadSchemas() {
|
|
43
43
|
// Use externally provided props.schemas dict directly; no network call.
|
|
44
44
|
state.error = null;
|
|
45
|
-
const dict =
|
|
45
|
+
const dict =
|
|
46
|
+
props.schemas && typeof props.schemas === "object" ? props.schemas : {};
|
|
46
47
|
// Flatten to array for local operations
|
|
47
48
|
state.schemas = Object.values(dict);
|
|
48
49
|
state.schemaOptions = state.schemas.map((s) => ({
|
|
49
|
-
label: `${s.name}
|
|
50
|
+
label: `${s.name} - ${s.id}`,
|
|
50
51
|
value: s.id,
|
|
51
52
|
}));
|
|
52
53
|
// Maintain compatibility: loadingSchemas flag toggled quickly (no async work)
|
|
@@ -57,7 +58,7 @@ export default defineComponent({
|
|
|
57
58
|
const needle = (val || "").toLowerCase();
|
|
58
59
|
update(() => {
|
|
59
60
|
let opts = state.schemas.map((s) => ({
|
|
60
|
-
label: `${s.name}
|
|
61
|
+
label: `${s.name} - ${s.id}`,
|
|
61
62
|
value: s.id,
|
|
62
63
|
}));
|
|
63
64
|
if (needle) {
|
|
@@ -127,7 +128,6 @@ export default defineComponent({
|
|
|
127
128
|
}
|
|
128
129
|
});
|
|
129
130
|
|
|
130
|
-
|
|
131
131
|
function close() {
|
|
132
132
|
emit("close");
|
|
133
133
|
}
|
|
@@ -178,13 +178,12 @@ export default defineComponent({
|
|
|
178
178
|
:disable="!state.schemaFullname"
|
|
179
179
|
:loading="state.querying"
|
|
180
180
|
@click="onQuery" />
|
|
181
|
-
</div>
|
|
182
181
|
<q-btn
|
|
183
182
|
flat dense round icon="close"
|
|
184
183
|
aria-label="Close"
|
|
185
184
|
@click="close"
|
|
186
|
-
style="position:absolute; top:6px; right:6px; z-index:11; background:rgba(255,255,255,0.85);"
|
|
187
185
|
/>
|
|
186
|
+
</div>
|
|
188
187
|
<div v-if="state.error" style="position:absolute; top:52px; left:8px; z-index:10; color:#c10015; font-size:12px;">{{ state.error }}</div>
|
|
189
188
|
<div id="graph-schema-field" style="width:100%; height:100%; overflow:auto; background:#fafafa"></div>
|
|
190
189
|
</div>
|
|
@@ -57,6 +57,13 @@ export class GraphUI {
|
|
|
57
57
|
return $result;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
highlightSchemaBanner(node) {
|
|
61
|
+
const ele = node.querySelector("polygon[fill='#009485']")
|
|
62
|
+
if (ele) {
|
|
63
|
+
ele.setAttribute('fill', 'tomato');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
_init() {
|
|
61
68
|
const self = this;
|
|
62
69
|
$(this.selector).graphviz({
|
|
@@ -65,6 +72,26 @@ export class GraphUI {
|
|
|
65
72
|
ready: function () {
|
|
66
73
|
self.gv = this;
|
|
67
74
|
|
|
75
|
+
self.gv.nodes().dblclick(function (event) {
|
|
76
|
+
event.stopPropagation();
|
|
77
|
+
try {
|
|
78
|
+
self.highlightSchemaBanner(this)
|
|
79
|
+
} catch(e) {
|
|
80
|
+
console.log(e)
|
|
81
|
+
}
|
|
82
|
+
const set = $();
|
|
83
|
+
set.push(this);
|
|
84
|
+
// const obj = { set, direction: "bidirectional" };
|
|
85
|
+
const schemaName = event.currentTarget.dataset.name;
|
|
86
|
+
// self.currentSelection = [obj];
|
|
87
|
+
if (schemaName) {
|
|
88
|
+
try {
|
|
89
|
+
self.options.onSchemaClick(schemaName);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.warn("onSchemaClick callback failed", e);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
68
95
|
self.gv.nodes().click(function (event) {
|
|
69
96
|
const set = $();
|
|
70
97
|
set.push(this);
|
|
@@ -83,12 +110,10 @@ export class GraphUI {
|
|
|
83
110
|
} else {
|
|
84
111
|
self.currentSelection = [obj];
|
|
85
112
|
self._highlight();
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
console.warn("onSchemaClick callback failed", e);
|
|
91
|
-
}
|
|
113
|
+
try {
|
|
114
|
+
self.options.resetCb();
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.warn("resetCb callback failed", e);
|
|
92
117
|
}
|
|
93
118
|
}
|
|
94
119
|
});
|
|
@@ -107,28 +132,34 @@ export class GraphUI {
|
|
|
107
132
|
|
|
108
133
|
// svg 背景点击高亮清空
|
|
109
134
|
|
|
110
|
-
$(document)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
135
|
+
$(document)
|
|
136
|
+
.off("click.graphui")
|
|
137
|
+
.on("click.graphui", function (evt) {
|
|
138
|
+
// 如果点击目标不在 graph 容器内,直接退出
|
|
139
|
+
const graphContainer = $(self.selector)[0];
|
|
140
|
+
if (
|
|
141
|
+
!graphContainer ||
|
|
142
|
+
!evt.target ||
|
|
143
|
+
!graphContainer.contains(evt.target)
|
|
144
|
+
) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
116
147
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
148
|
+
let isNode = false;
|
|
149
|
+
const $nodes = self.gv.nodes();
|
|
150
|
+
const node = evt.target.parentNode;
|
|
151
|
+
$nodes.each(function () {
|
|
152
|
+
if (this === node) {
|
|
153
|
+
isNode = true;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
if (!isNode && self.gv) {
|
|
157
|
+
self.gv.highlight();
|
|
158
|
+
if (self.options.resetCb) {
|
|
159
|
+
self.options.resetCb();
|
|
160
|
+
}
|
|
123
161
|
}
|
|
124
162
|
});
|
|
125
|
-
if (!isNode && self.gv) {
|
|
126
|
-
self.gv.highlight();
|
|
127
|
-
if (self.options.resetCb) {
|
|
128
|
-
self.options.resetCb();
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
163
|
|
|
133
164
|
$(document).on("keydown.graphui", function (evt) {
|
|
134
165
|
if (evt.keyCode === 27 && self.gv) {
|
|
@@ -127,12 +127,10 @@
|
|
|
127
127
|
<ul>
|
|
128
128
|
<li>scroll to zoom in/out</li>
|
|
129
129
|
<li>
|
|
130
|
-
click node to
|
|
131
|
-
chart, esc to unselect
|
|
130
|
+
double click node to view details.
|
|
132
131
|
</li>
|
|
133
132
|
<li>
|
|
134
|
-
shift + click to see schema's dependencies without
|
|
135
|
-
unrelated nodes
|
|
133
|
+
shift + click to see schema's dependencies without unrelated nodes.
|
|
136
134
|
</li>
|
|
137
135
|
</ul>
|
|
138
136
|
</div>
|
|
@@ -149,7 +147,6 @@
|
|
|
149
147
|
:width="state.drawerWidth"
|
|
150
148
|
side="right"
|
|
151
149
|
style="border-left: 1px solid #888;"
|
|
152
|
-
overlay
|
|
153
150
|
bordered
|
|
154
151
|
>
|
|
155
152
|
<!-- 可拖拽的调整栏 -->
|
|
@@ -221,6 +218,9 @@
|
|
|
221
218
|
:name="state.tag == tag.name ? 'folder' : 'folder_open'"
|
|
222
219
|
></q-icon>
|
|
223
220
|
<span>{{ tag.name }} <q-chip style="position:relative; top: -1px;" class="q-ml-sm" dense>{{ tag.routes.length }}</q-chip></span>
|
|
221
|
+
<a target="_blank" class="q-ml-sm" v-if="state.swaggerUrl" :href="state.swaggerUrl + '#/' + tag.name">
|
|
222
|
+
<q-icon size="small" name="link"></q-icon>
|
|
223
|
+
</a>
|
|
224
224
|
</div>
|
|
225
225
|
</template>
|
|
226
226
|
<q-list separator style="overflow: auto; max-height: 60vh;">
|
|
@@ -240,6 +240,9 @@
|
|
|
240
240
|
name="data_object"
|
|
241
241
|
></q-icon>
|
|
242
242
|
{{ route.name }}
|
|
243
|
+
<a target="_blank" class="q-ml-sm" v-if="state.swaggerUrl" :href="state.swaggerUrl + '#/' + tag.name + '/' + route.unique_id">
|
|
244
|
+
<q-icon size="small" name="link"></q-icon>
|
|
245
|
+
</a>
|
|
243
246
|
</span>
|
|
244
247
|
</q-item-section>
|
|
245
248
|
</q-item>
|
|
@@ -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 }
|
|
@@ -48,6 +49,7 @@ const app = createApp({
|
|
|
48
49
|
const schemaCodeName = ref("");
|
|
49
50
|
const routeCodeId = ref("");
|
|
50
51
|
const showRouteDetail = ref(false);
|
|
52
|
+
let graphUI = null;
|
|
51
53
|
|
|
52
54
|
function openDetail() {
|
|
53
55
|
showDetail.value = true;
|
|
@@ -77,6 +79,7 @@ const app = createApp({
|
|
|
77
79
|
}, {});
|
|
78
80
|
state.enableBriefMode = data.enable_brief_mode || false;
|
|
79
81
|
state.version = data.version || "";
|
|
82
|
+
state.swaggerUrl = data.swagger_url || null
|
|
80
83
|
|
|
81
84
|
// default route options placeholder
|
|
82
85
|
} catch (e) {
|
|
@@ -88,13 +91,13 @@ const app = createApp({
|
|
|
88
91
|
|
|
89
92
|
async function onFocusChange(val) {
|
|
90
93
|
if (val) {
|
|
91
|
-
await onGenerate(
|
|
94
|
+
await onGenerate(true); // target could be out of view when switchingfrom big to small
|
|
92
95
|
} else {
|
|
93
|
-
await onGenerate(false)
|
|
96
|
+
await onGenerate(false);
|
|
94
97
|
setTimeout(() => {
|
|
95
|
-
const ele = $(`[data-name='${schemaCodeName.value}'] polygon`)
|
|
96
|
-
ele.
|
|
97
|
-
}, 1)
|
|
98
|
+
const ele = $(`[data-name='${schemaCodeName.value}'] polygon`);
|
|
99
|
+
ele.dblclick();
|
|
100
|
+
}, 1);
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -118,30 +121,31 @@ const app = createApp({
|
|
|
118
121
|
const dotText = await res.text();
|
|
119
122
|
|
|
120
123
|
// create graph instance once
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
124
|
+
if (!graphUI) {
|
|
125
|
+
graphUI = new GraphUI("#graph", {
|
|
126
|
+
onSchemaShiftClick: (id) => {
|
|
127
|
+
if (state.rawSchemas.has(id)) {
|
|
128
|
+
resetDetailPanels();
|
|
129
|
+
schemaFieldFilterSchema.value = id;
|
|
130
|
+
showSchemaFieldFilter.value = true;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
onSchemaClick: (id) => {
|
|
134
|
+
resetDetailPanels();
|
|
135
|
+
if (state.rawSchemas.has(id)) {
|
|
136
|
+
schemaCodeName.value = id;
|
|
137
|
+
state.detailDrawer = true;
|
|
138
|
+
}
|
|
139
|
+
if (id in state.routeItems) {
|
|
140
|
+
routeCodeId.value = id;
|
|
141
|
+
showRouteDetail.value = true;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
resetCb: () => {
|
|
145
|
+
resetDetailPanels();
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
145
149
|
await graphUI.render(dotText, resetZoom);
|
|
146
150
|
} catch (e) {
|
|
147
151
|
console.error("Generate failed", e);
|
|
@@ -213,9 +217,9 @@ const app = createApp({
|
|
|
213
217
|
}
|
|
214
218
|
|
|
215
219
|
function resetDetailPanels() {
|
|
216
|
-
state.detailDrawer = false
|
|
220
|
+
state.detailDrawer = false;
|
|
217
221
|
showRouteDetail.value = false;
|
|
218
|
-
schemaCodeName.value =
|
|
222
|
+
schemaCodeName.value = "";
|
|
219
223
|
}
|
|
220
224
|
|
|
221
225
|
async function onReset() {
|
|
@@ -223,9 +227,8 @@ const app = createApp({
|
|
|
223
227
|
state.routeId = "";
|
|
224
228
|
state.schemaId = null;
|
|
225
229
|
// state.showFields = "object";
|
|
226
|
-
state.
|
|
227
|
-
|
|
228
|
-
schemaCodeName.value = ''
|
|
230
|
+
state.focus = false;
|
|
231
|
+
schemaCodeName.value = "";
|
|
229
232
|
onGenerate();
|
|
230
233
|
}
|
|
231
234
|
|
|
@@ -234,11 +237,11 @@ const app = createApp({
|
|
|
234
237
|
state._tag = tagName;
|
|
235
238
|
state.tag = tagName;
|
|
236
239
|
state.routeId = "";
|
|
237
|
-
state.focus = false
|
|
238
|
-
schemaCodeName.value =
|
|
240
|
+
state.focus = false;
|
|
241
|
+
schemaCodeName.value = "";
|
|
239
242
|
onGenerate();
|
|
240
243
|
} else {
|
|
241
|
-
state._tag = null
|
|
244
|
+
state._tag = null;
|
|
242
245
|
}
|
|
243
246
|
|
|
244
247
|
state.detailDrawer = false;
|
|
@@ -253,8 +256,8 @@ const app = createApp({
|
|
|
253
256
|
}
|
|
254
257
|
state.detailDrawer = false;
|
|
255
258
|
showRouteDetail.value = false;
|
|
256
|
-
state.focus = false
|
|
257
|
-
schemaCodeName.value =
|
|
259
|
+
state.focus = false;
|
|
260
|
+
schemaCodeName.value = "";
|
|
258
261
|
onGenerate();
|
|
259
262
|
}
|
|
260
263
|
|
|
@@ -276,24 +279,24 @@ const app = createApp({
|
|
|
276
279
|
function startDragDrawer(e) {
|
|
277
280
|
const startX = e.clientX;
|
|
278
281
|
const startWidth = state.drawerWidth;
|
|
279
|
-
|
|
282
|
+
|
|
280
283
|
function onMouseMove(moveEvent) {
|
|
281
|
-
const deltaX = startX - moveEvent.clientX;
|
|
284
|
+
const deltaX = startX - moveEvent.clientX;
|
|
282
285
|
const newWidth = Math.max(300, Math.min(800, startWidth + deltaX));
|
|
283
286
|
state.drawerWidth = newWidth;
|
|
284
287
|
}
|
|
285
|
-
|
|
288
|
+
|
|
286
289
|
function onMouseUp() {
|
|
287
|
-
document.removeEventListener(
|
|
288
|
-
document.removeEventListener(
|
|
289
|
-
document.body.style.cursor =
|
|
290
|
-
document.body.style.userSelect =
|
|
290
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
291
|
+
document.removeEventListener("mouseup", onMouseUp);
|
|
292
|
+
document.body.style.cursor = "";
|
|
293
|
+
document.body.style.userSelect = "";
|
|
291
294
|
}
|
|
292
|
-
|
|
293
|
-
document.addEventListener(
|
|
294
|
-
document.addEventListener(
|
|
295
|
-
document.body.style.cursor =
|
|
296
|
-
document.body.style.userSelect =
|
|
295
|
+
|
|
296
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
297
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
298
|
+
document.body.style.cursor = "col-resize";
|
|
299
|
+
document.body.style.userSelect = "none";
|
|
297
300
|
e.preventDefault();
|
|
298
301
|
}
|
|
299
302
|
|
|
@@ -333,7 +336,7 @@ const app = createApp({
|
|
|
333
336
|
renderCoreData,
|
|
334
337
|
toggleShowField,
|
|
335
338
|
startDragDrawer,
|
|
336
|
-
onFocusChange
|
|
339
|
+
onFocusChange,
|
|
337
340
|
};
|
|
338
341
|
},
|
|
339
342
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from fastapi_voyager import create_voyager
|
|
2
2
|
from tests.demo import app
|
|
3
3
|
|
|
4
|
-
app.mount('/voyager', create_voyager(app, module_color={"tests.service": "red"}, module_prefix="tests.service"))
|
|
4
|
+
app.mount('/voyager', create_voyager(app, module_color={"tests.service": "red"}, module_prefix="tests.service", swagger_url='/docs'))
|
|
5
5
|
# app.mount('/voyager', create_voyager(app, module_color={"tests.service": "red"}))
|
|
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.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/component/render-graph.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/icon/apple-touch-icon.png
RENAMED
|
File without changes
|
{fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/icon/favicon-16x16.png
RENAMED
|
File without changes
|
{fastapi_voyager-0.10.4 → fastapi_voyager-0.11.1}/src/fastapi_voyager/web/icon/favicon-32x32.png
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.10.4 → fastapi_voyager-0.11.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|