fastapi-voyager 0.4.5__py3-none-any.whl → 0.5.1__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/cli.py +5 -5
- fastapi_voyager/render.py +163 -0
- fastapi_voyager/server.py +32 -12
- fastapi_voyager/type.py +20 -5
- fastapi_voyager/version.py +1 -1
- fastapi_voyager/{graph.py → voyager.py} +35 -176
- fastapi_voyager/web/component/render-graph.js +59 -0
- fastapi_voyager/web/index.html +51 -2
- fastapi_voyager/web/vue-main.js +98 -9
- {fastapi_voyager-0.4.5.dist-info → fastapi_voyager-0.5.1.dist-info}/METADATA +11 -4
- {fastapi_voyager-0.4.5.dist-info → fastapi_voyager-0.5.1.dist-info}/RECORD +14 -12
- {fastapi_voyager-0.4.5.dist-info → fastapi_voyager-0.5.1.dist-info}/WHEEL +0 -0
- {fastapi_voyager-0.4.5.dist-info → fastapi_voyager-0.5.1.dist-info}/entry_points.txt +0 -0
- {fastapi_voyager-0.4.5.dist-info → fastapi_voyager-0.5.1.dist-info}/licenses/LICENSE +0 -0
fastapi_voyager/cli.py
CHANGED
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
from typing import Optional
|
|
8
8
|
|
|
9
9
|
from fastapi import FastAPI
|
|
10
|
-
from fastapi_voyager.
|
|
10
|
+
from fastapi_voyager.voyager import Voyager
|
|
11
11
|
from fastapi_voyager.version import __version__
|
|
12
12
|
from fastapi_voyager import server as viz_server
|
|
13
13
|
|
|
@@ -49,7 +49,7 @@ def load_fastapi_app_from_file(module_path: str, app_name: str = "app") -> Optio
|
|
|
49
49
|
def load_fastapi_app_from_module(module_name: str, app_name: str = "app") -> Optional[FastAPI]:
|
|
50
50
|
"""Load FastAPI app from a Python module name."""
|
|
51
51
|
try:
|
|
52
|
-
#
|
|
52
|
+
# Temporarily add the current working directory to sys.path
|
|
53
53
|
current_dir = os.getcwd()
|
|
54
54
|
if current_dir not in sys.path:
|
|
55
55
|
sys.path.insert(0, current_dir)
|
|
@@ -73,7 +73,7 @@ def load_fastapi_app_from_module(module_name: str, app_name: str = "app") -> Opt
|
|
|
73
73
|
print(f"Error: No attribute '{app_name}' found in module '{module_name}'")
|
|
74
74
|
return None
|
|
75
75
|
finally:
|
|
76
|
-
#
|
|
76
|
+
# Cleanup: if we added the path, remove it
|
|
77
77
|
if path_added and current_dir in sys.path:
|
|
78
78
|
sys.path.remove(current_dir)
|
|
79
79
|
|
|
@@ -95,7 +95,7 @@ def generate_visualization(
|
|
|
95
95
|
):
|
|
96
96
|
|
|
97
97
|
"""Generate DOT file for FastAPI router visualization."""
|
|
98
|
-
analytics =
|
|
98
|
+
analytics = Voyager(
|
|
99
99
|
include_tags=tags,
|
|
100
100
|
schema=schema,
|
|
101
101
|
show_fields=show_fields,
|
|
@@ -105,7 +105,7 @@ def generate_visualization(
|
|
|
105
105
|
|
|
106
106
|
analytics.analysis(app)
|
|
107
107
|
|
|
108
|
-
dot_content = analytics.
|
|
108
|
+
dot_content = analytics.render_dot()
|
|
109
109
|
|
|
110
110
|
# Optionally write to file
|
|
111
111
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from fastapi_voyager.type import SchemaNode, ModuleNode, Link, Tag, Route, FieldType, PK
|
|
2
|
+
from fastapi_voyager.module import build_module_tree
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Renderer:
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
*,
|
|
9
|
+
show_fields: FieldType = 'single',
|
|
10
|
+
module_color: dict[str, str] | None = None,
|
|
11
|
+
schema: str | None = None,
|
|
12
|
+
) -> None:
|
|
13
|
+
self.show_fields = show_fields if show_fields in ('single', 'object', 'all') else 'single'
|
|
14
|
+
self.module_color = module_color or {}
|
|
15
|
+
self.schema = schema
|
|
16
|
+
|
|
17
|
+
def render_schema_label(self, node: SchemaNode) -> str:
|
|
18
|
+
has_base_fields = any(f.from_base for f in node.fields)
|
|
19
|
+
fields = [n for n in node.fields if n.from_base is False]
|
|
20
|
+
|
|
21
|
+
if self.show_fields == 'all':
|
|
22
|
+
_fields = fields
|
|
23
|
+
elif self.show_fields == 'object':
|
|
24
|
+
_fields = [f for f in fields if f.is_object is True]
|
|
25
|
+
else: # 'single'
|
|
26
|
+
_fields = []
|
|
27
|
+
|
|
28
|
+
fields_parts: list[str] = []
|
|
29
|
+
if self.show_fields == 'all' and has_base_fields:
|
|
30
|
+
fields_parts.append('<tr><td align="left" cellpadding="8"><font color="#999"> Inherited Fields ... </font></td></tr>')
|
|
31
|
+
|
|
32
|
+
for field in _fields:
|
|
33
|
+
type_name = field.type_name[:25] + '..' if len(field.type_name) > 25 else field.type_name
|
|
34
|
+
display_xml = f'<s align="left">{field.name}: {type_name}</s>' if field.is_exclude else f'{field.name}: {type_name}'
|
|
35
|
+
field_str = f"""<tr><td align="left" port="f{field.name}" cellpadding="8"><font> {display_xml} </font></td></tr>"""
|
|
36
|
+
fields_parts.append(field_str)
|
|
37
|
+
|
|
38
|
+
header_color = 'tomato' if node.id == self.schema else '#009485'
|
|
39
|
+
header = f"""<tr><td cellpadding="1.5" bgcolor="{header_color}" align="center" colspan="1" port="{PK}"> <font color="white"> {node.name} </font> </td> </tr>"""
|
|
40
|
+
field_content = ''.join(fields_parts) if fields_parts else ''
|
|
41
|
+
return f"""<<table border="1" cellborder="0" cellpadding="0" bgcolor="white"> {header} {field_content} </table>>"""
|
|
42
|
+
|
|
43
|
+
def _handle_schema_anchor(self, source: str) -> str:
|
|
44
|
+
if '::' in source:
|
|
45
|
+
a, b = source.split('::', 1)
|
|
46
|
+
return f'"{a}":{b}'
|
|
47
|
+
return f'"{source}"'
|
|
48
|
+
|
|
49
|
+
def render_link(self, link: Link) -> str:
|
|
50
|
+
h = self._handle_schema_anchor
|
|
51
|
+
if link.type == 'tag_route':
|
|
52
|
+
return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid", minlen=3];"""
|
|
53
|
+
elif link.type == 'route_to_schema':
|
|
54
|
+
return f"""{h(link.source)}:e -> {h(link.target)}:{PK} [style = "solid", dir="back", arrowtail="odot", minlen=3];"""
|
|
55
|
+
elif link.type == 'schema':
|
|
56
|
+
return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid", label = "", dir="back", minlen=3, arrowtail="odot"];"""
|
|
57
|
+
elif link.type == 'parent':
|
|
58
|
+
return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid, dashed", dir="back", minlen=3, taillabel = "< inherit >", color = "purple", tailport="n"];"""
|
|
59
|
+
elif link.type == 'subset':
|
|
60
|
+
return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid, dashed", dir="back", minlen=3, taillabel = "< subset >", color = "orange", tailport="n"];"""
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(f'Unknown link type: {link.type}')
|
|
63
|
+
|
|
64
|
+
def render_module(self, mod: ModuleNode) -> str:
|
|
65
|
+
color = self.module_color.get(mod.fullname)
|
|
66
|
+
inner_nodes = [
|
|
67
|
+
f'''
|
|
68
|
+
"{node.id}" [
|
|
69
|
+
label = {self.render_schema_label(node)}
|
|
70
|
+
shape = "plain"
|
|
71
|
+
margin="0.5,0.1"
|
|
72
|
+
];''' for node in mod.schema_nodes
|
|
73
|
+
]
|
|
74
|
+
inner_nodes_str = '\n'.join(inner_nodes)
|
|
75
|
+
child_str = '\n'.join(self.render_module(m) for m in mod.modules)
|
|
76
|
+
return f'''
|
|
77
|
+
subgraph cluster_module_{mod.fullname.replace('.', '_')} {{
|
|
78
|
+
tooltip="{mod.fullname}"
|
|
79
|
+
color = "#666"
|
|
80
|
+
style="rounded"
|
|
81
|
+
label = " {mod.name}"
|
|
82
|
+
labeljust = "l"
|
|
83
|
+
{(f'pencolor = "{color}"' if color else 'pencolor="#ccc"')}
|
|
84
|
+
{(f'penwidth = 3' if color else 'penwidth=""')}
|
|
85
|
+
{inner_nodes_str}
|
|
86
|
+
{child_str}
|
|
87
|
+
}}'''
|
|
88
|
+
|
|
89
|
+
def render_dot(self, tags: list[Tag], routes: list[Route], nodes: list[SchemaNode], links: list[Link]) -> str:
|
|
90
|
+
modules = build_module_tree(nodes)
|
|
91
|
+
|
|
92
|
+
tag_str = '\n'.join([
|
|
93
|
+
f'''
|
|
94
|
+
"{t.id}" [
|
|
95
|
+
label = " {t.name} "
|
|
96
|
+
shape = "record"
|
|
97
|
+
margin="0.5,0.1"
|
|
98
|
+
];''' for t in tags
|
|
99
|
+
])
|
|
100
|
+
|
|
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
|
+
|
|
110
|
+
modules_str = '\n'.join(self.render_module(m) for m in modules)
|
|
111
|
+
link_str = '\n'.join(self.render_link(link) for link in links)
|
|
112
|
+
|
|
113
|
+
dot_str = f'''
|
|
114
|
+
digraph world {{
|
|
115
|
+
pad="0.5"
|
|
116
|
+
nodesep=0.8
|
|
117
|
+
fontname="Helvetica,Arial,sans-serif"
|
|
118
|
+
node [fontname="Helvetica,Arial,sans-serif"]
|
|
119
|
+
edge [
|
|
120
|
+
fontname="Helvetica,Arial,sans-serif"
|
|
121
|
+
color="gray"
|
|
122
|
+
]
|
|
123
|
+
graph [
|
|
124
|
+
rankdir = "LR"
|
|
125
|
+
];
|
|
126
|
+
node [
|
|
127
|
+
fontsize = "16"
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
subgraph cluster_tags {{
|
|
131
|
+
color = "#aaa"
|
|
132
|
+
margin=18
|
|
133
|
+
style="dashed"
|
|
134
|
+
label = " Tags"
|
|
135
|
+
labeljust = "l"
|
|
136
|
+
fontsize = "20"
|
|
137
|
+
{tag_str}
|
|
138
|
+
}}
|
|
139
|
+
|
|
140
|
+
subgraph cluster_router {{
|
|
141
|
+
color = "#aaa"
|
|
142
|
+
margin=18
|
|
143
|
+
style="dashed"
|
|
144
|
+
label = " Routes"
|
|
145
|
+
labeljust = "l"
|
|
146
|
+
fontsize = "20"
|
|
147
|
+
{route_str}
|
|
148
|
+
}}
|
|
149
|
+
|
|
150
|
+
subgraph cluster_schema {{
|
|
151
|
+
color = "#aaa"
|
|
152
|
+
margin=18
|
|
153
|
+
style="dashed"
|
|
154
|
+
label=" Schema"
|
|
155
|
+
labeljust="l"
|
|
156
|
+
fontsize="20"
|
|
157
|
+
{modules_str}
|
|
158
|
+
}}
|
|
159
|
+
|
|
160
|
+
{link_str}
|
|
161
|
+
}}
|
|
162
|
+
'''
|
|
163
|
+
return dot_str
|
fastapi_voyager/server.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import Optional
|
|
3
3
|
from fastapi import FastAPI
|
|
4
4
|
from starlette.middleware.gzip import GZipMiddleware
|
|
5
5
|
from pydantic import BaseModel
|
|
6
|
-
from fastapi.responses import HTMLResponse, PlainTextResponse
|
|
6
|
+
from fastapi.responses import HTMLResponse, PlainTextResponse, JSONResponse
|
|
7
7
|
from fastapi.staticfiles import StaticFiles
|
|
8
|
-
from fastapi_voyager.
|
|
9
|
-
from fastapi_voyager.type import Tag, FieldInfo
|
|
8
|
+
from fastapi_voyager.voyager import Voyager
|
|
9
|
+
from fastapi_voyager.type import Tag, FieldInfo, CoreData
|
|
10
|
+
from fastapi_voyager.render import Renderer
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
WEB_DIR = Path(__file__).parent / "web"
|
|
@@ -50,12 +51,12 @@ def create_app_with_fastapi(
|
|
|
50
51
|
|
|
51
52
|
@app.get("/dot", response_model=OptionParam)
|
|
52
53
|
def get_dot() -> str:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
dot =
|
|
54
|
+
voyager = Voyager(module_color=module_color, load_meta=True)
|
|
55
|
+
voyager.analysis(target_app)
|
|
56
|
+
dot = voyager.render_dot()
|
|
56
57
|
|
|
57
58
|
# include tags and their routes
|
|
58
|
-
tags =
|
|
59
|
+
tags = voyager.tags
|
|
59
60
|
|
|
60
61
|
schemas = [
|
|
61
62
|
SchemaType(
|
|
@@ -64,7 +65,7 @@ def create_app_with_fastapi(
|
|
|
64
65
|
fields=s.fields,
|
|
65
66
|
source_code=s.source_code,
|
|
66
67
|
vscode_link=s.vscode_link
|
|
67
|
-
) for s in
|
|
68
|
+
) for s in voyager.nodes
|
|
68
69
|
]
|
|
69
70
|
schemas.sort(key=lambda s: s.name)
|
|
70
71
|
|
|
@@ -72,7 +73,7 @@ def create_app_with_fastapi(
|
|
|
72
73
|
|
|
73
74
|
@app.post("/dot", response_class=PlainTextResponse)
|
|
74
75
|
def get_filtered_dot(payload: Payload) -> str:
|
|
75
|
-
|
|
76
|
+
voyager = Voyager(
|
|
76
77
|
include_tags=payload.tags,
|
|
77
78
|
schema=payload.schema_name,
|
|
78
79
|
schema_field=payload.schema_field,
|
|
@@ -81,8 +82,27 @@ def create_app_with_fastapi(
|
|
|
81
82
|
route_name=payload.route_name,
|
|
82
83
|
load_meta=False,
|
|
83
84
|
)
|
|
84
|
-
|
|
85
|
-
return
|
|
85
|
+
voyager.analysis(target_app)
|
|
86
|
+
return voyager.render_dot()
|
|
87
|
+
|
|
88
|
+
@app.post("/dot-core-data", response_model=CoreData)
|
|
89
|
+
def get_filtered_dot_core_data(payload: Payload) -> str:
|
|
90
|
+
voyager = Voyager(
|
|
91
|
+
include_tags=payload.tags,
|
|
92
|
+
schema=payload.schema_name,
|
|
93
|
+
schema_field=payload.schema_field,
|
|
94
|
+
show_fields=payload.show_fields,
|
|
95
|
+
module_color=module_color,
|
|
96
|
+
route_name=payload.route_name,
|
|
97
|
+
load_meta=False,
|
|
98
|
+
)
|
|
99
|
+
voyager.analysis(target_app)
|
|
100
|
+
return voyager.dump_core_data()
|
|
101
|
+
|
|
102
|
+
@app.post('/dot-render-core-data', response_class=PlainTextResponse)
|
|
103
|
+
def render_dot_from_core_data(core_data: CoreData) -> str:
|
|
104
|
+
renderer = Renderer(show_fields=core_data.show_fields, module_color=core_data.module_color, schema=core_data.schema)
|
|
105
|
+
return renderer.render_dot(core_data.tags, core_data.routes, core_data.nodes, core_data.links)
|
|
86
106
|
|
|
87
107
|
@app.get("/", response_class=HTMLResponse)
|
|
88
108
|
def index():
|
fastapi_voyager/type.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
from dataclasses import
|
|
2
|
-
from
|
|
1
|
+
from dataclasses import field
|
|
2
|
+
from pydantic.dataclasses import dataclass
|
|
3
|
+
from typing import Literal, Optional
|
|
3
4
|
|
|
4
5
|
@dataclass
|
|
5
6
|
class NodeBase:
|
|
@@ -39,11 +40,12 @@ class ModuleNode:
|
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
# type:
|
|
42
|
-
# -
|
|
43
|
+
# - tag_route: tag -> route
|
|
44
|
+
# - route_to_schema: route -> response model
|
|
43
45
|
# - subset: schema -> schema (subset)
|
|
44
46
|
# - parent: schema -> schema (inheritance)
|
|
45
|
-
# -
|
|
46
|
-
LinkType = Literal['
|
|
47
|
+
# - schema: schema -> schema (field reference)
|
|
48
|
+
LinkType = Literal['schema', 'parent', 'tag_route', 'subset', 'route_to_schema']
|
|
47
49
|
|
|
48
50
|
@dataclass
|
|
49
51
|
class Link:
|
|
@@ -55,3 +57,16 @@ class Link:
|
|
|
55
57
|
source_origin: str
|
|
56
58
|
target_origin: str
|
|
57
59
|
type: LinkType
|
|
60
|
+
|
|
61
|
+
FieldType = Literal['single', 'object', 'all']
|
|
62
|
+
PK = "PK"
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class CoreData:
|
|
66
|
+
tags: list[Tag]
|
|
67
|
+
routes: list[Route]
|
|
68
|
+
nodes: list[SchemaNode]
|
|
69
|
+
links: list[Link]
|
|
70
|
+
show_fields: FieldType
|
|
71
|
+
module_color: Optional[dict[str, str]] = None
|
|
72
|
+
schema: Optional[str] = None
|
fastapi_voyager/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.5.1"
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import inspect
|
|
2
|
-
from typing import Literal
|
|
3
2
|
from fastapi import FastAPI, routing
|
|
4
3
|
from fastapi_voyager.type_helper import (
|
|
5
4
|
get_core_types,
|
|
@@ -12,20 +11,18 @@ from fastapi_voyager.type_helper import (
|
|
|
12
11
|
update_forward_refs
|
|
13
12
|
)
|
|
14
13
|
from pydantic import BaseModel
|
|
15
|
-
from fastapi_voyager.type import Route, SchemaNode, Link, Tag,
|
|
16
|
-
from fastapi_voyager.module import build_module_tree
|
|
14
|
+
from fastapi_voyager.type import Route, SchemaNode, Link, Tag, LinkType, FieldType, PK, CoreData
|
|
17
15
|
from fastapi_voyager.filter import filter_graph
|
|
16
|
+
from fastapi_voyager.render import Renderer
|
|
17
|
+
import pydantic_resolve.constant as const
|
|
18
18
|
|
|
19
|
-
# support pydantic-resolve's ensure_subset
|
|
20
|
-
ENSURE_SUBSET_REFERENCE = '__pydantic_resolve_ensure_subset_reference__'
|
|
21
|
-
PK = "PK"
|
|
22
19
|
|
|
23
|
-
class
|
|
20
|
+
class Voyager:
|
|
24
21
|
def __init__(
|
|
25
22
|
self,
|
|
26
23
|
schema: str | None = None,
|
|
27
24
|
schema_field: str | None = None,
|
|
28
|
-
show_fields:
|
|
25
|
+
show_fields: FieldType = 'single',
|
|
29
26
|
include_tags: list[str] | None = None,
|
|
30
27
|
module_color: dict[str, str] | None = None,
|
|
31
28
|
route_name: str | None = None,
|
|
@@ -94,8 +91,8 @@ class Analytics:
|
|
|
94
91
|
route_obj = Route(
|
|
95
92
|
id=route_id,
|
|
96
93
|
name=route_name,
|
|
97
|
-
vscode_link=get_vscode_link(route.endpoint) if self.load_meta else
|
|
98
|
-
source_code=inspect.getsource(route.endpoint) if self.load_meta else
|
|
94
|
+
vscode_link=get_vscode_link(route.endpoint) if self.load_meta else '',
|
|
95
|
+
source_code=inspect.getsource(route.endpoint) if self.load_meta else ''
|
|
99
96
|
)
|
|
100
97
|
|
|
101
98
|
self.routes.append(route_obj)
|
|
@@ -106,7 +103,7 @@ class Analytics:
|
|
|
106
103
|
source_origin=tag_id,
|
|
107
104
|
target=route_id,
|
|
108
105
|
target_origin=route_id,
|
|
109
|
-
type='
|
|
106
|
+
type='tag_route'
|
|
110
107
|
))
|
|
111
108
|
|
|
112
109
|
# add response_models and create links from route -> response_model
|
|
@@ -118,7 +115,7 @@ class Analytics:
|
|
|
118
115
|
source_origin=route_id,
|
|
119
116
|
target=self.generate_node_head(target_name),
|
|
120
117
|
target_origin=target_name,
|
|
121
|
-
type='
|
|
118
|
+
type='route_to_schema'
|
|
122
119
|
))
|
|
123
120
|
|
|
124
121
|
schemas.append(schema)
|
|
@@ -143,8 +140,8 @@ class Analytics:
|
|
|
143
140
|
id=full_name,
|
|
144
141
|
module=schema.__module__,
|
|
145
142
|
name=schema.__name__,
|
|
146
|
-
source_code=get_source(schema) if self.load_meta else
|
|
147
|
-
vscode_link=get_vscode_link(schema) if self.load_meta else
|
|
143
|
+
source_code=get_source(schema) if self.load_meta else '',
|
|
144
|
+
vscode_link=get_vscode_link(schema) if self.load_meta else '',
|
|
148
145
|
fields=get_pydantic_fields(schema, bases_fields)
|
|
149
146
|
)
|
|
150
147
|
return full_name
|
|
@@ -186,7 +183,7 @@ class Analytics:
|
|
|
186
183
|
self.add_to_node_set(schema)
|
|
187
184
|
|
|
188
185
|
# handle schema inside ensure_subset(schema)
|
|
189
|
-
if subset_reference := getattr(schema,
|
|
186
|
+
if subset_reference := getattr(schema, const.ENSURE_SUBSET_REFERENCE, None):
|
|
190
187
|
if is_inheritance_of_pydantic_base(subset_reference):
|
|
191
188
|
|
|
192
189
|
self.add_to_node_set(subset_reference)
|
|
@@ -223,98 +220,34 @@ class Analytics:
|
|
|
223
220
|
source_origin=full_class_name(schema),
|
|
224
221
|
target=self.generate_node_head(full_class_name(anno)),
|
|
225
222
|
target_origin=full_class_name(anno),
|
|
226
|
-
type='
|
|
223
|
+
type='schema'):
|
|
227
224
|
self.analysis_schemas(anno)
|
|
228
225
|
|
|
229
226
|
|
|
230
227
|
def generate_node_head(self, link_name: str):
|
|
231
228
|
return f'{link_name}::{PK}'
|
|
232
229
|
|
|
230
|
+
def dump_core_data(self):
|
|
231
|
+
_tags, _routes, _nodes, _links = filter_graph(
|
|
232
|
+
schema=self.schema,
|
|
233
|
+
schema_field=self.schema_field,
|
|
234
|
+
tags=self.tags,
|
|
235
|
+
routes=self.routes,
|
|
236
|
+
nodes=self.nodes,
|
|
237
|
+
links=self.links,
|
|
238
|
+
node_set=self.node_set,
|
|
239
|
+
)
|
|
240
|
+
return CoreData(
|
|
241
|
+
tags=_tags,
|
|
242
|
+
routes=_routes,
|
|
243
|
+
nodes=_nodes,
|
|
244
|
+
links=_links,
|
|
245
|
+
show_fields=self.show_fields,
|
|
246
|
+
module_color=self.module_color,
|
|
247
|
+
schema=self.schema
|
|
248
|
+
)
|
|
233
249
|
|
|
234
|
-
def
|
|
235
|
-
has_base_fields = any(f.from_base for f in node.fields)
|
|
236
|
-
|
|
237
|
-
fields = [n for n in node.fields if n.from_base is False]
|
|
238
|
-
|
|
239
|
-
name = node.name
|
|
240
|
-
fields_parts: list[str] = []
|
|
241
|
-
|
|
242
|
-
if self.show_fields == 'all':
|
|
243
|
-
_fields = fields
|
|
244
|
-
if has_base_fields:
|
|
245
|
-
fields_parts.append('<tr><td align="left" cellpadding="8"><font color="#999"> Inherited Fields ... </font></td></tr>')
|
|
246
|
-
elif self.show_fields == 'object':
|
|
247
|
-
_fields = [f for f in fields if f.is_object is True]
|
|
248
|
-
|
|
249
|
-
else: # 'single'
|
|
250
|
-
_fields = []
|
|
251
|
-
|
|
252
|
-
for field in _fields:
|
|
253
|
-
type_name = field.type_name[:25] + '..' if len(field.type_name) > 25 else field.type_name
|
|
254
|
-
display_xml = f'<s align="left">{field.name}: {type_name}</s>' if field.is_exclude else f'{field.name}: {type_name}'
|
|
255
|
-
field_str = f"""<tr><td align="left" port="f{field.name}" cellpadding="8"><font> {display_xml} </font></td></tr>"""
|
|
256
|
-
fields_parts.append(field_str)
|
|
257
|
-
|
|
258
|
-
header_color = 'tomato' if node.id == self.schema else '#009485'
|
|
259
|
-
header = f"""<tr><td cellpadding="1.5" bgcolor="{header_color}" align="center" colspan="1" port="{PK}"> <font color="white"> {name} </font> </td> </tr>"""
|
|
260
|
-
field_content = ''.join(fields_parts) if fields_parts else ''
|
|
261
|
-
|
|
262
|
-
return f"""<<table border="1" cellborder="0" cellpadding="0" bgcolor="white"> {header} {field_content} </table>>"""
|
|
263
|
-
|
|
264
|
-
def generate_dot(self):
|
|
265
|
-
|
|
266
|
-
def generate_link(link: Link):
|
|
267
|
-
if link.type == 'internal':
|
|
268
|
-
return f'''{handle_entry(link.source)}:e -> {handle_entry(link.target)}:w [ {get_link_attributes(link)} ];'''
|
|
269
|
-
else:
|
|
270
|
-
return f'''{handle_entry(link.source)} -> {handle_entry(link.target)} [ {get_link_attributes(link)} ];'''
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
def get_link_attributes(link: Link):
|
|
274
|
-
if link.type == 'parent':
|
|
275
|
-
return 'style = "solid, dashed", dir="back", minlen=3, taillabel = "< inherit >", color = "purple", tailport="n"'
|
|
276
|
-
elif link.type == 'entry':
|
|
277
|
-
return 'style = "solid", label = "", minlen=3, tailport="e", headport="w"'
|
|
278
|
-
elif link.type == 'subset':
|
|
279
|
-
return 'style = "solid, dashed", dir="back", minlen=3, taillabel = "< subset >", color = "orange", tailport="n"'
|
|
280
|
-
|
|
281
|
-
return 'style = "solid", arrowtail="odot", dir="back", minlen=3'
|
|
282
|
-
|
|
283
|
-
def render_module(mod: ModuleNode):
|
|
284
|
-
color = self.module_color.get(mod.fullname)
|
|
285
|
-
# render schema nodes inside this module
|
|
286
|
-
inner_nodes = [
|
|
287
|
-
f'''
|
|
288
|
-
"{node.id}" [
|
|
289
|
-
label = {self.generate_node_label(node)}
|
|
290
|
-
shape = "plain"
|
|
291
|
-
margin="0.5,0.1"
|
|
292
|
-
];''' for node in mod.schema_nodes
|
|
293
|
-
]
|
|
294
|
-
inner_nodes_str = '\n'.join(inner_nodes)
|
|
295
|
-
|
|
296
|
-
# render child modules recursively
|
|
297
|
-
child_str = '\n'.join(render_module(m) for m in mod.modules)
|
|
298
|
-
|
|
299
|
-
return f'''
|
|
300
|
-
subgraph cluster_module_{mod.fullname.replace('.', '_')} {{
|
|
301
|
-
color = "#666"
|
|
302
|
-
style="rounded"
|
|
303
|
-
label = " {mod.name}"
|
|
304
|
-
labeljust = "l"
|
|
305
|
-
{(f'pencolor = "{color}"' if color else 'pencolor="#ccc"')}
|
|
306
|
-
{(f'penwidth = 3' if color else 'penwidth=""')}
|
|
307
|
-
{inner_nodes_str}
|
|
308
|
-
{child_str}
|
|
309
|
-
}}'''
|
|
310
|
-
|
|
311
|
-
def handle_entry(source: str):
|
|
312
|
-
if '::' in source:
|
|
313
|
-
a, b = source.split('::', 1)
|
|
314
|
-
return f'"{a}":{b}'
|
|
315
|
-
return f'"{source}"'
|
|
316
|
-
|
|
317
|
-
|
|
250
|
+
def render_dot(self):
|
|
318
251
|
_tags, _routes, _nodes, _links = filter_graph(
|
|
319
252
|
schema=self.schema,
|
|
320
253
|
schema_field=self.schema_field,
|
|
@@ -324,79 +257,5 @@ class Analytics:
|
|
|
324
257
|
links=self.links,
|
|
325
258
|
node_set=self.node_set,
|
|
326
259
|
)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
tags = [
|
|
330
|
-
f'''
|
|
331
|
-
"{t.id}" [
|
|
332
|
-
label = " {t.name} "
|
|
333
|
-
shape = "record"
|
|
334
|
-
margin="0.5,0.1"
|
|
335
|
-
];''' for t in _tags]
|
|
336
|
-
tag_str = '\n'.join(tags)
|
|
337
|
-
|
|
338
|
-
routes = [
|
|
339
|
-
f'''
|
|
340
|
-
"{r.id}" [
|
|
341
|
-
label = " {r.name} "
|
|
342
|
-
margin="0.5,0.1"
|
|
343
|
-
shape = "record"
|
|
344
|
-
];''' for r in _routes]
|
|
345
|
-
route_str = '\n'.join(routes)
|
|
346
|
-
|
|
347
|
-
modules_str = '\n'.join(render_module(m) for m in _modules)
|
|
348
|
-
|
|
349
|
-
links = [ generate_link(link) for link in _links ]
|
|
350
|
-
link_str = '\n'.join(links)
|
|
351
|
-
|
|
352
|
-
template = f'''
|
|
353
|
-
digraph world {{
|
|
354
|
-
pad="0.5"
|
|
355
|
-
nodesep=0.8
|
|
356
|
-
fontname="Helvetica,Arial,sans-serif"
|
|
357
|
-
node [fontname="Helvetica,Arial,sans-serif"]
|
|
358
|
-
edge [
|
|
359
|
-
fontname="Helvetica,Arial,sans-serif"
|
|
360
|
-
color="gray"
|
|
361
|
-
]
|
|
362
|
-
graph [
|
|
363
|
-
rankdir = "LR"
|
|
364
|
-
];
|
|
365
|
-
node [
|
|
366
|
-
fontsize = "16"
|
|
367
|
-
];
|
|
368
|
-
|
|
369
|
-
subgraph cluster_tags {{
|
|
370
|
-
color = "#aaa"
|
|
371
|
-
margin=18
|
|
372
|
-
style="dashed"
|
|
373
|
-
label = " Tags"
|
|
374
|
-
labeljust = "l"
|
|
375
|
-
fontsize = "20"
|
|
376
|
-
{tag_str}
|
|
377
|
-
}}
|
|
378
|
-
|
|
379
|
-
subgraph cluster_router {{
|
|
380
|
-
color = "#aaa"
|
|
381
|
-
margin=18
|
|
382
|
-
style="dashed"
|
|
383
|
-
label = " Routes"
|
|
384
|
-
labeljust = "l"
|
|
385
|
-
fontsize = "20"
|
|
386
|
-
{route_str}
|
|
387
|
-
}}
|
|
388
|
-
|
|
389
|
-
subgraph cluster_schema {{
|
|
390
|
-
color = "#aaa"
|
|
391
|
-
margin=18
|
|
392
|
-
style="dashed"
|
|
393
|
-
label=" Schema"
|
|
394
|
-
labeljust="l"
|
|
395
|
-
fontsize="20"
|
|
396
|
-
{modules_str}
|
|
397
|
-
}}
|
|
398
|
-
|
|
399
|
-
{link_str}
|
|
400
|
-
}}
|
|
401
|
-
'''
|
|
402
|
-
return template
|
|
260
|
+
renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema)
|
|
261
|
+
return renderer.render_dot(_tags, _routes, _nodes, _links)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { GraphUI } from "../graph-ui.js";
|
|
2
|
+
const { defineComponent, ref, onMounted, watch, nextTick } = window.Vue;
|
|
3
|
+
|
|
4
|
+
// Simple dialog-embeddable component that renders a DOT graph.
|
|
5
|
+
// Props:
|
|
6
|
+
// - dot: String (required) the DOT source to render
|
|
7
|
+
// Emits:
|
|
8
|
+
// - close: when the close button is clicked
|
|
9
|
+
export default defineComponent({
|
|
10
|
+
name: "RenderGraph",
|
|
11
|
+
props: {
|
|
12
|
+
dot: { type: String, required: true },
|
|
13
|
+
},
|
|
14
|
+
emits: ["close"],
|
|
15
|
+
setup(props, { emit }) {
|
|
16
|
+
const containerId = `graph-render-${Math.random().toString(36).slice(2, 9)}`;
|
|
17
|
+
const hasRendered = ref(false);
|
|
18
|
+
let graphInstance = null;
|
|
19
|
+
|
|
20
|
+
async function renderDot() {
|
|
21
|
+
if (!props.dot) return;
|
|
22
|
+
await nextTick();
|
|
23
|
+
if (!graphInstance) {
|
|
24
|
+
graphInstance = new GraphUI(`#${containerId}`);
|
|
25
|
+
}
|
|
26
|
+
await graphInstance.render(props.dot);
|
|
27
|
+
hasRendered.value = true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onMounted(async () => {
|
|
31
|
+
await renderDot();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
watch(
|
|
35
|
+
() => props.dot,
|
|
36
|
+
async () => {
|
|
37
|
+
await renderDot();
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
function close() {
|
|
42
|
+
emit("close");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { containerId, close, hasRendered };
|
|
46
|
+
},
|
|
47
|
+
template: `
|
|
48
|
+
<div style="height:100%; position:relative; background:#fff;">
|
|
49
|
+
<q-btn
|
|
50
|
+
flat dense round icon="close"
|
|
51
|
+
aria-label="Close"
|
|
52
|
+
@click="close"
|
|
53
|
+
style="position:absolute; top:6px; right:6px; z-index:11; background:rgba(255,255,255,0.85);"
|
|
54
|
+
/>
|
|
55
|
+
<div :id="containerId" style="width:100%; height:100%; overflow:auto; background:#fafafa"></div>
|
|
56
|
+
</div>
|
|
57
|
+
`,
|
|
58
|
+
});
|
|
59
|
+
|
fastapi_voyager/web/index.html
CHANGED
|
@@ -110,13 +110,23 @@
|
|
|
110
110
|
</div>
|
|
111
111
|
|
|
112
112
|
<div class="col-auto">
|
|
113
|
-
<q-btn
|
|
113
|
+
<q-btn-dropdown
|
|
114
114
|
class="q-ml-md"
|
|
115
|
+
split
|
|
115
116
|
:loading="state.generating"
|
|
116
117
|
@click="onGenerate"
|
|
117
118
|
label="Generate"
|
|
118
119
|
outline
|
|
119
|
-
|
|
120
|
+
>
|
|
121
|
+
<q-list>
|
|
122
|
+
<q-item clickable v-close-popup @click="onDumpData">
|
|
123
|
+
<q-item-section>Dump data</q-item-section>
|
|
124
|
+
</q-item>
|
|
125
|
+
<q-item clickable v-close-popup @click="openImportDialog">
|
|
126
|
+
<q-item-section>Import data</q-item-section>
|
|
127
|
+
</q-item>
|
|
128
|
+
</q-list>
|
|
129
|
+
</q-btn-dropdown>
|
|
120
130
|
</div>
|
|
121
131
|
<div class="col-auto">
|
|
122
132
|
<q-btn flat @click="onReset" label="Reset" />
|
|
@@ -204,6 +214,45 @@
|
|
|
204
214
|
@close="showRouteCode = false" />
|
|
205
215
|
</q-dialog>
|
|
206
216
|
|
|
217
|
+
<!-- Dump Core Data Dialog -->
|
|
218
|
+
<q-dialog v-model="showDumpDialog" :maximized="true" :persistent="false">
|
|
219
|
+
<div style="height:100%; position:relative; background:#fff;">
|
|
220
|
+
<q-btn
|
|
221
|
+
flat dense round icon="content_copy"
|
|
222
|
+
aria-label="Copy"
|
|
223
|
+
@click="copyDumpJson"
|
|
224
|
+
style="position:absolute; top:6px; right:62px; z-index:11; background:rgba(255,255,255,0.85);"
|
|
225
|
+
></q-btn>
|
|
226
|
+
<q-btn
|
|
227
|
+
flat dense round icon="close"
|
|
228
|
+
aria-label="Close"
|
|
229
|
+
@click="showDumpDialog = false"
|
|
230
|
+
style="position:absolute; top:6px; right:6px; z-index:11; background:rgba(255,255,255,0.85);"
|
|
231
|
+
></q-btn>
|
|
232
|
+
<div>
|
|
233
|
+
<pre style="padding:20px; overflow: auto;"><code>{{ dumpJson }}</code></pre>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</q-dialog>
|
|
237
|
+
|
|
238
|
+
<!-- Import Core Data Dialog -->
|
|
239
|
+
<q-dialog v-model="showImportDialog" :persistent="true">
|
|
240
|
+
<q-card style="min-width:70vw; max-width:90vw;">
|
|
241
|
+
<q-card-section class="text-h6">Import core data JSON</q-card-section>
|
|
242
|
+
<q-card-section >
|
|
243
|
+
<q-btn color="primary" label="Render" @click="onImportConfirm" />
|
|
244
|
+
</q-card-section>
|
|
245
|
+
<q-card-section>
|
|
246
|
+
<q-input v-model="importJsonText" type="textarea" autogrow filled label="Paste JSON here" />
|
|
247
|
+
</q-card-section>
|
|
248
|
+
</q-card>
|
|
249
|
+
</q-dialog>
|
|
250
|
+
|
|
251
|
+
<!-- Render Graph Dialog (from imported core data) -->
|
|
252
|
+
<q-dialog v-model="showRenderGraph" :maximized="true" :persistent="false">
|
|
253
|
+
<render-graph :dot="renderDotString" @close="showRenderGraph = false" />
|
|
254
|
+
</q-dialog>
|
|
255
|
+
|
|
207
256
|
<div id="graph" style="width: 100%; flex: 1 1 auto; overflow: auto"></div>
|
|
208
257
|
</div>
|
|
209
258
|
|
fastapi_voyager/web/vue-main.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import SchemaFieldFilter from "./component/schema-field-filter.js";
|
|
2
2
|
import SchemaCodeDisplay from "./component/schema-code-display.js";
|
|
3
3
|
import RouteCodeDisplay from "./component/route-code-display.js";
|
|
4
|
+
import RenderGraph from "./component/render-graph.js";
|
|
4
5
|
import { GraphUI } from "./graph-ui.js";
|
|
5
6
|
const { createApp, reactive, onMounted, watch, ref } = window.Vue;
|
|
6
7
|
|
|
@@ -25,16 +26,23 @@ const app = createApp({
|
|
|
25
26
|
rawTags: [], // [{ name, routes: [{ id, name }] }]
|
|
26
27
|
rawSchemas: [], // [{ name, fullname }]
|
|
27
28
|
rawSchemasFull: [], // full objects with source_code & fields
|
|
28
|
-
|
|
29
|
+
initializing: true,
|
|
29
30
|
});
|
|
30
31
|
const showDetail = ref(false);
|
|
31
32
|
const showSchemaFieldFilter = ref(false);
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const showSchemaCode = ref(false);
|
|
34
|
+
const showRouteCode = ref(false);
|
|
35
|
+
// Dump/Import dialogs and rendered graph dialog
|
|
36
|
+
const showDumpDialog = ref(false);
|
|
37
|
+
const dumpJson = ref("");
|
|
38
|
+
const showImportDialog = ref(false);
|
|
39
|
+
const importJsonText = ref("");
|
|
40
|
+
const showRenderGraph = ref(false);
|
|
41
|
+
const renderDotString = ref("");
|
|
34
42
|
const schemaName = ref(""); // used by detail dialog
|
|
35
43
|
const schemaFieldFilterSchema = ref(null); // external schemaName for schema-field-filter
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
const schemaCodeName = ref("");
|
|
45
|
+
const routeCodeId = ref("");
|
|
38
46
|
function openDetail() {
|
|
39
47
|
showDetail.value = true;
|
|
40
48
|
}
|
|
@@ -94,10 +102,13 @@ const app = createApp({
|
|
|
94
102
|
name: s.name,
|
|
95
103
|
fullname: s.fullname,
|
|
96
104
|
}));
|
|
97
|
-
state.routeItems = data.tags
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
state.routeItems = data.tags
|
|
106
|
+
.map((t) => t.routes)
|
|
107
|
+
.flat()
|
|
108
|
+
.reduce((acc, r) => {
|
|
109
|
+
acc[r.id] = r;
|
|
110
|
+
return acc;
|
|
111
|
+
}, {});
|
|
101
112
|
|
|
102
113
|
state.tagOptions = state.rawTags.map((t) => t.name);
|
|
103
114
|
state.schemaOptions = state.rawSchemas.map((s) => ({
|
|
@@ -161,6 +172,71 @@ const app = createApp({
|
|
|
161
172
|
}
|
|
162
173
|
}
|
|
163
174
|
|
|
175
|
+
async function onDumpData() {
|
|
176
|
+
try {
|
|
177
|
+
const payload = {
|
|
178
|
+
tags: state.tag ? [state.tag] : null,
|
|
179
|
+
schema_name: state.schemaFullname || null,
|
|
180
|
+
route_name: state.routeId || null,
|
|
181
|
+
show_fields: state.showFields,
|
|
182
|
+
};
|
|
183
|
+
const res = await fetch("/dot-core-data", {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: { "Content-Type": "application/json" },
|
|
186
|
+
body: JSON.stringify(payload),
|
|
187
|
+
});
|
|
188
|
+
const json = await res.json();
|
|
189
|
+
dumpJson.value = JSON.stringify(json, null, 2);
|
|
190
|
+
showDumpDialog.value = true;
|
|
191
|
+
} catch (e) {
|
|
192
|
+
console.error("Dump data failed", e);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function copyDumpJson() {
|
|
197
|
+
try {
|
|
198
|
+
await navigator.clipboard.writeText(dumpJson.value || "");
|
|
199
|
+
if (window.Quasar?.Notify) {
|
|
200
|
+
window.Quasar.Notify.create({ type: "positive", message: "Copied" });
|
|
201
|
+
}
|
|
202
|
+
} catch (e) {
|
|
203
|
+
console.error("Copy failed", e);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function openImportDialog() {
|
|
208
|
+
importJsonText.value = "";
|
|
209
|
+
showImportDialog.value = true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function onImportConfirm() {
|
|
213
|
+
let payloadObj = null;
|
|
214
|
+
try {
|
|
215
|
+
payloadObj = JSON.parse(importJsonText.value || "{}");
|
|
216
|
+
} catch (e) {
|
|
217
|
+
if (window.Quasar?.Notify) {
|
|
218
|
+
window.Quasar.Notify.create({
|
|
219
|
+
type: "negative",
|
|
220
|
+
message: "Invalid JSON",
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const res = await fetch("/dot-render-core-data", {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: { "Content-Type": "application/json" },
|
|
229
|
+
body: JSON.stringify(payloadObj),
|
|
230
|
+
});
|
|
231
|
+
const dotText = await res.text();
|
|
232
|
+
renderDotString.value = dotText;
|
|
233
|
+
showRenderGraph.value = true;
|
|
234
|
+
showImportDialog.value = false;
|
|
235
|
+
} catch (e) {
|
|
236
|
+
console.error("Import render failed", e);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
164
240
|
function showDialog() {
|
|
165
241
|
schemaFieldFilterSchema.value = null;
|
|
166
242
|
showSchemaFieldFilter.value = true;
|
|
@@ -203,6 +279,18 @@ const app = createApp({
|
|
|
203
279
|
showRouteCode,
|
|
204
280
|
schemaCodeName,
|
|
205
281
|
routeCodeId,
|
|
282
|
+
// dump/import
|
|
283
|
+
showDumpDialog,
|
|
284
|
+
dumpJson,
|
|
285
|
+
copyDumpJson,
|
|
286
|
+
onDumpData,
|
|
287
|
+
showImportDialog,
|
|
288
|
+
importJsonText,
|
|
289
|
+
openImportDialog,
|
|
290
|
+
onImportConfirm,
|
|
291
|
+
// render graph dialog
|
|
292
|
+
showRenderGraph,
|
|
293
|
+
renderDotString,
|
|
206
294
|
};
|
|
207
295
|
},
|
|
208
296
|
});
|
|
@@ -210,4 +298,5 @@ app.use(window.Quasar);
|
|
|
210
298
|
app.component("schema-field-filter", SchemaFieldFilter);
|
|
211
299
|
app.component("schema-code-display", SchemaCodeDisplay);
|
|
212
300
|
app.component("route-code-display", RouteCodeDisplay);
|
|
301
|
+
app.component("render-graph", RenderGraph);
|
|
213
302
|
app.mount("#q-app");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-voyager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -162,11 +162,14 @@ features
|
|
|
162
162
|
- [x] alt+click to show field details
|
|
163
163
|
- [x] display source code of routes (including response_model)
|
|
164
164
|
- [x] handle excluded field
|
|
165
|
-
- [ ]
|
|
165
|
+
- [ ] add tooltips
|
|
166
|
+
- [ ] route
|
|
167
|
+
- [ ] group routes by module hierarchy
|
|
168
|
+
- [ ] add response_model in route
|
|
166
169
|
- [ ] support dataclass
|
|
170
|
+
- [ ] click field to highlight links
|
|
167
171
|
- [ ] user can generate nodes/edges manually and connect to generated ones
|
|
168
|
-
- [ ]
|
|
169
|
-
- [ ] abstract render module
|
|
172
|
+
- [ ] add owner
|
|
170
173
|
- [ ] ui optimization
|
|
171
174
|
- [ ] fixed left/right bar show field information
|
|
172
175
|
- [ ] display standard ER diagram
|
|
@@ -176,6 +179,10 @@ features
|
|
|
176
179
|
- [x] strikethrough for excluded fields
|
|
177
180
|
- [ ] display loader as edges
|
|
178
181
|
- [ ] add tests
|
|
182
|
+
- [ ] refactor
|
|
183
|
+
- [ ] abstract render module
|
|
184
|
+
- [ ] export voyager core data into json (for better debugging)
|
|
185
|
+
- [ ] add api to rebuild core data from json, and render it
|
|
179
186
|
|
|
180
187
|
bugs:
|
|
181
188
|
- [ ] fix duplicated link from class and parent class, it also break clicking highlight
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
fastapi_voyager/__init__.py,sha256=E5WTV_sYs2LK8I6jzA7AuvFU5a8_vjnDseC3DMha0iQ,149
|
|
2
|
-
fastapi_voyager/cli.py,sha256=
|
|
2
|
+
fastapi_voyager/cli.py,sha256=scBD2ojOq0_sJ8Usu79MoGUvkEc_zlnBO6xK0grz7dI,10478
|
|
3
3
|
fastapi_voyager/filter.py,sha256=uZrVVMhHG5E7j1wdsiB02RAyoDdF1q8A4J04oCboYAU,4644
|
|
4
|
-
fastapi_voyager/graph.py,sha256=0lTU2XiIer35IPXoQBuh2KajvXUGeea3-vuHyPnketA,14734
|
|
5
4
|
fastapi_voyager/module.py,sha256=NyhSpjevhYtIJdlJxpksGAzfbOV5bHZ7PWbVT4x8LIU,2664
|
|
6
|
-
fastapi_voyager/
|
|
7
|
-
fastapi_voyager/
|
|
5
|
+
fastapi_voyager/render.py,sha256=gK0ur8CAqI3jNt2H3mr-JSpC0sD1JxzLRlFcuykBkxQ,6366
|
|
6
|
+
fastapi_voyager/server.py,sha256=Eer6GdD9-EG9QGHlYmClF3MeVxw68amjK4iW_YZ5ShI,3751
|
|
7
|
+
fastapi_voyager/type.py,sha256=Tef61aKaQdTinM8N_lpmrse8elXV8DAgY3ULySc8TEE,1642
|
|
8
8
|
fastapi_voyager/type_helper.py,sha256=j7AiFXsfl4kaxshYtofbsqo08dIXiHvJ190soIzUdLk,8380
|
|
9
|
-
fastapi_voyager/version.py,sha256=
|
|
9
|
+
fastapi_voyager/version.py,sha256=KuUsLbsba6ZKn4IW2EH9junXCihYukG7hMgo1ojf5as,48
|
|
10
|
+
fastapi_voyager/voyager.py,sha256=xreK0tuwx1WBme4e9-YX7j92oRPpaAdY9Wh4MHu0Yhk,9626
|
|
10
11
|
fastapi_voyager/web/graph-ui.js,sha256=eEjDnJVMvk35LdRoxcqX_fZxLFS9_bUrGAZL6K2O5C0,4176
|
|
11
12
|
fastapi_voyager/web/graphviz.svg.css,sha256=zDCjjpT0Idufu5YOiZI76PL70-avP3vTyzGPh9M85Do,1563
|
|
12
13
|
fastapi_voyager/web/graphviz.svg.js,sha256=lvAdbjHc-lMSk4GQp-iqYA2PCFX4RKnW7dFaoe0LUHs,16005
|
|
13
|
-
fastapi_voyager/web/index.html,sha256=
|
|
14
|
+
fastapi_voyager/web/index.html,sha256=zIihJ0S-adznFDbtoFa0cbXuP6viqZ0UtgsZWH8YSRY,10456
|
|
14
15
|
fastapi_voyager/web/quasar.min.css,sha256=F5jQe7X2XT54VlvAaa2V3GsBFdVD-vxDZeaPLf6U9CU,203145
|
|
15
16
|
fastapi_voyager/web/quasar.min.js,sha256=h0ftyPMW_CRiyzeVfQqiup0vrVt4_QWojpqmpnpn07E,502974
|
|
16
|
-
fastapi_voyager/web/vue-main.js,sha256=
|
|
17
|
+
fastapi_voyager/web/vue-main.js,sha256=k_3SX5VHtJhtXzXEqFsB0maSJH0ljFSx7i-X4Vq40AY,9012
|
|
18
|
+
fastapi_voyager/web/component/render-graph.js,sha256=5BdxBOWsSxWUR5tha5zEQh8eojBZjQ_vGawZFC9WNa8,1450
|
|
17
19
|
fastapi_voyager/web/component/route-code-display.js,sha256=NECC1OGcPCdDfbghtRJEnmFM6HmH5J3win2ibapWPeA,2649
|
|
18
20
|
fastapi_voyager/web/component/schema-code-display.js,sha256=oOusgTvCaWGnoKb-NBwu0SXqJJf2PTUtp3lUczokTBM,5515
|
|
19
21
|
fastapi_voyager/web/component/schema-field-filter.js,sha256=9WBjO6JJl2yf6OiiXoddMgvL32qTDu0PM-RxkkJ7t5M,6267
|
|
@@ -24,8 +26,8 @@ fastapi_voyager/web/icon/favicon-16x16.png,sha256=JC07jEzfIYxBIoQn_FHXvyHuxESdhW
|
|
|
24
26
|
fastapi_voyager/web/icon/favicon-32x32.png,sha256=C7v1h58cfWOsiLp9yOIZtlx-dLasBcq3NqpHVGRmpt4,1859
|
|
25
27
|
fastapi_voyager/web/icon/favicon.ico,sha256=tZolYIXkkBcFiYl1A8ksaXN2VjGamzcSdes838dLvNc,15406
|
|
26
28
|
fastapi_voyager/web/icon/site.webmanifest,sha256=ep4Hzh9zhmiZF2At3Fp1dQrYQuYF_3ZPZxc1KcGBvwQ,263
|
|
27
|
-
fastapi_voyager-0.
|
|
28
|
-
fastapi_voyager-0.
|
|
29
|
-
fastapi_voyager-0.
|
|
30
|
-
fastapi_voyager-0.
|
|
31
|
-
fastapi_voyager-0.
|
|
29
|
+
fastapi_voyager-0.5.1.dist-info/METADATA,sha256=KMBZwza3Z2hhpS1HK8WxoVBntmYEkmP0EAofWiYa2x0,6766
|
|
30
|
+
fastapi_voyager-0.5.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
31
|
+
fastapi_voyager-0.5.1.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
|
|
32
|
+
fastapi_voyager-0.5.1.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
|
|
33
|
+
fastapi_voyager-0.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|