fastapi-voyager 0.8.3__py3-none-any.whl → 0.9.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_voyager/__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 -28
- fastapi_voyager/web/component/schema-code-display.js +53 -19
- fastapi_voyager/web/component/schema-field-filter.js +13 -10
- fastapi_voyager/web/index.html +1 -2
- fastapi_voyager/web/vue-main.js +38 -79
- {fastapi_voyager-0.8.3.dist-info → fastapi_voyager-0.9.2.dist-info}/METADATA +85 -42
- {fastapi_voyager-0.8.3.dist-info → fastapi_voyager-0.9.2.dist-info}/RECORD +16 -16
- {fastapi_voyager-0.8.3.dist-info → fastapi_voyager-0.9.2.dist-info}/WHEEL +0 -0
- {fastapi_voyager-0.8.3.dist-info → fastapi_voyager-0.9.2.dist-info}/entry_points.txt +0 -0
- {fastapi_voyager-0.8.3.dist-info → fastapi_voyager-0.9.2.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.2"
|
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
|
|
@@ -4,60 +4,119 @@ const { defineComponent, ref, watch, onMounted } = window.Vue;
|
|
|
4
4
|
// Props:
|
|
5
5
|
// routeId: route id key in routeItems
|
|
6
6
|
// modelValue: dialog visibility
|
|
7
|
-
// routes: object map { id: { id, name, source_code } }
|
|
8
7
|
export default defineComponent({
|
|
9
|
-
name:
|
|
8
|
+
name: "RouteCodeDisplay",
|
|
10
9
|
props: {
|
|
11
10
|
routeId: { type: String, required: true },
|
|
12
11
|
modelValue: { type: Boolean, default: false },
|
|
13
|
-
routes: { type: Object, default: () => ({}) },
|
|
14
12
|
},
|
|
15
|
-
emits: [
|
|
13
|
+
emits: ["close"],
|
|
16
14
|
setup(props, { emit }) {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
15
|
+
const loading = ref(false);
|
|
16
|
+
const code = ref("");
|
|
17
|
+
const error = ref("");
|
|
18
|
+
const link = ref("");
|
|
20
19
|
|
|
21
|
-
function close() {
|
|
20
|
+
function close() {
|
|
21
|
+
emit("close");
|
|
22
|
+
}
|
|
22
23
|
|
|
23
24
|
function highlightLater() {
|
|
24
25
|
requestAnimationFrame(() => {
|
|
25
26
|
try {
|
|
26
27
|
if (window.hljs) {
|
|
27
|
-
const block = document.querySelector(
|
|
28
|
+
const block = document.querySelector(
|
|
29
|
+
".frv-route-code-display pre code.language-python"
|
|
30
|
+
);
|
|
28
31
|
if (block) {
|
|
29
32
|
window.hljs.highlightElement(block);
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
} catch (e) {
|
|
33
|
-
console.warn(
|
|
36
|
+
console.warn("highlight failed", e);
|
|
34
37
|
}
|
|
35
38
|
});
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
function load() {
|
|
41
|
+
async function load() {
|
|
42
|
+
if (!props.routeId) {
|
|
43
|
+
code.value = "";
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
loading.value = true;
|
|
39
48
|
error.value = null;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
code.value = "";
|
|
50
|
+
link.value = "";
|
|
51
|
+
|
|
52
|
+
// try to fetch from server: POST /source with { schema_name: routeId }
|
|
53
|
+
const payload = { schema_name: props.routeId };
|
|
54
|
+
try {
|
|
55
|
+
const resp = await fetch(`source`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: {
|
|
58
|
+
Accept: "application/json",
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(payload),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const data = await resp.json().catch(() => ({}));
|
|
65
|
+
if (resp.ok) {
|
|
66
|
+
code.value = data.source_code || "// no source code available";
|
|
67
|
+
} else {
|
|
68
|
+
error.value = (data && data.error) || "Failed to load source";
|
|
69
|
+
}
|
|
70
|
+
} catch (e) {
|
|
71
|
+
error.value = e && e.message ? e.message : "Failed to load source";
|
|
72
|
+
} finally {
|
|
73
|
+
loading.value = false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const resp = await fetch(`vscode-link`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: {
|
|
80
|
+
Accept: "application/json",
|
|
81
|
+
"Content-Type": "application/json",
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify(payload),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const data = await resp.json().catch(() => ({}));
|
|
87
|
+
if (resp.ok) {
|
|
88
|
+
link.value = data.link || "// no source code available";
|
|
89
|
+
} else {
|
|
90
|
+
error.value += (data && data.error) || "Failed to load vscode link";
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
} finally {
|
|
94
|
+
loading.value = false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!error.value) {
|
|
45
98
|
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
99
|
}
|
|
53
100
|
}
|
|
54
101
|
|
|
55
|
-
watch(
|
|
56
|
-
|
|
102
|
+
watch(
|
|
103
|
+
() => props.modelValue,
|
|
104
|
+
(v) => {
|
|
105
|
+
if (v) load();
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
watch(
|
|
109
|
+
() => props.routeId,
|
|
110
|
+
() => {
|
|
111
|
+
if (props.modelValue) load();
|
|
112
|
+
}
|
|
113
|
+
);
|
|
57
114
|
|
|
58
|
-
onMounted(() => {
|
|
115
|
+
onMounted(() => {
|
|
116
|
+
if (props.modelValue) load();
|
|
117
|
+
});
|
|
59
118
|
|
|
60
|
-
return { code, error, close, link };
|
|
119
|
+
return { loading, code, error, close, link };
|
|
61
120
|
},
|
|
62
121
|
template: `
|
|
63
122
|
<div class="frv-route-code-display" style="border:1px solid #ccc; position:relative; width:50vw; max-width:50vw; height:100%; background:#fff;">
|
|
@@ -66,8 +125,9 @@ export default defineComponent({
|
|
|
66
125
|
<a :href="link" target="_blank" rel="noopener" style="font-size:12px; color:#3b82f6;">Open in VSCode</a>
|
|
67
126
|
</div>
|
|
68
127
|
<div style="padding:40px 16px 16px 16px; height:100%; box-sizing:border-box; overflow:auto;">
|
|
69
|
-
<div v-if="
|
|
128
|
+
<div v-if="loading" style="font-family:Menlo, monospace; font-size:12px;">Loading source...</div>
|
|
129
|
+
<div v-else-if="error" style="color:#c10015; font-family:Menlo, monospace; font-size:12px;">{{ error }}</div>
|
|
70
130
|
<pre v-else style="margin:0;"><code class="language-python">{{ code }}</code></pre>
|
|
71
131
|
</div>
|
|
72
|
-
</div
|
|
132
|
+
</div>`,
|
|
73
133
|
});
|
|
@@ -14,17 +14,16 @@ export default defineComponent({
|
|
|
14
14
|
props: {
|
|
15
15
|
schemaName: { type: String, required: true },
|
|
16
16
|
modelValue: { type: Boolean, default: false },
|
|
17
|
-
|
|
18
|
-
schemas: { type: Array, default: () => [] },
|
|
17
|
+
schemas: { type: Object, default: () => ({}) },
|
|
19
18
|
},
|
|
20
19
|
emits: ["close"],
|
|
21
20
|
setup(props, { emit }) {
|
|
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,73 @@ 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";
|
|
74
|
+
} else {
|
|
75
|
+
error.value = (data && data.error) || "Failed to load source";
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
error.value = "Failed to load source";
|
|
79
|
+
} finally {
|
|
80
|
+
loading.value = false;
|
|
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";
|
|
68
96
|
} else {
|
|
69
|
-
error.value
|
|
97
|
+
error.value += (data && data.error) || "Failed to load source";
|
|
70
98
|
}
|
|
71
99
|
} catch (e) {
|
|
72
100
|
error.value = "Failed to load source";
|
|
73
101
|
} finally {
|
|
74
102
|
loading.value = false;
|
|
75
103
|
}
|
|
104
|
+
|
|
105
|
+
fields.value = props.schemas[props.schemaName].fields || []
|
|
106
|
+
|
|
107
|
+
if (!error.value && tab.value === "source") {
|
|
108
|
+
highlightLater();
|
|
109
|
+
}
|
|
76
110
|
}
|
|
77
111
|
|
|
78
112
|
// re-highlight when switching back to source tab
|
|
79
113
|
watch(
|
|
80
114
|
() => tab.value,
|
|
81
115
|
(val) => {
|
|
82
|
-
if (val ===
|
|
116
|
+
if (val === "source") {
|
|
83
117
|
highlightLater();
|
|
84
118
|
}
|
|
85
119
|
}
|
|
@@ -14,7 +14,8 @@ export default defineComponent({
|
|
|
14
14
|
name: "SchemaFieldFilter",
|
|
15
15
|
props: {
|
|
16
16
|
schemaName: { type: String, default: null }, // external injection triggers auto-query
|
|
17
|
-
|
|
17
|
+
// externally provided schemas dict (state.rawSchemasFull): { [id]: schema }
|
|
18
|
+
schemas: { type: Object, default: () => ({}) },
|
|
18
19
|
},
|
|
19
20
|
emits: ["queried", "close"],
|
|
20
21
|
setup(props, { emit }) {
|
|
@@ -39,12 +40,14 @@ export default defineComponent({
|
|
|
39
40
|
let lastAppliedExternal = null;
|
|
40
41
|
|
|
41
42
|
async function loadSchemas() {
|
|
42
|
-
//
|
|
43
|
+
// Use externally provided props.schemas dict directly; no network call.
|
|
43
44
|
state.error = null;
|
|
44
|
-
|
|
45
|
+
const dict = props.schemas && typeof props.schemas === "object" ? props.schemas : {};
|
|
46
|
+
// Flatten to array for local operations
|
|
47
|
+
state.schemas = Object.values(dict);
|
|
45
48
|
state.schemaOptions = state.schemas.map((s) => ({
|
|
46
|
-
label: `${s.name} (${s.
|
|
47
|
-
value: s.
|
|
49
|
+
label: `${s.name} (${s.id})`,
|
|
50
|
+
value: s.id,
|
|
48
51
|
}));
|
|
49
52
|
// Maintain compatibility: loadingSchemas flag toggled quickly (no async work)
|
|
50
53
|
state.loadingSchemas = false;
|
|
@@ -54,8 +57,8 @@ export default defineComponent({
|
|
|
54
57
|
const needle = (val || "").toLowerCase();
|
|
55
58
|
update(() => {
|
|
56
59
|
let opts = state.schemas.map((s) => ({
|
|
57
|
-
label: `${s.name} (${s.
|
|
58
|
-
value: s.
|
|
60
|
+
label: `${s.name} (${s.id})`,
|
|
61
|
+
value: s.id,
|
|
59
62
|
}));
|
|
60
63
|
if (needle) {
|
|
61
64
|
opts = opts.filter((o) => o.label.toLowerCase().includes(needle));
|
|
@@ -67,7 +70,7 @@ export default defineComponent({
|
|
|
67
70
|
function onSchemaChange(val) {
|
|
68
71
|
state.schemaFullname = val;
|
|
69
72
|
state.fieldName = null;
|
|
70
|
-
const schema = state.schemas.find((s) => s.
|
|
73
|
+
const schema = state.schemas.find((s) => s.id === val);
|
|
71
74
|
state.fieldOptions = schema ? schema.fields.map((f) => f.name) : [];
|
|
72
75
|
}
|
|
73
76
|
|
|
@@ -106,9 +109,9 @@ export default defineComponent({
|
|
|
106
109
|
function applyExternalSchema(name) {
|
|
107
110
|
if (!name || !state.schemas.length) return;
|
|
108
111
|
if (lastAppliedExternal === name) return; // avoid duplicate
|
|
109
|
-
const schema = state.schemas.find((s) => s.
|
|
112
|
+
const schema = state.schemas.find((s) => s.id === name);
|
|
110
113
|
if (!schema) return;
|
|
111
|
-
state.schemaFullname = schema.
|
|
114
|
+
state.schemaFullname = schema.id;
|
|
112
115
|
state.fieldOptions = schema.fields.map((f) => f.name);
|
|
113
116
|
state.fieldName = null; // reset field for external injection
|
|
114
117
|
lastAppliedExternal = name;
|
fastapi_voyager/web/index.html
CHANGED
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
</div>
|
|
198
198
|
</div>
|
|
199
199
|
</div>
|
|
200
|
-
<div style="flex: 1 1 auto; min-height:
|
|
200
|
+
<div style="flex: 1 1 auto; min-height: 10px;">
|
|
201
201
|
<q-splitter
|
|
202
202
|
v-model="state.splitter"
|
|
203
203
|
unit="px"
|
|
@@ -323,7 +323,6 @@
|
|
|
323
323
|
<route-code-display
|
|
324
324
|
:route-id="routeCodeId"
|
|
325
325
|
:model-value="showRouteCode"
|
|
326
|
-
:routes="state.routeItems"
|
|
327
326
|
@close="showRouteCode = false"
|
|
328
327
|
/>
|
|
329
328
|
</q-dialog>
|
fastapi_voyager/web/vue-main.js
CHANGED
|
@@ -9,13 +9,9 @@ const app = createApp({
|
|
|
9
9
|
setup() {
|
|
10
10
|
const state = reactive({
|
|
11
11
|
// options and selections
|
|
12
|
-
tag: null,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
routeOptions: [], // [{ label, value }]
|
|
16
|
-
schemaFullname: null,
|
|
17
|
-
schemaOptions: [], // [{ label, value }]
|
|
18
|
-
routeItems: {}, // { id: { label, value } }
|
|
12
|
+
tag: null, // picked tag
|
|
13
|
+
routeId: null, // picked route
|
|
14
|
+
schemaId: null, // picked schema
|
|
19
15
|
showFields: "object",
|
|
20
16
|
fieldOptions: [
|
|
21
17
|
{ label: "No fields", value: "single" },
|
|
@@ -26,17 +22,17 @@ const app = createApp({
|
|
|
26
22
|
hidePrimitiveRoute: false,
|
|
27
23
|
generating: false,
|
|
28
24
|
rawTags: [], // [{ name, routes: [{ id, name }] }]
|
|
29
|
-
rawSchemas:
|
|
30
|
-
rawSchemasFull:
|
|
25
|
+
rawSchemas: new Set(), // [{ name, id }]
|
|
26
|
+
rawSchemasFull: {}, // full schemas dict: { [schema.id]: schema }
|
|
31
27
|
initializing: true,
|
|
32
28
|
// Splitter size (left panel width in px)
|
|
33
29
|
splitter: 300,
|
|
34
30
|
});
|
|
31
|
+
|
|
35
32
|
const showDetail = ref(false);
|
|
36
33
|
const showSchemaFieldFilter = ref(false);
|
|
37
34
|
const showSchemaCode = ref(false);
|
|
38
35
|
const showRouteCode = ref(false);
|
|
39
|
-
// Dump/Import dialogs and rendered graph dialog
|
|
40
36
|
const showDumpDialog = ref(false);
|
|
41
37
|
const dumpJson = ref("");
|
|
42
38
|
const showImportDialog = ref(false);
|
|
@@ -47,6 +43,7 @@ const app = createApp({
|
|
|
47
43
|
const schemaFieldFilterSchema = ref(null); // external schemaName for schema-field-filter
|
|
48
44
|
const schemaCodeName = ref("");
|
|
49
45
|
const routeCodeId = ref("");
|
|
46
|
+
|
|
50
47
|
function openDetail() {
|
|
51
48
|
showDetail.value = true;
|
|
52
49
|
}
|
|
@@ -54,47 +51,18 @@ const app = createApp({
|
|
|
54
51
|
showDetail.value = false;
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
function onFilterTags(val, update) {
|
|
58
|
-
const normalized = (val || "").toLowerCase();
|
|
59
|
-
update(() => {
|
|
60
|
-
if (!normalized) {
|
|
61
|
-
state.tagOptions = state.rawTags.map((t) => t.name);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
state.tagOptions = state.rawTags
|
|
65
|
-
.map((t) => t.name)
|
|
66
|
-
.filter((n) => n.toLowerCase().includes(normalized));
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function onFilterSchemas(val, update) {
|
|
71
|
-
const normalized = (val || "").toLowerCase();
|
|
72
|
-
update(() => {
|
|
73
|
-
const makeLabel = (s) => `${s.name} (${s.fullname})`;
|
|
74
|
-
let list = state.rawSchemas.map((s) => ({
|
|
75
|
-
label: makeLabel(s),
|
|
76
|
-
value: s.fullname,
|
|
77
|
-
}));
|
|
78
|
-
if (normalized) {
|
|
79
|
-
list = list.filter((opt) =>
|
|
80
|
-
opt.label.toLowerCase().includes(normalized)
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
state.schemaOptions = list;
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
54
|
async function loadInitial() {
|
|
88
55
|
state.initializing = true;
|
|
89
56
|
try {
|
|
90
57
|
const res = await fetch("dot");
|
|
91
58
|
const data = await res.json();
|
|
92
59
|
state.rawTags = Array.isArray(data.tags) ? data.tags : [];
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
60
|
+
const schemasArr = Array.isArray(data.schemas) ? data.schemas : [];
|
|
61
|
+
// Build dict keyed by id for faster lookups and simpler prop passing
|
|
62
|
+
state.rawSchemasFull = Object.fromEntries(
|
|
63
|
+
schemasArr.map((s) => [s.id, s])
|
|
64
|
+
);
|
|
65
|
+
state.rawSchemas = new Set(Object.keys(state.rawSchemasFull));
|
|
98
66
|
state.routeItems = data.tags
|
|
99
67
|
.map((t) => t.routes)
|
|
100
68
|
.flat()
|
|
@@ -103,13 +71,7 @@ const app = createApp({
|
|
|
103
71
|
return acc;
|
|
104
72
|
}, {});
|
|
105
73
|
|
|
106
|
-
state.tagOptions = state.rawTags.map((t) => t.name);
|
|
107
|
-
state.schemaOptions = state.rawSchemas.map((s) => ({
|
|
108
|
-
label: `${s.name} (${s.fullname})`,
|
|
109
|
-
value: s.fullname,
|
|
110
|
-
}));
|
|
111
74
|
// default route options placeholder
|
|
112
|
-
state.routeOptions = [];
|
|
113
75
|
} catch (e) {
|
|
114
76
|
console.error("Initial load failed", e);
|
|
115
77
|
} finally {
|
|
@@ -117,16 +79,16 @@ const app = createApp({
|
|
|
117
79
|
}
|
|
118
80
|
}
|
|
119
81
|
|
|
120
|
-
async function onGenerate(resetZoom=true) {
|
|
82
|
+
async function onGenerate(resetZoom = true) {
|
|
121
83
|
state.generating = true;
|
|
122
84
|
try {
|
|
123
85
|
const payload = {
|
|
124
86
|
tags: state.tag ? [state.tag] : null,
|
|
125
|
-
schema_name: state.
|
|
87
|
+
schema_name: state.schemaId || null,
|
|
126
88
|
route_name: state.routeId || null,
|
|
127
89
|
show_fields: state.showFields,
|
|
128
90
|
brief: state.brief,
|
|
129
|
-
hide_primitive_route: state.hidePrimitiveRoute
|
|
91
|
+
hide_primitive_route: state.hidePrimitiveRoute,
|
|
130
92
|
};
|
|
131
93
|
|
|
132
94
|
const res = await fetch("dot", {
|
|
@@ -138,21 +100,20 @@ const app = createApp({
|
|
|
138
100
|
|
|
139
101
|
// create graph instance once
|
|
140
102
|
const graphUI = new GraphUI("#graph", {
|
|
141
|
-
onSchemaClick: (
|
|
142
|
-
if (state.rawSchemas.
|
|
143
|
-
schemaFieldFilterSchema.value =
|
|
103
|
+
onSchemaClick: (id) => {
|
|
104
|
+
if (state.rawSchemas.has(id)) {
|
|
105
|
+
schemaFieldFilterSchema.value = id;
|
|
144
106
|
showSchemaFieldFilter.value = true;
|
|
145
107
|
}
|
|
146
108
|
},
|
|
147
|
-
onSchemaAltClick: (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
schemaCodeName.value = name;
|
|
109
|
+
onSchemaAltClick: (id) => {
|
|
110
|
+
if (state.rawSchemas.has(id)) {
|
|
111
|
+
schemaCodeName.value = id;
|
|
151
112
|
showSchemaCode.value = true;
|
|
152
113
|
return;
|
|
153
114
|
}
|
|
154
|
-
if (
|
|
155
|
-
routeCodeId.value =
|
|
115
|
+
if (id in state.routeItems) {
|
|
116
|
+
routeCodeId.value = id;
|
|
156
117
|
showRouteCode.value = true;
|
|
157
118
|
return;
|
|
158
119
|
}
|
|
@@ -171,7 +132,7 @@ const app = createApp({
|
|
|
171
132
|
try {
|
|
172
133
|
const payload = {
|
|
173
134
|
tags: state.tag ? [state.tag] : null,
|
|
174
|
-
schema_name: state.
|
|
135
|
+
schema_name: state.schemaId || null,
|
|
175
136
|
route_name: state.routeId || null,
|
|
176
137
|
show_fields: state.showFields,
|
|
177
138
|
brief: state.brief,
|
|
@@ -232,16 +193,16 @@ const app = createApp({
|
|
|
232
193
|
async function onReset() {
|
|
233
194
|
state.tag = null;
|
|
234
195
|
state.routeId = "";
|
|
235
|
-
state.
|
|
196
|
+
state.schemaId = null;
|
|
236
197
|
// state.showFields = "object";
|
|
237
198
|
state.brief = false;
|
|
238
|
-
onGenerate()
|
|
199
|
+
onGenerate();
|
|
239
200
|
}
|
|
240
201
|
|
|
241
202
|
function toggleTag(tagName, expanded = null) {
|
|
242
203
|
if (expanded === true) {
|
|
243
204
|
state.tag = tagName;
|
|
244
|
-
state.routeId =
|
|
205
|
+
state.routeId = "";
|
|
245
206
|
onGenerate();
|
|
246
207
|
return;
|
|
247
208
|
}
|
|
@@ -249,26 +210,26 @@ const app = createApp({
|
|
|
249
210
|
|
|
250
211
|
function selectRoute(routeId) {
|
|
251
212
|
if (state.routeId === routeId) {
|
|
252
|
-
state.routeId =
|
|
213
|
+
state.routeId = "";
|
|
253
214
|
} else {
|
|
254
|
-
state.routeId = routeId
|
|
215
|
+
state.routeId = routeId;
|
|
255
216
|
}
|
|
256
|
-
onGenerate()
|
|
217
|
+
onGenerate();
|
|
257
218
|
}
|
|
258
219
|
|
|
259
220
|
function toggleShowField(field) {
|
|
260
221
|
state.showFields = field;
|
|
261
|
-
onGenerate(false)
|
|
222
|
+
onGenerate(false);
|
|
262
223
|
}
|
|
263
224
|
|
|
264
225
|
function toggleBrief(val) {
|
|
265
226
|
state.brief = val;
|
|
266
|
-
onGenerate()
|
|
227
|
+
onGenerate();
|
|
267
228
|
}
|
|
268
|
-
|
|
229
|
+
|
|
269
230
|
function toggleHidePrimitiveRoute(val) {
|
|
270
231
|
state.hidePrimitiveRoute = val;
|
|
271
|
-
onGenerate(false)
|
|
232
|
+
onGenerate(false);
|
|
272
233
|
}
|
|
273
234
|
|
|
274
235
|
onMounted(async () => {
|
|
@@ -281,8 +242,6 @@ const app = createApp({
|
|
|
281
242
|
toggleBrief,
|
|
282
243
|
toggleHidePrimitiveRoute,
|
|
283
244
|
selectRoute,
|
|
284
|
-
onFilterTags,
|
|
285
|
-
onFilterSchemas,
|
|
286
245
|
onGenerate,
|
|
287
246
|
onReset,
|
|
288
247
|
showDetail,
|
|
@@ -308,14 +267,14 @@ const app = createApp({
|
|
|
308
267
|
// render graph dialog
|
|
309
268
|
showRenderGraph,
|
|
310
269
|
renderCoreData,
|
|
311
|
-
toggleShowField
|
|
270
|
+
toggleShowField,
|
|
312
271
|
};
|
|
313
272
|
},
|
|
314
273
|
});
|
|
315
274
|
app.use(window.Quasar);
|
|
316
275
|
// Set Quasar primary theme color to green
|
|
317
|
-
if (window.Quasar && typeof window.Quasar.setCssVar ===
|
|
318
|
-
window.Quasar.setCssVar(
|
|
276
|
+
if (window.Quasar && typeof window.Quasar.setCssVar === "function") {
|
|
277
|
+
window.Quasar.setCssVar("primary", "#009485");
|
|
319
278
|
}
|
|
320
279
|
app.component("schema-field-filter", SchemaFieldFilter);
|
|
321
280
|
app.component("schema-code-display", SchemaCodeDisplay);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-voyager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: Visualize FastAPI application's routing tree and dependencies
|
|
5
5
|
Project-URL: Homepage, https://github.com/allmonday/fastapi-voyager
|
|
6
6
|
Project-URL: Source, https://github.com/allmonday/fastapi-voyager
|
|
@@ -28,13 +28,12 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|
[](https://pypi.python.org/pypi/fastapi-voyager)
|
|
29
29
|

|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
> This repo is still in early stage, currently it supports pydantic v2 only, previous name: fastapi-router-viz
|
|
31
|
+
> This repo is still in early stage, it supports pydantic v2 only
|
|
34
32
|
|
|
35
|
-
Inspect your API interactively
|
|
33
|
+
Inspect your API interactively!
|
|
36
34
|
|
|
37
|
-
<
|
|
35
|
+
<p align="center"><img src="./voyager.jpg" alt="" /></p>
|
|
36
|
+
<p align="center"><a target="_blank" rel="" href="https://www.youtube.com/watch?v=PGlbQq1M-n8"><img src="http://img.youtube.com/vi/PGlbQq1M-n8/0.jpg" alt="" style="max-width: 100%;"></a></p>
|
|
38
37
|
|
|
39
38
|
## Installation
|
|
40
39
|
|
|
@@ -48,11 +47,6 @@ uv add fastapi-voyager
|
|
|
48
47
|
voyager -m path.to.your.app.module --server
|
|
49
48
|
```
|
|
50
49
|
|
|
51
|
-
## Dependencies
|
|
52
|
-
|
|
53
|
-
- FastAPI
|
|
54
|
-
- [pydantic-resolve](https://github.com/allmonday/pydantic-resolve)
|
|
55
|
-
|
|
56
50
|
|
|
57
51
|
## Feature
|
|
58
52
|
|
|
@@ -75,9 +69,9 @@ voyager -m tests.demo
|
|
|
75
69
|
```
|
|
76
70
|
|
|
77
71
|
### generate the graph
|
|
78
|
-
after initialization, pick tag, rotue
|
|
72
|
+
after initialization, pick tag, rotue to render graph
|
|
79
73
|
|
|
80
|
-
<img width="
|
|
74
|
+
<img width="1628" height="765" alt="image" src="https://github.com/user-attachments/assets/b4712f82-e754-453b-aa69-24c932b8f48f" />
|
|
81
75
|
|
|
82
76
|
### highlight
|
|
83
77
|
click a node to highlight it's upperstream and downstream nodes. figure out the related models of one page, or homw many pages are related with one model.
|
|
@@ -85,9 +79,9 @@ click a node to highlight it's upperstream and downstream nodes. figure out the
|
|
|
85
79
|
<img width="1485" height="616" alt="image" style="border: 1px solid #aaa" src="https://github.com/user-attachments/assets/70c4095f-86c7-45da-a6f0-fd41ac645813" />
|
|
86
80
|
|
|
87
81
|
### filter related nodes
|
|
88
|
-
`shift` click a node to check related node, pick a field to narrow the result.
|
|
82
|
+
`shift` click a node to check related node, pick a field to narrow the result, picked node is marked as red.
|
|
89
83
|
|
|
90
|
-
<img width="
|
|
84
|
+
<img width="1423" height="552" alt="image" src="https://github.com/user-attachments/assets/468a058d-afa1-4601-a7c5-c6aad6a8a557" />
|
|
91
85
|
|
|
92
86
|
### view source code
|
|
93
87
|
`alt` click a node to show source code or open file in vscode.
|
|
@@ -105,10 +99,10 @@ click a node to highlight it's upperstream and downstream nodes. figure out the
|
|
|
105
99
|
|
|
106
100
|
```python
|
|
107
101
|
from fastapi import FastAPI
|
|
108
|
-
from fastapi_voyager
|
|
102
|
+
from fastapi_voyager import create_voyager
|
|
109
103
|
from tests.demo import app
|
|
110
104
|
|
|
111
|
-
app.mount('/voyager',
|
|
105
|
+
app.mount('/voyager', create_voyager(
|
|
112
106
|
app,
|
|
113
107
|
module_color={"tests.service": "red"},
|
|
114
108
|
module_prefix="tests.service"))
|
|
@@ -164,7 +158,25 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
|
|
|
164
158
|
|
|
165
159
|
## Plan before v1.0
|
|
166
160
|
|
|
167
|
-
|
|
161
|
+
|
|
162
|
+
### backlog
|
|
163
|
+
- [ ] user can generate nodes/edges manually and connect to generated ones
|
|
164
|
+
- [ ] add owner
|
|
165
|
+
- [ ] add extra info for schema
|
|
166
|
+
- [ ] fixed left/right bar show field information
|
|
167
|
+
- [ ] display standard ER diagram `hard`
|
|
168
|
+
- [ ] display potential invalid links
|
|
169
|
+
- [ ] support dataclass (pending)
|
|
170
|
+
|
|
171
|
+
### in analysis
|
|
172
|
+
- [ ] click field to highlight links
|
|
173
|
+
- [ ] animation effect for edges
|
|
174
|
+
- [ ] customrized right click panel
|
|
175
|
+
- [ ] show own dependencies
|
|
176
|
+
- [ ] clean up fe code
|
|
177
|
+
|
|
178
|
+
### plan:
|
|
179
|
+
#### <0.9:
|
|
168
180
|
- [x] group schemas by module hierarchy
|
|
169
181
|
- [x] module-based coloring via Analytics(module_color={...})
|
|
170
182
|
- [x] view in web browser
|
|
@@ -183,49 +195,80 @@ features:
|
|
|
183
195
|
- [x] add tooltips
|
|
184
196
|
- [x] route
|
|
185
197
|
- [x] group routes by module hierarchy
|
|
186
|
-
- [
|
|
187
|
-
- [
|
|
188
|
-
- [ ] click field to highlight links
|
|
189
|
-
- [ ] user can generate nodes/edges manually and connect to generated ones
|
|
190
|
-
- [ ] add owner
|
|
191
|
-
- [ ] add extra info for schema
|
|
192
|
-
- [ ] ui optimization
|
|
193
|
-
- [ ] fixed left/right bar show field information
|
|
194
|
-
- [x] fixed left bar show tag/ route
|
|
195
|
-
- [ ] display standard ER diagram `difference`
|
|
196
|
-
- [ ] 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
|
|
198
|
+
- [x] add response_model in route
|
|
199
|
+
- [x] fixed left bar show tag/ route
|
|
201
200
|
- [x] export voyager core data into json (for better debugging)
|
|
202
201
|
- [x] add api to rebuild core data from json, and render it
|
|
203
202
|
- [x] fix Generic case `test_generic.py`
|
|
204
|
-
- [
|
|
205
|
-
- [ ] add http method for route
|
|
206
|
-
|
|
207
|
-
bugs & non feature:
|
|
203
|
+
- [x] show tips for routes not return pydantic type.
|
|
208
204
|
- [x] fix duplicated link from class and parent class, it also break clicking highlight
|
|
205
|
+
- [x] refactor: abstract render module
|
|
206
|
+
|
|
207
|
+
#### 0.9
|
|
208
|
+
- [x] refactor: server.py
|
|
209
|
+
- [x] rename create_app_with_fastapi -> create_voyager
|
|
210
|
+
- [x] add doc for parameters
|
|
211
|
+
- [x] improve initialization time cost
|
|
212
|
+
- [x] query route / schema info through realtime api
|
|
213
|
+
- [x] adjust fe
|
|
214
|
+
|
|
215
|
+
#### 0.10
|
|
216
|
+
- [ ] support opening route in swagger
|
|
217
|
+
- config docs path
|
|
218
|
+
- [ ] add http method for route
|
|
219
|
+
- [ ] enable/disable module cluster (may save space)
|
|
220
|
+
- [ ] logging information
|
|
209
221
|
- [ ] add tests
|
|
210
|
-
- [
|
|
211
|
-
|
|
222
|
+
- [ ] hide brief mode if not configured
|
|
223
|
+
- [ ] optimize static resource
|
|
224
|
+
- [ ] show route count in tag expansion item
|
|
225
|
+
- [ ] route list show have a max height to trigger scrollable
|
|
226
|
+
- [ ] fix layout issue when rendering huge graph
|
|
227
|
+
|
|
228
|
+
#### 0.11
|
|
229
|
+
- [ ] improve user experience
|
|
230
|
+
- double click to show detail
|
|
231
|
+
- improve search dialog
|
|
232
|
+
|
|
233
|
+
#### 0.12
|
|
234
|
+
- [ ] integration with pydantic-resolve
|
|
235
|
+
- [ ] show hint for resolve, post fields
|
|
236
|
+
- [ ] display loader as edges
|
|
237
|
+
|
|
212
238
|
|
|
213
239
|
## Using with pydantic-resolve
|
|
214
240
|
|
|
241
|
+
WIP: ...
|
|
242
|
+
|
|
215
243
|
pydantic-resolve's @ensure_subset decorator is helpful to pick fields from `source class` in safe.
|
|
216
244
|
|
|
217
|
-
TODO: ...
|
|
218
245
|
|
|
219
246
|
|
|
220
247
|
## Credits
|
|
221
248
|
|
|
222
|
-
- https://apis.guru/graphql-voyager/, for inspiration.
|
|
223
|
-
- https://github.com/tintinweb/vscode-interactive-graphviz, for web visualization.
|
|
249
|
+
- https://apis.guru/graphql-voyager/, thanks for inspiration.
|
|
250
|
+
- https://github.com/tintinweb/vscode-interactive-graphviz, thanks for web visualization.
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
## Dependencies
|
|
254
|
+
|
|
255
|
+
- FastAPI
|
|
256
|
+
- [pydantic-resolve](https://github.com/allmonday/pydantic-resolve)
|
|
257
|
+
- Quasar
|
|
224
258
|
|
|
225
259
|
|
|
226
260
|
## Changelog
|
|
227
261
|
|
|
262
|
+
- 0.9:
|
|
263
|
+
- 0.9.2:
|
|
264
|
+
- fix: missing fields in schema detail panel
|
|
265
|
+
- optimization: clean up fe codes.
|
|
266
|
+
- 0.9.1:
|
|
267
|
+
- api change: from `create_app_with_fastapi` to `create_voyager`, and expose as `from fastapi_voyager import create_voyager`
|
|
268
|
+
- optimization: lazy load vscode link and source code, speed up the initialization.
|
|
228
269
|
- 0.8:
|
|
270
|
+
- 0.8.3
|
|
271
|
+
- upgrade theme
|
|
229
272
|
- 0.8.2
|
|
230
273
|
- fix silly typo.
|
|
231
274
|
- 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=
|
|
9
|
+
fastapi_voyager/version.py,sha256=Su4VudgwOkDWEBzNahJ3LX1SbIA7uezku9g1zsUddHU,48
|
|
10
|
+
fastapi_voyager/voyager.py,sha256=pXvA3ye5Jq6aJ6YrZFbTHjJ9MzdydqWllIgY8RFK_4A,10793
|
|
11
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=pmctp4OizRd8ETKEI0vl0Seu9nIQklfnbFDDyztFoy4,14264
|
|
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=ZRJQBV54YqoKkLI2a8WHF6uYeFUpqwBl-lykkaYxCXI,8182
|
|
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=Pdg8JX_yHhoUUGSqgRs_zTciIfw5wYzG6-kQfHtdWFU,3956
|
|
20
|
+
fastapi_voyager/web/component/schema-code-display.js,sha256=Aa_mXxmNgIbImoT1_zuzsPgbPq8QaakUAzNWTxHxM58,6584
|
|
21
|
+
fastapi_voyager/web/component/schema-field-filter.js,sha256=PR8d2_u1UiSLrS5zqAvY2UR8LbvjBqiUDt71tm1DJTs,6345
|
|
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.2.dist-info/METADATA,sha256=k_l0KTvYeGwMhRPdulVlob3_q-uZ9UTEHo_KLeXTHsQ,9221
|
|
30
|
+
fastapi_voyager-0.9.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
31
|
+
fastapi_voyager-0.9.2.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
|
|
32
|
+
fastapi_voyager-0.9.2.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
|
|
33
|
+
fastapi_voyager-0.9.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|