fastapi-voyager 0.8.2__py3-none-any.whl → 0.9.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/__init__.py +4 -0
- fastapi_voyager/cli.py +1 -1
- fastapi_voyager/server.py +89 -21
- fastapi_voyager/type.py +0 -4
- fastapi_voyager/version.py +1 -1
- fastapi_voyager/voyager.py +1 -7
- fastapi_voyager/web/component/route-code-display.js +88 -26
- fastapi_voyager/web/component/schema-code-display.js +50 -18
- fastapi_voyager/web/component/schema-field-filter.js +7 -7
- fastapi_voyager/web/graph-ui.js +2 -2
- fastapi_voyager/web/index.html +102 -53
- fastapi_voyager/web/vue-main.js +51 -63
- {fastapi_voyager-0.8.2.dist-info → fastapi_voyager-0.9.1.dist-info}/METADATA +58 -24
- {fastapi_voyager-0.8.2.dist-info → fastapi_voyager-0.9.1.dist-info}/RECORD +17 -17
- {fastapi_voyager-0.8.2.dist-info → fastapi_voyager-0.9.1.dist-info}/WHEEL +0 -0
- {fastapi_voyager-0.8.2.dist-info → fastapi_voyager-0.9.1.dist-info}/entry_points.txt +0 -0
- {fastapi_voyager-0.8.2.dist-info → fastapi_voyager-0.9.1.dist-info}/licenses/LICENSE +0 -0
fastapi_voyager/__init__.py
CHANGED
fastapi_voyager/cli.py
CHANGED
|
@@ -281,7 +281,7 @@ Examples:
|
|
|
281
281
|
except ImportError:
|
|
282
282
|
print("Error: uvicorn is required to run the server. Install via 'pip install uvicorn' or 'uv add uvicorn'.")
|
|
283
283
|
sys.exit(1)
|
|
284
|
-
app_server = viz_server.
|
|
284
|
+
app_server = viz_server.create_voyager(
|
|
285
285
|
app,
|
|
286
286
|
module_color=module_color,
|
|
287
287
|
module_prefix=args.module_prefix,
|
fastapi_voyager/server.py
CHANGED
|
@@ -6,23 +6,18 @@ from pydantic import BaseModel
|
|
|
6
6
|
from fastapi.responses import HTMLResponse, PlainTextResponse, JSONResponse
|
|
7
7
|
from fastapi.staticfiles import StaticFiles
|
|
8
8
|
from fastapi_voyager.voyager import Voyager
|
|
9
|
-
from fastapi_voyager.type import Tag, FieldInfo, CoreData
|
|
9
|
+
from fastapi_voyager.type import Tag, FieldInfo, CoreData, SchemaNode
|
|
10
10
|
from fastapi_voyager.render import Renderer
|
|
11
|
+
from fastapi_voyager.type_helper import get_source, get_vscode_link
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
WEB_DIR = Path(__file__).parent / "web"
|
|
14
15
|
WEB_DIR.mkdir(exist_ok=True)
|
|
15
16
|
|
|
16
|
-
class SchemaType(BaseModel):
|
|
17
|
-
name: str
|
|
18
|
-
fullname: str
|
|
19
|
-
source_code: str
|
|
20
|
-
vscode_link: str
|
|
21
|
-
fields: list[FieldInfo]
|
|
22
17
|
|
|
23
18
|
class OptionParam(BaseModel):
|
|
24
19
|
tags: list[Tag]
|
|
25
|
-
schemas: list[
|
|
20
|
+
schemas: list[SchemaNode]
|
|
26
21
|
dot: str
|
|
27
22
|
|
|
28
23
|
class Payload(BaseModel):
|
|
@@ -40,26 +35,22 @@ def create_route(
|
|
|
40
35
|
module_color: dict[str, str] | None = None,
|
|
41
36
|
module_prefix: Optional[str] = None,
|
|
42
37
|
):
|
|
38
|
+
"""
|
|
39
|
+
module_color: dict mapping module name to color string, e.g. {'models': 'lightblue'}
|
|
40
|
+
module_prefix: prefix string to define schemas show in brief mode
|
|
41
|
+
"""
|
|
43
42
|
router = APIRouter(tags=['fastapi-voyager'])
|
|
44
43
|
|
|
45
44
|
@router.get("/dot", response_model=OptionParam)
|
|
46
45
|
def get_dot() -> str:
|
|
47
|
-
voyager = Voyager(module_color=module_color
|
|
46
|
+
voyager = Voyager(module_color=module_color)
|
|
48
47
|
voyager.analysis(target_app)
|
|
49
48
|
dot = voyager.render_dot()
|
|
50
49
|
|
|
51
50
|
# include tags and their routes
|
|
52
51
|
tags = voyager.tags
|
|
53
52
|
|
|
54
|
-
schemas = [
|
|
55
|
-
SchemaType(
|
|
56
|
-
name=s.name,
|
|
57
|
-
fullname=s.id,
|
|
58
|
-
fields=s.fields,
|
|
59
|
-
source_code=s.source_code,
|
|
60
|
-
vscode_link=s.vscode_link
|
|
61
|
-
) for s in voyager.nodes
|
|
62
|
-
]
|
|
53
|
+
schemas = voyager.nodes[:]
|
|
63
54
|
schemas.sort(key=lambda s: s.name)
|
|
64
55
|
|
|
65
56
|
return OptionParam(tags=tags, schemas=schemas, dot=dot)
|
|
@@ -74,7 +65,6 @@ def create_route(
|
|
|
74
65
|
show_fields=payload.show_fields,
|
|
75
66
|
module_color=module_color,
|
|
76
67
|
route_name=payload.route_name,
|
|
77
|
-
load_meta=False,
|
|
78
68
|
hide_primitive_route=payload.hide_primitive_route,
|
|
79
69
|
)
|
|
80
70
|
voyager.analysis(target_app)
|
|
@@ -92,7 +82,6 @@ def create_route(
|
|
|
92
82
|
show_fields=payload.show_fields,
|
|
93
83
|
module_color=module_color,
|
|
94
84
|
route_name=payload.route_name,
|
|
95
|
-
load_meta=False,
|
|
96
85
|
)
|
|
97
86
|
voyager.analysis(target_app)
|
|
98
87
|
return voyager.dump_core_data()
|
|
@@ -117,11 +106,90 @@ def create_route(
|
|
|
117
106
|
</body>
|
|
118
107
|
</html>
|
|
119
108
|
"""
|
|
109
|
+
|
|
110
|
+
class SourcePayload(BaseModel):
|
|
111
|
+
schema_name: str
|
|
120
112
|
|
|
113
|
+
@router.post("/source")
|
|
114
|
+
def get_object_by_module_name(payload: SourcePayload):
|
|
115
|
+
"""
|
|
116
|
+
input: __module__ + __name__, eg: tests.demo.PageStories
|
|
117
|
+
output: source code of the object
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
components = payload.schema_name.split('.')
|
|
121
|
+
if len(components) < 2:
|
|
122
|
+
return JSONResponse(
|
|
123
|
+
status_code=400,
|
|
124
|
+
content={"error": "Invalid schema name format. Expected format: module.ClassName"}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
module_name = '.'.join(components[:-1])
|
|
128
|
+
class_name = components[-1]
|
|
129
|
+
|
|
130
|
+
mod = __import__(module_name, fromlist=[class_name])
|
|
131
|
+
obj = getattr(mod, class_name)
|
|
132
|
+
source_code = get_source(obj)
|
|
133
|
+
|
|
134
|
+
return JSONResponse(content={"source_code": source_code})
|
|
135
|
+
except ImportError as e:
|
|
136
|
+
return JSONResponse(
|
|
137
|
+
status_code=404,
|
|
138
|
+
content={"error": f"Module not found: {e}"}
|
|
139
|
+
)
|
|
140
|
+
except AttributeError as e:
|
|
141
|
+
return JSONResponse(
|
|
142
|
+
status_code=404,
|
|
143
|
+
content={"error": f"Class not found: {e}"}
|
|
144
|
+
)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
return JSONResponse(
|
|
147
|
+
status_code=500,
|
|
148
|
+
content={"error": f"Internal error: {str(e)}"}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@router.post("/vscode-link")
|
|
152
|
+
def get_vscode_link_by_module_name(payload: SourcePayload):
|
|
153
|
+
"""
|
|
154
|
+
input: __module__ + __name__, eg: tests.demo.PageStories
|
|
155
|
+
output: source path of the object
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
components = payload.schema_name.split('.')
|
|
159
|
+
if len(components) < 2:
|
|
160
|
+
return JSONResponse(
|
|
161
|
+
status_code=400,
|
|
162
|
+
content={"error": "Invalid schema name format. Expected format: module.ClassName"}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
module_name = '.'.join(components[:-1])
|
|
166
|
+
class_name = components[-1]
|
|
167
|
+
|
|
168
|
+
mod = __import__(module_name, fromlist=[class_name])
|
|
169
|
+
obj = getattr(mod, class_name)
|
|
170
|
+
link = get_vscode_link(obj)
|
|
171
|
+
|
|
172
|
+
return JSONResponse(content={"link": link})
|
|
173
|
+
except ImportError as e:
|
|
174
|
+
return JSONResponse(
|
|
175
|
+
status_code=404,
|
|
176
|
+
content={"error": f"Module not found: {e}"}
|
|
177
|
+
)
|
|
178
|
+
except AttributeError as e:
|
|
179
|
+
return JSONResponse(
|
|
180
|
+
status_code=404,
|
|
181
|
+
content={"error": f"Class not found: {e}"}
|
|
182
|
+
)
|
|
183
|
+
except Exception as e:
|
|
184
|
+
return JSONResponse(
|
|
185
|
+
status_code=500,
|
|
186
|
+
content={"error": f"Internal error: {str(e)}"}
|
|
187
|
+
)
|
|
188
|
+
|
|
121
189
|
return router
|
|
122
190
|
|
|
123
191
|
|
|
124
|
-
def
|
|
192
|
+
def create_voyager(
|
|
125
193
|
target_app: FastAPI,
|
|
126
194
|
module_color: dict[str, str] | None = None,
|
|
127
195
|
gzip_minimum_size: int | None = 500,
|
fastapi_voyager/type.py
CHANGED
|
@@ -22,8 +22,6 @@ class Tag(NodeBase):
|
|
|
22
22
|
@dataclass
|
|
23
23
|
class Route(NodeBase):
|
|
24
24
|
module: str
|
|
25
|
-
source_code: str = ''
|
|
26
|
-
vscode_link: str = '' # optional vscode deep link
|
|
27
25
|
response_schema: str = ''
|
|
28
26
|
is_primitive: bool = True
|
|
29
27
|
|
|
@@ -37,8 +35,6 @@ class ModuleRoute:
|
|
|
37
35
|
@dataclass
|
|
38
36
|
class SchemaNode(NodeBase):
|
|
39
37
|
module: str
|
|
40
|
-
source_code: str = '' # optional for tests / backward compatibility
|
|
41
|
-
vscode_link: str = '' # optional vscode deep link
|
|
42
38
|
fields: list[FieldInfo] = field(default_factory=list)
|
|
43
39
|
|
|
44
40
|
@dataclass
|
fastapi_voyager/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.9.1"
|
fastapi_voyager/voyager.py
CHANGED
|
@@ -29,7 +29,6 @@ class Voyager:
|
|
|
29
29
|
module_color: dict[str, str] | None = None,
|
|
30
30
|
route_name: str | None = None,
|
|
31
31
|
hide_primitive_route: bool = False,
|
|
32
|
-
load_meta: bool = False
|
|
33
32
|
):
|
|
34
33
|
|
|
35
34
|
self.routes: list[Route] = []
|
|
@@ -50,7 +49,6 @@ class Voyager:
|
|
|
50
49
|
self.show_fields = show_fields if show_fields in ('single','object','all') else 'object'
|
|
51
50
|
self.module_color = module_color or {}
|
|
52
51
|
self.route_name = route_name
|
|
53
|
-
self.load_meta = load_meta
|
|
54
52
|
self.hide_primitive_route = hide_primitive_route
|
|
55
53
|
|
|
56
54
|
|
|
@@ -85,7 +83,7 @@ class Voyager:
|
|
|
85
83
|
self.tags.append(tag_obj)
|
|
86
84
|
|
|
87
85
|
# add route and create links
|
|
88
|
-
route_id =
|
|
86
|
+
route_id = full_class_name(route.endpoint)
|
|
89
87
|
route_name = route.endpoint.__name__
|
|
90
88
|
route_module = route.endpoint.__module__
|
|
91
89
|
|
|
@@ -110,8 +108,6 @@ class Voyager:
|
|
|
110
108
|
id=route_id,
|
|
111
109
|
name=route_name,
|
|
112
110
|
module=route_module,
|
|
113
|
-
vscode_link=get_vscode_link(route.endpoint) if self.load_meta else '',
|
|
114
|
-
source_code=inspect.getsource(route.endpoint) if self.load_meta else '',
|
|
115
111
|
response_schema=get_type_name(route.response_model),
|
|
116
112
|
is_primitive=is_primitive_response
|
|
117
113
|
)
|
|
@@ -154,8 +150,6 @@ class Voyager:
|
|
|
154
150
|
id=full_name,
|
|
155
151
|
module=schema.__module__,
|
|
156
152
|
name=schema.__name__,
|
|
157
|
-
source_code=get_source(schema) if self.load_meta else '',
|
|
158
|
-
vscode_link=get_vscode_link(schema) if self.load_meta else '',
|
|
159
153
|
fields=get_pydantic_fields(schema, bases_fields)
|
|
160
154
|
)
|
|
161
155
|
return full_name
|
|
@@ -6,58 +6,119 @@ const { defineComponent, ref, watch, onMounted } = window.Vue;
|
|
|
6
6
|
// modelValue: dialog visibility
|
|
7
7
|
// routes: object map { id: { id, name, source_code } }
|
|
8
8
|
export default defineComponent({
|
|
9
|
-
name:
|
|
9
|
+
name: "RouteCodeDisplay",
|
|
10
10
|
props: {
|
|
11
11
|
routeId: { type: String, required: true },
|
|
12
12
|
modelValue: { type: Boolean, default: false },
|
|
13
13
|
routes: { type: Object, default: () => ({}) },
|
|
14
14
|
},
|
|
15
|
-
emits: [
|
|
15
|
+
emits: ["close"],
|
|
16
16
|
setup(props, { emit }) {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
17
|
+
const loading = ref(false);
|
|
18
|
+
const code = ref("");
|
|
19
|
+
const error = ref("");
|
|
20
|
+
const link = ref("");
|
|
20
21
|
|
|
21
|
-
function close() {
|
|
22
|
+
function close() {
|
|
23
|
+
emit("close");
|
|
24
|
+
}
|
|
22
25
|
|
|
23
26
|
function highlightLater() {
|
|
24
27
|
requestAnimationFrame(() => {
|
|
25
28
|
try {
|
|
26
29
|
if (window.hljs) {
|
|
27
|
-
const block = document.querySelector(
|
|
30
|
+
const block = document.querySelector(
|
|
31
|
+
".frv-route-code-display pre code.language-python"
|
|
32
|
+
);
|
|
28
33
|
if (block) {
|
|
29
34
|
window.hljs.highlightElement(block);
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
} catch (e) {
|
|
33
|
-
console.warn(
|
|
38
|
+
console.warn("highlight failed", e);
|
|
34
39
|
}
|
|
35
40
|
});
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
function load() {
|
|
43
|
+
async function load() {
|
|
44
|
+
if (!props.routeId) {
|
|
45
|
+
code.value = "";
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
loading.value = true;
|
|
39
50
|
error.value = null;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
code.value = "";
|
|
52
|
+
link.value = "";
|
|
53
|
+
|
|
54
|
+
// try to fetch from server: POST /source with { schema_name: routeId }
|
|
55
|
+
const payload = { schema_name: props.routeId };
|
|
56
|
+
try {
|
|
57
|
+
const resp = await fetch(`source`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
Accept: "application/json",
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify(payload),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const data = await resp.json().catch(() => ({}));
|
|
67
|
+
if (resp.ok) {
|
|
68
|
+
code.value = data.source_code || "// no source code available";
|
|
69
|
+
} else {
|
|
70
|
+
error.value = (data && data.error) || "Failed to load source";
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
error.value = e && e.message ? e.message : "Failed to load source";
|
|
74
|
+
} finally {
|
|
75
|
+
loading.value = false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const resp = await fetch(`vscode-link`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: {
|
|
82
|
+
Accept: "application/json",
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify(payload),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const data = await resp.json().catch(() => ({}));
|
|
89
|
+
if (resp.ok) {
|
|
90
|
+
link.value = data.link || "// no source code available";
|
|
91
|
+
} else {
|
|
92
|
+
error.value += (data && data.error) || "Failed to load vscode link";
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
} finally {
|
|
96
|
+
loading.value = false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!error.value) {
|
|
45
100
|
highlightLater();
|
|
46
|
-
} else if (item) {
|
|
47
|
-
code.value = '// no source code available';
|
|
48
|
-
link.value = item.vscode_link || '';
|
|
49
|
-
} else {
|
|
50
|
-
error.value = 'Route not found';
|
|
51
|
-
link.value = '';
|
|
52
101
|
}
|
|
53
102
|
}
|
|
54
103
|
|
|
55
|
-
watch(
|
|
56
|
-
|
|
104
|
+
watch(
|
|
105
|
+
() => props.modelValue,
|
|
106
|
+
(v) => {
|
|
107
|
+
if (v) load();
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
watch(
|
|
111
|
+
() => props.routeId,
|
|
112
|
+
() => {
|
|
113
|
+
if (props.modelValue) load();
|
|
114
|
+
}
|
|
115
|
+
);
|
|
57
116
|
|
|
58
|
-
onMounted(() => {
|
|
117
|
+
onMounted(() => {
|
|
118
|
+
if (props.modelValue) load();
|
|
119
|
+
});
|
|
59
120
|
|
|
60
|
-
return { code, error, close, link };
|
|
121
|
+
return { loading, code, error, close, link };
|
|
61
122
|
},
|
|
62
123
|
template: `
|
|
63
124
|
<div class="frv-route-code-display" style="border:1px solid #ccc; position:relative; width:50vw; max-width:50vw; height:100%; background:#fff;">
|
|
@@ -66,8 +127,9 @@ export default defineComponent({
|
|
|
66
127
|
<a :href="link" target="_blank" rel="noopener" style="font-size:12px; color:#3b82f6;">Open in VSCode</a>
|
|
67
128
|
</div>
|
|
68
129
|
<div style="padding:40px 16px 16px 16px; height:100%; box-sizing:border-box; overflow:auto;">
|
|
69
|
-
<div v-if="
|
|
130
|
+
<div v-if="loading" style="font-family:Menlo, monospace; font-size:12px;">Loading source...</div>
|
|
131
|
+
<div v-else-if="error" style="color:#c10015; font-family:Menlo, monospace; font-size:12px;">{{ error }}</div>
|
|
70
132
|
<pre v-else style="margin:0;"><code class="language-python">{{ code }}</code></pre>
|
|
71
133
|
</div>
|
|
72
|
-
</div
|
|
134
|
+
</div>`,
|
|
73
135
|
});
|
|
@@ -14,7 +14,6 @@ export default defineComponent({
|
|
|
14
14
|
props: {
|
|
15
15
|
schemaName: { type: String, required: true },
|
|
16
16
|
modelValue: { type: Boolean, default: false },
|
|
17
|
-
source: { type: String, default: null },
|
|
18
17
|
schemas: { type: Array, default: () => [] },
|
|
19
18
|
},
|
|
20
19
|
emits: ["close"],
|
|
@@ -22,9 +21,9 @@ export default defineComponent({
|
|
|
22
21
|
const loading = ref(false);
|
|
23
22
|
const code = ref("");
|
|
24
23
|
const link = ref("");
|
|
25
|
-
const error = ref(
|
|
24
|
+
const error = ref("");
|
|
26
25
|
const fields = ref([]); // schema fields list
|
|
27
|
-
const tab = ref(
|
|
26
|
+
const tab = ref("source");
|
|
28
27
|
|
|
29
28
|
function close() {
|
|
30
29
|
emit("close");
|
|
@@ -48,38 +47,71 @@ export default defineComponent({
|
|
|
48
47
|
});
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
function loadSource() {
|
|
50
|
+
async function loadSource() {
|
|
52
51
|
if (!props.schemaName) return;
|
|
53
|
-
|
|
54
|
-
code.value = props.source;
|
|
55
|
-
highlightLater();
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
52
|
+
|
|
58
53
|
loading.value = true;
|
|
59
54
|
error.value = null;
|
|
55
|
+
code.value = "";
|
|
56
|
+
link.value = "";
|
|
57
|
+
|
|
58
|
+
// try to fetch from server: /source/{schema_name}
|
|
59
|
+
const payload = { schema_name: props.schemaName };
|
|
60
60
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
// validate input: ensure we have a non-empty schemaName
|
|
62
|
+
const resp = await fetch(`source`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
Accept: "application/json",
|
|
66
|
+
"Content-Type": "application/json",
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify(payload),
|
|
69
|
+
});
|
|
70
|
+
// surface server-side validation message for bad request
|
|
71
|
+
const data = await resp.json().catch(() => ({}));
|
|
72
|
+
if (resp.ok) {
|
|
73
|
+
code.value = data.source_code || "// no source code available";
|
|
68
74
|
} else {
|
|
69
|
-
error.value = "
|
|
75
|
+
error.value = (data && data.error) || "Failed to load source";
|
|
70
76
|
}
|
|
71
77
|
} catch (e) {
|
|
72
78
|
error.value = "Failed to load source";
|
|
73
79
|
} finally {
|
|
74
80
|
loading.value = false;
|
|
75
81
|
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const resp = await fetch(`vscode-link`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: {
|
|
87
|
+
Accept: "application/json",
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify(payload),
|
|
91
|
+
});
|
|
92
|
+
// surface server-side validation message for bad request
|
|
93
|
+
const data = await resp.json().catch(() => ({}));
|
|
94
|
+
if (resp.ok) {
|
|
95
|
+
link.value = data.link || "// no vscode link available";
|
|
96
|
+
} else {
|
|
97
|
+
error.value += (data && data.error) || "Failed to load source";
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
error.value = "Failed to load source";
|
|
101
|
+
} finally {
|
|
102
|
+
loading.value = false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!error.value && tab.value === "source") {
|
|
106
|
+
highlightLater();
|
|
107
|
+
}
|
|
76
108
|
}
|
|
77
109
|
|
|
78
110
|
// re-highlight when switching back to source tab
|
|
79
111
|
watch(
|
|
80
112
|
() => tab.value,
|
|
81
113
|
(val) => {
|
|
82
|
-
if (val ===
|
|
114
|
+
if (val === "source") {
|
|
83
115
|
highlightLater();
|
|
84
116
|
}
|
|
85
117
|
}
|
|
@@ -43,8 +43,8 @@ export default defineComponent({
|
|
|
43
43
|
state.error = null;
|
|
44
44
|
state.schemas = Array.isArray(props.schemas) ? props.schemas : [];
|
|
45
45
|
state.schemaOptions = state.schemas.map((s) => ({
|
|
46
|
-
label: `${s.name} (${s.
|
|
47
|
-
value: s.
|
|
46
|
+
label: `${s.name} (${s.id})`,
|
|
47
|
+
value: s.id,
|
|
48
48
|
}));
|
|
49
49
|
// Maintain compatibility: loadingSchemas flag toggled quickly (no async work)
|
|
50
50
|
state.loadingSchemas = false;
|
|
@@ -54,8 +54,8 @@ export default defineComponent({
|
|
|
54
54
|
const needle = (val || "").toLowerCase();
|
|
55
55
|
update(() => {
|
|
56
56
|
let opts = state.schemas.map((s) => ({
|
|
57
|
-
label: `${s.name} (${s.
|
|
58
|
-
value: s.
|
|
57
|
+
label: `${s.name} (${s.id})`,
|
|
58
|
+
value: s.id,
|
|
59
59
|
}));
|
|
60
60
|
if (needle) {
|
|
61
61
|
opts = opts.filter((o) => o.label.toLowerCase().includes(needle));
|
|
@@ -67,7 +67,7 @@ export default defineComponent({
|
|
|
67
67
|
function onSchemaChange(val) {
|
|
68
68
|
state.schemaFullname = val;
|
|
69
69
|
state.fieldName = null;
|
|
70
|
-
const schema = state.schemas.find((s) => s.
|
|
70
|
+
const schema = state.schemas.find((s) => s.id === val);
|
|
71
71
|
state.fieldOptions = schema ? schema.fields.map((f) => f.name) : [];
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -106,9 +106,9 @@ export default defineComponent({
|
|
|
106
106
|
function applyExternalSchema(name) {
|
|
107
107
|
if (!name || !state.schemas.length) return;
|
|
108
108
|
if (lastAppliedExternal === name) return; // avoid duplicate
|
|
109
|
-
const schema = state.schemas.find((s) => s.
|
|
109
|
+
const schema = state.schemas.find((s) => s.id === name);
|
|
110
110
|
if (!schema) return;
|
|
111
|
-
state.schemaFullname = schema.
|
|
111
|
+
state.schemaFullname = schema.id;
|
|
112
112
|
state.fieldOptions = schema.fields.map((f) => f.name);
|
|
113
113
|
state.fieldName = null; // reset field for external injection
|
|
114
114
|
lastAppliedExternal = name;
|
fastapi_voyager/web/graph-ui.js
CHANGED
|
@@ -110,7 +110,7 @@ export class GraphUI {
|
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
async render(dotSrc) {
|
|
113
|
+
async render(dotSrc, resetZoom = true) {
|
|
114
114
|
const height = this.options.height || "100%";
|
|
115
115
|
return new Promise((resolve, reject) => {
|
|
116
116
|
try {
|
|
@@ -126,7 +126,7 @@ export class GraphUI {
|
|
|
126
126
|
.renderDot(dotSrc)
|
|
127
127
|
.on("end", () => {
|
|
128
128
|
$(this.selector).data("graphviz.svg").setup();
|
|
129
|
-
this.graphviz.resetZoom();
|
|
129
|
+
if (resetZoom) this.graphviz.resetZoom();
|
|
130
130
|
resolve();
|
|
131
131
|
});
|
|
132
132
|
} catch (err) {
|
fastapi_voyager/web/index.html
CHANGED
|
@@ -89,13 +89,20 @@
|
|
|
89
89
|
>
|
|
90
90
|
<div class="row items-center q-col-gutter-md">
|
|
91
91
|
<div class="col-auto">
|
|
92
|
-
<div
|
|
92
|
+
<div
|
|
93
|
+
style="width: 282px; margin-left: 8px"
|
|
94
|
+
class="text-h6 text-primary"
|
|
95
|
+
>
|
|
96
|
+
<q-icon class="q-mr-sm" name="satellite_alt"></q-icon>
|
|
97
|
+
<span> FastAPI Voyager </span>
|
|
98
|
+
</div>
|
|
93
99
|
</div>
|
|
94
100
|
<div class="col-auto">
|
|
95
101
|
<div class="column">
|
|
96
102
|
<q-option-group
|
|
97
103
|
v-model="state.showFields"
|
|
98
104
|
:options="state.fieldOptions"
|
|
105
|
+
@update:model-value="(val) => toggleShowField(val)"
|
|
99
106
|
color="primary"
|
|
100
107
|
type="radio"
|
|
101
108
|
inline
|
|
@@ -104,7 +111,6 @@
|
|
|
104
111
|
</div>
|
|
105
112
|
</div>
|
|
106
113
|
|
|
107
|
-
|
|
108
114
|
<!-- <div class="col-auto">
|
|
109
115
|
<q-btn-dropdown
|
|
110
116
|
class="q-ml-md"
|
|
@@ -126,11 +132,31 @@
|
|
|
126
132
|
</div> -->
|
|
127
133
|
|
|
128
134
|
<div class="col-auto q-ml-auto">
|
|
129
|
-
<q-toggle
|
|
130
|
-
|
|
135
|
+
<q-toggle
|
|
136
|
+
class="q-ml-md"
|
|
137
|
+
v-model="state.brief"
|
|
138
|
+
label="Brief Mode"
|
|
139
|
+
@update:model-value="(val) => toggleBrief(val)"
|
|
140
|
+
title="skip middle classes, config module_prefix to enable it"
|
|
141
|
+
dense
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="col-auto">
|
|
145
|
+
<q-toggle
|
|
146
|
+
v-model="state.hidePrimitiveRoute"
|
|
147
|
+
@update:model-value="(val) => toggleHidePrimitiveRoute(val)"
|
|
148
|
+
label="Hide Primitive"
|
|
149
|
+
title="hide routes return primitive"
|
|
150
|
+
dense
|
|
151
|
+
/>
|
|
131
152
|
</div>
|
|
132
153
|
<div class="col-auto">
|
|
133
|
-
<q-btn
|
|
154
|
+
<q-btn
|
|
155
|
+
outline
|
|
156
|
+
@click="onReset"
|
|
157
|
+
title="may be very slow"
|
|
158
|
+
label="Show All"
|
|
159
|
+
/>
|
|
134
160
|
</div>
|
|
135
161
|
<div class="col-auto">
|
|
136
162
|
<q-btn outline icon="search" label="Search" @click="showDialog()" />
|
|
@@ -171,57 +197,80 @@
|
|
|
171
197
|
</div>
|
|
172
198
|
</div>
|
|
173
199
|
</div>
|
|
174
|
-
<div
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
borderRight: '1px solid #e0e0e0',
|
|
181
|
-
backgroundColor: '#fff',
|
|
182
|
-
minHeight: 0,
|
|
183
|
-
}"
|
|
200
|
+
<div style="flex: 1 1 auto; min-height: 0;">
|
|
201
|
+
<q-splitter
|
|
202
|
+
v-model="state.splitter"
|
|
203
|
+
unit="px"
|
|
204
|
+
:limits="[200, 800]"
|
|
205
|
+
class="fit"
|
|
184
206
|
>
|
|
185
|
-
<
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
:
|
|
192
|
-
|
|
193
|
-
:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
clickable
|
|
207
|
-
v-ripple
|
|
208
|
-
:active="state.routeId === route.id"
|
|
209
|
-
active-class=""
|
|
210
|
-
@click="selectRoute(route.id)"
|
|
207
|
+
<template #before>
|
|
208
|
+
<div
|
|
209
|
+
id="tag-navigator"
|
|
210
|
+
class="column no-wrap"
|
|
211
|
+
:style="{
|
|
212
|
+
borderRight: '1px solid #e0e0e0',
|
|
213
|
+
backgroundColor: '#fff',
|
|
214
|
+
minHeight: 0,
|
|
215
|
+
height: '100%'
|
|
216
|
+
}"
|
|
217
|
+
>
|
|
218
|
+
<q-scroll-area class="fit">
|
|
219
|
+
<q-list dense separator>
|
|
220
|
+
<q-expansion-item
|
|
221
|
+
v-for="tag in state.rawTags"
|
|
222
|
+
:key="tag.name"
|
|
223
|
+
expand-separator
|
|
224
|
+
:model-value="state.tag === tag.name"
|
|
225
|
+
@update:model-value="(val) => toggleTag(tag.name, val)"
|
|
226
|
+
:header-class="state.tag === tag.name ? 'text-primary text-bold' : ''"
|
|
227
|
+
content-class="q-pa-none"
|
|
211
228
|
>
|
|
212
|
-
<
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
229
|
+
<template #header>
|
|
230
|
+
<div class="row items-center" style="width: 100%">
|
|
231
|
+
<div class="row items-end">
|
|
232
|
+
<q-icon
|
|
233
|
+
class="q-mr-sm"
|
|
234
|
+
:name="state.tag == tag.name ? 'folder' : 'folder_open'"
|
|
235
|
+
></q-icon>
|
|
236
|
+
<span>{{ tag.name }}</span>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</template>
|
|
240
|
+
|
|
241
|
+
<q-list separator>
|
|
242
|
+
<q-item
|
|
243
|
+
v-for="route in (state.hidePrimitiveRoute ? tag.routes.filter(r => !r.is_primitive) :tag.routes || [])"
|
|
244
|
+
:key="route.id"
|
|
245
|
+
clickable
|
|
246
|
+
v-ripple
|
|
247
|
+
:active="state.routeId === route.id"
|
|
248
|
+
active-class=""
|
|
249
|
+
@click="selectRoute(route.id)"
|
|
250
|
+
>
|
|
251
|
+
<q-item-section>
|
|
252
|
+
<span class="q-ml-lg" style="white-space: nowrap">
|
|
253
|
+
<q-icon class="q-mr-sm" name="data_object"></q-icon>
|
|
254
|
+
{{ route.name }}
|
|
255
|
+
</span>
|
|
256
|
+
</q-item-section>
|
|
257
|
+
</q-item>
|
|
258
|
+
<q-item v-if="!tag.routes || tag.routes.length === 0" dense>
|
|
259
|
+
<q-item-section class="text-grey-6"
|
|
260
|
+
>No routes</q-item-section
|
|
261
|
+
>
|
|
262
|
+
</q-item>
|
|
263
|
+
</q-list>
|
|
264
|
+
</q-expansion-item>
|
|
219
265
|
</q-list>
|
|
220
|
-
</q-
|
|
221
|
-
</
|
|
222
|
-
</
|
|
223
|
-
|
|
224
|
-
|
|
266
|
+
</q-scroll-area>
|
|
267
|
+
</div>
|
|
268
|
+
</template>
|
|
269
|
+
|
|
270
|
+
<template #after>
|
|
271
|
+
<div id="graph" class="fit"></div>
|
|
272
|
+
</template>
|
|
273
|
+
</q-splitter>
|
|
225
274
|
</div>
|
|
226
275
|
|
|
227
276
|
<!-- Detail Dialog -->
|
fastapi_voyager/web/vue-main.js
CHANGED
|
@@ -13,7 +13,7 @@ const app = createApp({
|
|
|
13
13
|
tagOptions: [], // array of strings
|
|
14
14
|
routeId: null,
|
|
15
15
|
routeOptions: [], // [{ label, value }]
|
|
16
|
-
|
|
16
|
+
schemaId: null,
|
|
17
17
|
schemaOptions: [], // [{ label, value }]
|
|
18
18
|
routeItems: {}, // { id: { label, value } }
|
|
19
19
|
showFields: "object",
|
|
@@ -26,9 +26,11 @@ const app = createApp({
|
|
|
26
26
|
hidePrimitiveRoute: false,
|
|
27
27
|
generating: false,
|
|
28
28
|
rawTags: [], // [{ name, routes: [{ id, name }] }]
|
|
29
|
-
rawSchemas: [], // [{ name,
|
|
29
|
+
rawSchemas: [], // [{ name, id }]
|
|
30
30
|
rawSchemasFull: [], // full objects with source_code & fields
|
|
31
31
|
initializing: true,
|
|
32
|
+
// Splitter size (left panel width in px)
|
|
33
|
+
splitter: 300,
|
|
32
34
|
});
|
|
33
35
|
const showDetail = ref(false);
|
|
34
36
|
const showSchemaFieldFilter = ref(false);
|
|
@@ -45,6 +47,7 @@ const app = createApp({
|
|
|
45
47
|
const schemaFieldFilterSchema = ref(null); // external schemaName for schema-field-filter
|
|
46
48
|
const schemaCodeName = ref("");
|
|
47
49
|
const routeCodeId = ref("");
|
|
50
|
+
|
|
48
51
|
function openDetail() {
|
|
49
52
|
showDetail.value = true;
|
|
50
53
|
}
|
|
@@ -68,10 +71,10 @@ const app = createApp({
|
|
|
68
71
|
function onFilterSchemas(val, update) {
|
|
69
72
|
const normalized = (val || "").toLowerCase();
|
|
70
73
|
update(() => {
|
|
71
|
-
const makeLabel = (s) => `${s.name} (${s.
|
|
74
|
+
const makeLabel = (s) => `${s.name} (${s.id})`;
|
|
72
75
|
let list = state.rawSchemas.map((s) => ({
|
|
73
76
|
label: makeLabel(s),
|
|
74
|
-
value: s.
|
|
77
|
+
value: s.id,
|
|
75
78
|
}));
|
|
76
79
|
if (normalized) {
|
|
77
80
|
list = list.filter((opt) =>
|
|
@@ -91,7 +94,7 @@ const app = createApp({
|
|
|
91
94
|
state.rawSchemasFull = Array.isArray(data.schemas) ? data.schemas : [];
|
|
92
95
|
state.rawSchemas = state.rawSchemasFull.map((s) => ({
|
|
93
96
|
name: s.name,
|
|
94
|
-
|
|
97
|
+
id: s.id,
|
|
95
98
|
}));
|
|
96
99
|
state.routeItems = data.tags
|
|
97
100
|
.map((t) => t.routes)
|
|
@@ -103,8 +106,8 @@ const app = createApp({
|
|
|
103
106
|
|
|
104
107
|
state.tagOptions = state.rawTags.map((t) => t.name);
|
|
105
108
|
state.schemaOptions = state.rawSchemas.map((s) => ({
|
|
106
|
-
label: `${s.name} (${s.
|
|
107
|
-
value: s.
|
|
109
|
+
label: `${s.name} (${s.id})`,
|
|
110
|
+
value: s.id,
|
|
108
111
|
}));
|
|
109
112
|
// default route options placeholder
|
|
110
113
|
state.routeOptions = [];
|
|
@@ -115,16 +118,16 @@ const app = createApp({
|
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
120
|
|
|
118
|
-
async function onGenerate() {
|
|
121
|
+
async function onGenerate(resetZoom = true) {
|
|
119
122
|
state.generating = true;
|
|
120
123
|
try {
|
|
121
124
|
const payload = {
|
|
122
125
|
tags: state.tag ? [state.tag] : null,
|
|
123
|
-
schema_name: state.
|
|
126
|
+
schema_name: state.schemaId || null,
|
|
124
127
|
route_name: state.routeId || null,
|
|
125
128
|
show_fields: state.showFields,
|
|
126
129
|
brief: state.brief,
|
|
127
|
-
hide_primitive_route: state.hidePrimitiveRoute
|
|
130
|
+
hide_primitive_route: state.hidePrimitiveRoute,
|
|
128
131
|
};
|
|
129
132
|
|
|
130
133
|
const res = await fetch("dot", {
|
|
@@ -136,28 +139,28 @@ const app = createApp({
|
|
|
136
139
|
|
|
137
140
|
// create graph instance once
|
|
138
141
|
const graphUI = new GraphUI("#graph", {
|
|
139
|
-
onSchemaClick: (
|
|
140
|
-
if (state.rawSchemas.find((s) => s.
|
|
141
|
-
schemaFieldFilterSchema.value =
|
|
142
|
+
onSchemaClick: (id) => {
|
|
143
|
+
if (state.rawSchemas.find((s) => s.id === id)) {
|
|
144
|
+
schemaFieldFilterSchema.value = id;
|
|
142
145
|
showSchemaFieldFilter.value = true;
|
|
143
146
|
}
|
|
144
147
|
},
|
|
145
|
-
onSchemaAltClick: (
|
|
146
|
-
// priority: schema
|
|
147
|
-
if (state.rawSchemas.find((s) => s.
|
|
148
|
-
schemaCodeName.value =
|
|
148
|
+
onSchemaAltClick: (id) => {
|
|
149
|
+
// priority: schema id; else route id
|
|
150
|
+
if (state.rawSchemas.find((s) => s.id === id)) {
|
|
151
|
+
schemaCodeName.value = id;
|
|
149
152
|
showSchemaCode.value = true;
|
|
150
153
|
return;
|
|
151
154
|
}
|
|
152
|
-
if (
|
|
153
|
-
routeCodeId.value =
|
|
155
|
+
if (id in state.routeItems) {
|
|
156
|
+
routeCodeId.value = id;
|
|
154
157
|
showRouteCode.value = true;
|
|
155
158
|
return;
|
|
156
159
|
}
|
|
157
160
|
},
|
|
158
161
|
});
|
|
159
162
|
|
|
160
|
-
await graphUI.render(dotText);
|
|
163
|
+
await graphUI.render(dotText, resetZoom);
|
|
161
164
|
} catch (e) {
|
|
162
165
|
console.error("Generate failed", e);
|
|
163
166
|
} finally {
|
|
@@ -169,7 +172,7 @@ const app = createApp({
|
|
|
169
172
|
try {
|
|
170
173
|
const payload = {
|
|
171
174
|
tags: state.tag ? [state.tag] : null,
|
|
172
|
-
schema_name: state.
|
|
175
|
+
schema_name: state.schemaId || null,
|
|
173
176
|
route_name: state.routeId || null,
|
|
174
177
|
show_fields: state.showFields,
|
|
175
178
|
brief: state.brief,
|
|
@@ -230,66 +233,44 @@ const app = createApp({
|
|
|
230
233
|
async function onReset() {
|
|
231
234
|
state.tag = null;
|
|
232
235
|
state.routeId = "";
|
|
233
|
-
state.
|
|
234
|
-
state.showFields = "object";
|
|
236
|
+
state.schemaId = null;
|
|
237
|
+
// state.showFields = "object";
|
|
235
238
|
state.brief = false;
|
|
236
|
-
onGenerate()
|
|
237
|
-
// await loadInitial();
|
|
239
|
+
onGenerate();
|
|
238
240
|
}
|
|
239
241
|
|
|
240
242
|
function toggleTag(tagName, expanded = null) {
|
|
241
243
|
if (expanded === true) {
|
|
242
244
|
state.tag = tagName;
|
|
245
|
+
state.routeId = "";
|
|
246
|
+
onGenerate();
|
|
243
247
|
return;
|
|
244
248
|
}
|
|
245
249
|
}
|
|
246
250
|
|
|
247
251
|
function selectRoute(routeId) {
|
|
248
252
|
if (state.routeId === routeId) {
|
|
249
|
-
state.routeId =
|
|
253
|
+
state.routeId = "";
|
|
250
254
|
} else {
|
|
251
|
-
state.routeId = routeId
|
|
255
|
+
state.routeId = routeId;
|
|
252
256
|
}
|
|
253
|
-
onGenerate()
|
|
257
|
+
onGenerate();
|
|
254
258
|
}
|
|
255
259
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
()
|
|
259
|
-
|
|
260
|
-
if (!state.initializing) {
|
|
261
|
-
state.routeId = ''
|
|
262
|
-
onGenerate();
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
watch(
|
|
268
|
-
() => state.showFields,
|
|
269
|
-
() => {
|
|
270
|
-
if (!state.initializing) {
|
|
271
|
-
onGenerate();
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
)
|
|
260
|
+
function toggleShowField(field) {
|
|
261
|
+
state.showFields = field;
|
|
262
|
+
onGenerate(false);
|
|
263
|
+
}
|
|
275
264
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
()
|
|
279
|
-
|
|
280
|
-
onGenerate();
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
);
|
|
265
|
+
function toggleBrief(val) {
|
|
266
|
+
state.brief = val;
|
|
267
|
+
onGenerate();
|
|
268
|
+
}
|
|
284
269
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
()
|
|
288
|
-
|
|
289
|
-
onGenerate();
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
);
|
|
270
|
+
function toggleHidePrimitiveRoute(val) {
|
|
271
|
+
state.hidePrimitiveRoute = val;
|
|
272
|
+
onGenerate(false);
|
|
273
|
+
}
|
|
293
274
|
|
|
294
275
|
onMounted(async () => {
|
|
295
276
|
await loadInitial();
|
|
@@ -298,6 +279,8 @@ const app = createApp({
|
|
|
298
279
|
return {
|
|
299
280
|
state,
|
|
300
281
|
toggleTag,
|
|
282
|
+
toggleBrief,
|
|
283
|
+
toggleHidePrimitiveRoute,
|
|
301
284
|
selectRoute,
|
|
302
285
|
onFilterTags,
|
|
303
286
|
onFilterSchemas,
|
|
@@ -326,10 +309,15 @@ const app = createApp({
|
|
|
326
309
|
// render graph dialog
|
|
327
310
|
showRenderGraph,
|
|
328
311
|
renderCoreData,
|
|
312
|
+
toggleShowField,
|
|
329
313
|
};
|
|
330
314
|
},
|
|
331
315
|
});
|
|
332
316
|
app.use(window.Quasar);
|
|
317
|
+
// Set Quasar primary theme color to green
|
|
318
|
+
if (window.Quasar && typeof window.Quasar.setCssVar === "function") {
|
|
319
|
+
window.Quasar.setCssVar("primary", "#009485");
|
|
320
|
+
}
|
|
333
321
|
app.component("schema-field-filter", SchemaFieldFilter);
|
|
334
322
|
app.component("schema-code-display", SchemaCodeDisplay);
|
|
335
323
|
app.component("route-code-display", RouteCodeDisplay);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-voyager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -30,6 +30,10 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
|
|
31
31
|
<p align="center"><img src="./voyager.jpg" alt="" /></p>
|
|
32
32
|
|
|
33
|
+
|
|
34
|
+
[](https://www.youtube.com/watch?v=PGlbQq1M-n8 "FastAPI Voyager")
|
|
35
|
+
|
|
36
|
+
|
|
33
37
|
> This repo is still in early stage, currently it supports pydantic v2 only, previous name: fastapi-router-viz
|
|
34
38
|
|
|
35
39
|
Inspect your API interactively
|
|
@@ -105,10 +109,10 @@ click a node to highlight it's upperstream and downstream nodes. figure out the
|
|
|
105
109
|
|
|
106
110
|
```python
|
|
107
111
|
from fastapi import FastAPI
|
|
108
|
-
from fastapi_voyager
|
|
112
|
+
from fastapi_voyager import create_voyager
|
|
109
113
|
from tests.demo import app
|
|
110
114
|
|
|
111
|
-
app.mount('/voyager',
|
|
115
|
+
app.mount('/voyager', create_voyager(
|
|
112
116
|
app,
|
|
113
117
|
module_color={"tests.service": "red"},
|
|
114
118
|
module_prefix="tests.service"))
|
|
@@ -164,7 +168,7 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
|
|
|
164
168
|
|
|
165
169
|
## Plan before v1.0
|
|
166
170
|
|
|
167
|
-
features:
|
|
171
|
+
### features:
|
|
168
172
|
- [x] group schemas by module hierarchy
|
|
169
173
|
- [x] module-based coloring via Analytics(module_color={...})
|
|
170
174
|
- [x] view in web browser
|
|
@@ -183,38 +187,62 @@ features:
|
|
|
183
187
|
- [x] add tooltips
|
|
184
188
|
- [x] route
|
|
185
189
|
- [x] group routes by module hierarchy
|
|
186
|
-
- [
|
|
187
|
-
- [
|
|
188
|
-
- [
|
|
190
|
+
- [x] add response_model in route
|
|
191
|
+
- [x] fixed left bar show tag/ route
|
|
192
|
+
- [x] export voyager core data into json (for better debugging)
|
|
193
|
+
- [x] add api to rebuild core data from json, and render it
|
|
194
|
+
- [x] fix Generic case `test_generic.py`
|
|
195
|
+
- [x] show tips for routes not return pydantic type.
|
|
196
|
+
- [x] fix duplicated link from class and parent class, it also break clicking highlight
|
|
197
|
+
- [x] refactor: abstract render module
|
|
198
|
+
|
|
199
|
+
### backlog
|
|
189
200
|
- [ ] user can generate nodes/edges manually and connect to generated ones
|
|
190
201
|
- [ ] add owner
|
|
191
202
|
- [ ] add extra info for schema
|
|
192
|
-
- [ ]
|
|
193
|
-
|
|
194
|
-
- [x] fixed left bar show tag/ route
|
|
195
|
-
- [ ] display standard ER diagram `difference`
|
|
203
|
+
- [ ] fixed left/right bar show field information
|
|
204
|
+
- [ ] display standard ER diagram `hard`
|
|
196
205
|
- [ ] display potential invalid links
|
|
197
|
-
- [ ] integration with pydantic-resolve
|
|
198
|
-
- [ ] show difference between resolve, post fields
|
|
199
|
-
- [x] strikethrough for excluded fields
|
|
200
|
-
- [ ] display loader as edges
|
|
201
|
-
- [x] export voyager core data into json (for better debugging)
|
|
202
|
-
- [x] add api to rebuild core data from json, and render it
|
|
203
|
-
- [x] fix Generic case `test_generic.py`
|
|
204
|
-
- [ ] show tips for routes not return pydantic type.
|
|
205
|
-
- [ ] add http method for route
|
|
206
206
|
|
|
207
207
|
bugs & non feature:
|
|
208
|
-
- [x] fix duplicated link from class and parent class, it also break clicking highlight
|
|
209
208
|
- [ ] add tests
|
|
210
|
-
- [
|
|
211
|
-
|
|
209
|
+
- [ ] support dataclass (pending)
|
|
210
|
+
|
|
211
|
+
### in analysis
|
|
212
|
+
- [ ] click field to highlight links
|
|
213
|
+
- [ ] animation effect for edges
|
|
214
|
+
- [ ] customrized right click panel
|
|
215
|
+
- [ ] show own dependencies
|
|
216
|
+
|
|
217
|
+
### plan:
|
|
218
|
+
|
|
219
|
+
#### 0.9
|
|
220
|
+
- [x] refactor: server.py
|
|
221
|
+
- [x] rename create_app_with_fastapi -> create_voyager
|
|
222
|
+
- [x] add doc for parameters
|
|
223
|
+
- [x] improve initialization time cost
|
|
224
|
+
- [x] query route / schema info through realtime api
|
|
225
|
+
- [x] adjust fe
|
|
226
|
+
|
|
227
|
+
#### 0.10
|
|
228
|
+
- [ ] logging information
|
|
229
|
+
- [ ] open route in swagger
|
|
230
|
+
- config docs path
|
|
231
|
+
- [ ] add http method for route
|
|
232
|
+
- [ ] enable/disable module cluster (may save space)
|
|
233
|
+
|
|
234
|
+
#### 0.11
|
|
235
|
+
- [ ] integration with pydantic-resolve
|
|
236
|
+
- [ ] show hint for resolve, post fields
|
|
237
|
+
- [ ] display loader as edges
|
|
238
|
+
|
|
212
239
|
|
|
213
240
|
## Using with pydantic-resolve
|
|
214
241
|
|
|
242
|
+
WIP: ...
|
|
243
|
+
|
|
215
244
|
pydantic-resolve's @ensure_subset decorator is helpful to pick fields from `source class` in safe.
|
|
216
245
|
|
|
217
|
-
TODO: ...
|
|
218
246
|
|
|
219
247
|
|
|
220
248
|
## Credits
|
|
@@ -225,7 +253,13 @@ TODO: ...
|
|
|
225
253
|
|
|
226
254
|
## Changelog
|
|
227
255
|
|
|
256
|
+
- 0.9:
|
|
257
|
+
- 0.9.1:
|
|
258
|
+
- api change: from `create_app_with_fastapi` to `create_voyager`, and expose as `from fastapi_voyager import create_voyager`
|
|
259
|
+
- optimization: lazy load vscode link and source code, speed up the initialization.
|
|
228
260
|
- 0.8:
|
|
261
|
+
- 0.8.3
|
|
262
|
+
- upgrade theme
|
|
229
263
|
- 0.8.2
|
|
230
264
|
- fix silly typo.
|
|
231
265
|
- 0.8.1
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
fastapi_voyager/__init__.py,sha256=
|
|
2
|
-
fastapi_voyager/cli.py,sha256=
|
|
1
|
+
fastapi_voyager/__init__.py,sha256=tZy0Nkj8kTaMgbvHy-mGxVcFGVX0Km-36dnzsAIG2uk,230
|
|
2
|
+
fastapi_voyager/cli.py,sha256=kQb4g6JEGZR99e5r8LyFFEeb_-uT-n_gp_sDoYG3R7k,11118
|
|
3
3
|
fastapi_voyager/filter.py,sha256=2Yt37o8mhqSqleafO4YRrumh_ExYUqzXFOxQRPuTbAc,8078
|
|
4
4
|
fastapi_voyager/module.py,sha256=Z2QHNmiLk6ZAJlm2nSmO875Q33TweSg8UxZSzIpU9zY,3499
|
|
5
5
|
fastapi_voyager/render.py,sha256=qy3g1Rz1s8XkuR_6n1Q1YPwy_oMOjWjNswTHQjdz4N0,7765
|
|
6
|
-
fastapi_voyager/server.py,sha256=
|
|
7
|
-
fastapi_voyager/type.py,sha256=
|
|
6
|
+
fastapi_voyager/server.py,sha256=pg-LHDj4yU0usDA1b2X2Kt2_OCrexFA2G9ifGxb52Uc,6196
|
|
7
|
+
fastapi_voyager/type.py,sha256=pWYKmgb9e0W_JeD7k54Mr2lxUZV_Ir9TNpewGRwHyHQ,1629
|
|
8
8
|
fastapi_voyager/type_helper.py,sha256=hjBC4E0tgBpQDlYxGg74uK07SXjsrAgictEETJfIpYM,9231
|
|
9
|
-
fastapi_voyager/version.py,sha256=
|
|
10
|
-
fastapi_voyager/voyager.py,sha256=
|
|
11
|
-
fastapi_voyager/web/graph-ui.js,sha256=
|
|
9
|
+
fastapi_voyager/version.py,sha256=jjAALUE-D-m7hgihxqGZyrZQPe4_Ch5MQPu89WFuVmw,48
|
|
10
|
+
fastapi_voyager/voyager.py,sha256=pXvA3ye5Jq6aJ6YrZFbTHjJ9MzdydqWllIgY8RFK_4A,10793
|
|
11
|
+
fastapi_voyager/web/graph-ui.js,sha256=0hqMCzsPJfhgWDuwiXVXaGQL-_7urT_zryo4sXMN8jQ,4209
|
|
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=AnsqXfzDrUPO12EWtiT_XJdJ6Kx3qBsRn1IfX393G48,14298
|
|
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=p8akwqNf9sDM71xio0SN-3JznUOIksoy9SGRiqAeg2s,9459
|
|
18
18
|
fastapi_voyager/web/component/render-graph.js,sha256=e8Xgh2Kl-nYU0P1gstEmAepCgFnk2J6UdxW8TlMafGs,2322
|
|
19
|
-
fastapi_voyager/web/component/route-code-display.js,sha256=
|
|
20
|
-
fastapi_voyager/web/component/schema-code-display.js,sha256=
|
|
21
|
-
fastapi_voyager/web/component/schema-field-filter.js,sha256=
|
|
19
|
+
fastapi_voyager/web/component/route-code-display.js,sha256=4VN88n_7FYHxvruP5AbgTdF6Lyj6v_7Ocqy8zOVMjVA,4065
|
|
20
|
+
fastapi_voyager/web/component/schema-code-display.js,sha256=_Okkr8ceJsA_Xngyj_GpxJqdN01GzCjJTeAZamSA9Ks,6514
|
|
21
|
+
fastapi_voyager/web/component/schema-field-filter.js,sha256=1llPkN8w-RrBPHVtjjMcObndaqPnmZ7e4iU6dMJMIaE,6224
|
|
22
22
|
fastapi_voyager/web/icon/android-chrome-192x192.png,sha256=35sBy6jmUFJCcquStaafHH1qClZIbd-X3PIKSeLkrNo,37285
|
|
23
23
|
fastapi_voyager/web/icon/android-chrome-512x512.png,sha256=eb2eDjCwIruc05029_0L9hcrkVkv8KceLn1DJMYU0zY,210789
|
|
24
24
|
fastapi_voyager/web/icon/apple-touch-icon.png,sha256=gnWK46tPnvSw1-oYZjgI02wpoO4OrIzsVzGHC5oKWO0,33187
|
|
@@ -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.9.1.dist-info/METADATA,sha256=7yR9Wj87QH6f2kxA4QiqDnwWJPQ2-rGOhpC6ZYYfHfE,8914
|
|
30
|
+
fastapi_voyager-0.9.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
31
|
+
fastapi_voyager-0.9.1.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
|
|
32
|
+
fastapi_voyager-0.9.1.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
|
|
33
|
+
fastapi_voyager-0.9.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|