fastapi-voyager 0.6.2__py3-none-any.whl → 0.7.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 +14 -1
- fastapi_voyager/filter.py +84 -1
- fastapi_voyager/server.py +8 -2
- fastapi_voyager/version.py +1 -1
- fastapi_voyager/voyager.py +13 -1
- fastapi_voyager/web/index.html +8 -0
- fastapi_voyager/web/vue-main.js +4 -0
- {fastapi_voyager-0.6.2.dist-info → fastapi_voyager-0.7.1.dist-info}/METADATA +15 -13
- {fastapi_voyager-0.6.2.dist-info → fastapi_voyager-0.7.1.dist-info}/RECORD +12 -12
- {fastapi_voyager-0.6.2.dist-info → fastapi_voyager-0.7.1.dist-info}/WHEEL +0 -0
- {fastapi_voyager-0.6.2.dist-info → fastapi_voyager-0.7.1.dist-info}/entry_points.txt +0 -0
- {fastapi_voyager-0.6.2.dist-info → fastapi_voyager-0.7.1.dist-info}/licenses/LICENSE +0 -0
fastapi_voyager/cli.py
CHANGED
|
@@ -175,6 +175,12 @@ Examples:
|
|
|
175
175
|
default="127.0.0.1",
|
|
176
176
|
help="Host/IP for the preview server when --server is used (default: 127.0.0.1). Use 0.0.0.0 to listen on all interfaces."
|
|
177
177
|
)
|
|
178
|
+
parser.add_argument(
|
|
179
|
+
"--module_prefix",
|
|
180
|
+
type=str,
|
|
181
|
+
default=None,
|
|
182
|
+
help="Prefix routes with module name when rendering brief view (only valid with --server)"
|
|
183
|
+
)
|
|
178
184
|
|
|
179
185
|
parser.add_argument(
|
|
180
186
|
"--version", "-v",
|
|
@@ -233,6 +239,9 @@ Examples:
|
|
|
233
239
|
if not any(mc.startswith(key + ":") for mc in existing):
|
|
234
240
|
args.module_color = (args.module_color or []) + [d]
|
|
235
241
|
|
|
242
|
+
if args.module_prefix and not args.server:
|
|
243
|
+
parser.error("--module_prefix can only be used together with --server")
|
|
244
|
+
|
|
236
245
|
# Validate required target if not demo
|
|
237
246
|
if not args.demo and not (args.module_name or args.module):
|
|
238
247
|
parser.error("You must provide a module file, -m module name, or use --demo")
|
|
@@ -272,7 +281,11 @@ Examples:
|
|
|
272
281
|
except ImportError:
|
|
273
282
|
print("Error: uvicorn is required to run the server. Install via 'pip install uvicorn' or 'uv add uvicorn'.")
|
|
274
283
|
sys.exit(1)
|
|
275
|
-
app_server = viz_server.create_app_with_fastapi(
|
|
284
|
+
app_server = viz_server.create_app_with_fastapi(
|
|
285
|
+
app,
|
|
286
|
+
module_color=module_color,
|
|
287
|
+
module_prefix=args.module_prefix,
|
|
288
|
+
)
|
|
276
289
|
print(f"Starting preview server at http://{args.host}:{args.port} ... (Ctrl+C to stop)")
|
|
277
290
|
uvicorn.run(app_server, host=args.host, port=args.port)
|
|
278
291
|
else:
|
fastapi_voyager/filter.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
from collections import deque
|
|
2
3
|
from typing import Tuple
|
|
3
|
-
from fastapi_voyager.type import Tag, Route, SchemaNode, Link
|
|
4
|
+
from fastapi_voyager.type import Tag, Route, SchemaNode, Link, PK
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def filter_graph(
|
|
@@ -103,3 +104,85 @@ def filter_graph(
|
|
|
103
104
|
_routes = [r for r in routes if r.id in included_ids]
|
|
104
105
|
|
|
105
106
|
return _tags, _routes, _nodes, _links
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def filter_subgraph(
|
|
110
|
+
*,
|
|
111
|
+
tags: list[Tag],
|
|
112
|
+
routes: list[Route],
|
|
113
|
+
links: list[Link],
|
|
114
|
+
nodes: list[SchemaNode],
|
|
115
|
+
module_prefix: str
|
|
116
|
+
) -> Tuple[list[Tag], list[Route], list[SchemaNode], list[Link]]:
|
|
117
|
+
"""Collapse schema graph so routes link directly to nodes whose module matches ``module_prefix``.
|
|
118
|
+
|
|
119
|
+
The routine keeps tag→route links untouched, prunes schema nodes whose module does not start
|
|
120
|
+
with ``module_prefix``, and merges the remaining schema relationships so each route connects
|
|
121
|
+
directly to the surviving schema nodes. Traversal stops once a qualifying node is reached and
|
|
122
|
+
guards against cycles in the schema graph.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
if not module_prefix:
|
|
126
|
+
# empty prefix keeps existing graph structure, so simply reuse incoming data
|
|
127
|
+
return tags, routes, nodes, [lk for lk in links if lk.type in ("tag_route", "route_to_schema")]
|
|
128
|
+
|
|
129
|
+
route_links = [lk for lk in links if lk.type == "route_to_schema"]
|
|
130
|
+
schema_links = [lk for lk in links if lk.type in {"schema", "parent", "subset"}]
|
|
131
|
+
tag_route_links = [lk for lk in links if lk.type == "tag_route"]
|
|
132
|
+
|
|
133
|
+
node_lookup: dict[str, SchemaNode] = {node.id: node for node in nodes}
|
|
134
|
+
|
|
135
|
+
filtered_nodes = [node for node in nodes if node_lookup[node.id].module.startswith(module_prefix)]
|
|
136
|
+
filtered_node_ids = {node.id for node in filtered_nodes}
|
|
137
|
+
|
|
138
|
+
adjacency: dict[str, list[str]] = {}
|
|
139
|
+
for link in schema_links:
|
|
140
|
+
if link.source_origin not in node_lookup or link.target_origin not in node_lookup:
|
|
141
|
+
continue
|
|
142
|
+
adjacency.setdefault(link.source_origin, [])
|
|
143
|
+
if link.target_origin not in adjacency[link.source_origin]:
|
|
144
|
+
adjacency[link.source_origin].append(link.target_origin)
|
|
145
|
+
|
|
146
|
+
merged_links: list[Link] = []
|
|
147
|
+
seen_pairs: set[tuple[str, str]] = set()
|
|
148
|
+
|
|
149
|
+
for link in route_links:
|
|
150
|
+
route_id = link.source_origin
|
|
151
|
+
start_node_id = link.target_origin
|
|
152
|
+
if route_id is None or start_node_id is None:
|
|
153
|
+
continue
|
|
154
|
+
if start_node_id not in node_lookup:
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
visited: set[str] = set()
|
|
158
|
+
queue: deque[str] = deque([start_node_id])
|
|
159
|
+
|
|
160
|
+
while queue:
|
|
161
|
+
current = queue.popleft()
|
|
162
|
+
if current in visited:
|
|
163
|
+
continue
|
|
164
|
+
visited.add(current)
|
|
165
|
+
|
|
166
|
+
if current in filtered_node_ids:
|
|
167
|
+
key = (route_id, current)
|
|
168
|
+
if key not in seen_pairs:
|
|
169
|
+
seen_pairs.add(key)
|
|
170
|
+
merged_links.append(
|
|
171
|
+
Link(
|
|
172
|
+
source=link.source,
|
|
173
|
+
source_origin=route_id,
|
|
174
|
+
target=f"{current}::{PK}",
|
|
175
|
+
target_origin=current,
|
|
176
|
+
type="route_to_schema",
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
# stop traversing past a qualifying node
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
for next_node in adjacency.get(current, () ):
|
|
183
|
+
if next_node not in visited:
|
|
184
|
+
queue.append(next_node)
|
|
185
|
+
|
|
186
|
+
filtered_links = tag_route_links + merged_links
|
|
187
|
+
|
|
188
|
+
return tags, routes, filtered_nodes, filtered_links
|
fastapi_voyager/server.py
CHANGED
|
@@ -32,10 +32,12 @@ class Payload(BaseModel):
|
|
|
32
32
|
route_name: Optional[str] = None
|
|
33
33
|
show_fields: str = 'object'
|
|
34
34
|
show_meta: bool = False
|
|
35
|
+
brief: bool = False
|
|
35
36
|
|
|
36
37
|
def create_route(
|
|
37
38
|
target_app: FastAPI,
|
|
38
39
|
module_color: dict[str, str] | None = None,
|
|
40
|
+
module_prefix: Optional[str] = None,
|
|
39
41
|
):
|
|
40
42
|
router = APIRouter(tags=['fastapi-voyager'])
|
|
41
43
|
|
|
@@ -73,7 +75,10 @@ def create_route(
|
|
|
73
75
|
load_meta=False,
|
|
74
76
|
)
|
|
75
77
|
voyager.analysis(target_app)
|
|
76
|
-
|
|
78
|
+
if payload.brief:
|
|
79
|
+
return voyager.render_brief_dot(module_prefix=module_prefix)
|
|
80
|
+
else:
|
|
81
|
+
return voyager.render_dot()
|
|
77
82
|
|
|
78
83
|
@router.post("/dot-core-data", response_model=CoreData)
|
|
79
84
|
def get_filtered_dot_core_data(payload: Payload) -> str:
|
|
@@ -117,8 +122,9 @@ def create_app_with_fastapi(
|
|
|
117
122
|
target_app: FastAPI,
|
|
118
123
|
module_color: dict[str, str] | None = None,
|
|
119
124
|
gzip_minimum_size: int | None = 500,
|
|
125
|
+
module_prefix: Optional[str] = None,
|
|
120
126
|
) -> FastAPI:
|
|
121
|
-
router = create_route(target_app, module_color=module_color)
|
|
127
|
+
router = create_route(target_app, module_color=module_color, module_prefix=module_prefix)
|
|
122
128
|
|
|
123
129
|
app = FastAPI(title="fastapi-voyager demo server")
|
|
124
130
|
if gzip_minimum_size is not None and gzip_minimum_size >= 0:
|
fastapi_voyager/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.7.1"
|
fastapi_voyager/voyager.py
CHANGED
|
@@ -12,7 +12,7 @@ from fastapi_voyager.type_helper import (
|
|
|
12
12
|
)
|
|
13
13
|
from pydantic import BaseModel
|
|
14
14
|
from fastapi_voyager.type import Route, SchemaNode, Link, Tag, LinkType, FieldType, PK, CoreData
|
|
15
|
-
from fastapi_voyager.filter import filter_graph
|
|
15
|
+
from fastapi_voyager.filter import filter_graph, filter_subgraph
|
|
16
16
|
from fastapi_voyager.render import Renderer
|
|
17
17
|
import pydantic_resolve.constant as const
|
|
18
18
|
|
|
@@ -271,4 +271,16 @@ class Voyager:
|
|
|
271
271
|
node_set=self.node_set,
|
|
272
272
|
)
|
|
273
273
|
renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema)
|
|
274
|
+
return renderer.render_dot(_tags, _routes, _nodes, _links)
|
|
275
|
+
|
|
276
|
+
def render_brief_dot(self, module_prefix: str | None = None):
|
|
277
|
+
print(module_prefix)
|
|
278
|
+
_tags, _routes, _nodes, _links = filter_subgraph(
|
|
279
|
+
module_prefix=module_prefix,
|
|
280
|
+
tags=self.tags,
|
|
281
|
+
routes=self.routes,
|
|
282
|
+
nodes=self.nodes,
|
|
283
|
+
links=self.links,
|
|
284
|
+
)
|
|
285
|
+
renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=None)
|
|
274
286
|
return renderer.render_dot(_tags, _routes, _nodes, _links)
|
fastapi_voyager/web/index.html
CHANGED
fastapi_voyager/web/vue-main.js
CHANGED
|
@@ -22,6 +22,7 @@ const app = createApp({
|
|
|
22
22
|
{ label: "Object fields", value: "object" },
|
|
23
23
|
{ label: "All fields", value: "all" },
|
|
24
24
|
],
|
|
25
|
+
brief: false,
|
|
25
26
|
generating: false,
|
|
26
27
|
rawTags: [], // [{ name, routes: [{ id, name }] }]
|
|
27
28
|
rawSchemas: [], // [{ name, fullname }]
|
|
@@ -132,6 +133,7 @@ const app = createApp({
|
|
|
132
133
|
schema_name: state.schemaFullname || null,
|
|
133
134
|
route_name: state.routeId || null,
|
|
134
135
|
show_fields: state.showFields,
|
|
136
|
+
brief: state.brief,
|
|
135
137
|
};
|
|
136
138
|
|
|
137
139
|
const res = await fetch("dot", {
|
|
@@ -179,6 +181,7 @@ const app = createApp({
|
|
|
179
181
|
schema_name: state.schemaFullname || null,
|
|
180
182
|
route_name: state.routeId || null,
|
|
181
183
|
show_fields: state.showFields,
|
|
184
|
+
brief: state.brief,
|
|
182
185
|
};
|
|
183
186
|
const res = await fetch("dot-core-data", {
|
|
184
187
|
method: "POST",
|
|
@@ -238,6 +241,7 @@ const app = createApp({
|
|
|
238
241
|
state.routeId = "";
|
|
239
242
|
state.schemaFullname = null;
|
|
240
243
|
state.showFields = "object";
|
|
244
|
+
state.brief = false;
|
|
241
245
|
await loadInitial();
|
|
242
246
|
}
|
|
243
247
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-voyager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -146,7 +146,7 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
|
|
|
146
146
|
|
|
147
147
|
## Plan before v1.0
|
|
148
148
|
|
|
149
|
-
features
|
|
149
|
+
features:
|
|
150
150
|
- [x] group schemas by module hierarchy
|
|
151
151
|
- [x] module-based coloring via Analytics(module_color={...})
|
|
152
152
|
- [x] view in web browser
|
|
@@ -162,31 +162,31 @@ 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
|
-
- [
|
|
166
|
-
- [
|
|
167
|
-
- [
|
|
165
|
+
- [x] add tooltips
|
|
166
|
+
- [x] route
|
|
167
|
+
- [x] group routes by module hierarchy
|
|
168
168
|
- [ ] add response_model in route
|
|
169
|
-
- [ ] support dataclass
|
|
169
|
+
- [ ] support dataclass (pending)
|
|
170
170
|
- [ ] click field to highlight links
|
|
171
171
|
- [ ] user can generate nodes/edges manually and connect to generated ones
|
|
172
172
|
- [ ] add owner
|
|
173
173
|
- [ ] ui optimization
|
|
174
174
|
- [ ] fixed left/right bar show field information
|
|
175
|
-
- [ ] display standard ER diagram
|
|
175
|
+
- [ ] display standard ER diagram `difficult`
|
|
176
176
|
- [ ] display potential invalid links
|
|
177
177
|
- [ ] integration with pydantic-resolve
|
|
178
178
|
- [ ] show difference between resolve, post fields
|
|
179
179
|
- [x] strikethrough for excluded fields
|
|
180
180
|
- [ ] display loader as edges
|
|
181
|
+
- [x] export voyager core data into json (for better debugging)
|
|
182
|
+
- [x] add api to rebuild core data from json, and render it
|
|
183
|
+
- [x] fix Generic case `test_generic.py`
|
|
184
|
+
|
|
185
|
+
bugs & non feature:
|
|
186
|
+
- [x] fix duplicated link from class and parent class, it also break clicking highlight
|
|
181
187
|
- [ ] add tests
|
|
182
188
|
- [ ] refactor
|
|
183
189
|
- [ ] 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
|
|
186
|
-
- [x] fix Generic case `test_generic.py`
|
|
187
|
-
|
|
188
|
-
bugs:
|
|
189
|
-
- [ ] fix duplicated link from class and parent class, it also break clicking highlight
|
|
190
190
|
|
|
191
191
|
## Using with pydantic-resolve
|
|
192
192
|
|
|
@@ -203,5 +203,7 @@ TODO: ...
|
|
|
203
203
|
|
|
204
204
|
## Changelog
|
|
205
205
|
|
|
206
|
+
- 0.7.1:
|
|
207
|
+
- support brief mode, you can use `--module_prefix tests.service` to show links between routes and filtered schemas, to make the graph less complicated.
|
|
206
208
|
- 0.6.2:
|
|
207
209
|
- fix generic related issue
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
fastapi_voyager/__init__.py,sha256=E5WTV_sYs2LK8I6jzA7AuvFU5a8_vjnDseC3DMha0iQ,149
|
|
2
|
-
fastapi_voyager/cli.py,sha256=
|
|
3
|
-
fastapi_voyager/filter.py,sha256=
|
|
2
|
+
fastapi_voyager/cli.py,sha256=2eixX7mtPsZvukc4vrwQOt6XTPJgHUKIGLBy3IIC2jE,11127
|
|
3
|
+
fastapi_voyager/filter.py,sha256=pp2vOWxQVOvN2ClwYVMxHnxo_FSEbgAHO2DqynKls1k,7861
|
|
4
4
|
fastapi_voyager/module.py,sha256=Z2QHNmiLk6ZAJlm2nSmO875Q33TweSg8UxZSzIpU9zY,3499
|
|
5
5
|
fastapi_voyager/render.py,sha256=ctwad-KNbFajhgnA8OI8412s6s67UbV-dvZFXBt_Ssg,7410
|
|
6
|
-
fastapi_voyager/server.py,sha256=
|
|
6
|
+
fastapi_voyager/server.py,sha256=UT-fHggdqicIo5m3uUX86-XFhAVDLXpXBsBQwd1HdIg,4001
|
|
7
7
|
fastapi_voyager/type.py,sha256=nad4WNxTcZFi7Mskw6p2W7v2Gs4f0giVLNoFjZlKmbA,1778
|
|
8
8
|
fastapi_voyager/type_helper.py,sha256=f2Gy5r3Zi6a2wTkbqU9W-AkvcetajEYCfroACzcIcVY,9064
|
|
9
|
-
fastapi_voyager/version.py,sha256=
|
|
10
|
-
fastapi_voyager/voyager.py,sha256=
|
|
9
|
+
fastapi_voyager/version.py,sha256=9L0K0Mei0qJm8M5gxRnUbHrVzN-hpZON05PnRrfBt-Q,48
|
|
10
|
+
fastapi_voyager/voyager.py,sha256=uOQEzrs3o6UUUswvHGRKELNWYUH1ix0Z7SbzMHwm320,10709
|
|
11
11
|
fastapi_voyager/web/graph-ui.js,sha256=eEjDnJVMvk35LdRoxcqX_fZxLFS9_bUrGAZL6K2O5C0,4176
|
|
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=B_MaewnyHmEqozJHmPHYBfF6n5WOvrhoU3tDryS6ngY,10795
|
|
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=ZIxPa0mWOKK7xntfVWbAKzKSx_Tak-TLU-3rkr13wjI,8906
|
|
18
18
|
fastapi_voyager/web/component/render-graph.js,sha256=e8Xgh2Kl-nYU0P1gstEmAepCgFnk2J6UdxW8TlMafGs,2322
|
|
19
19
|
fastapi_voyager/web/component/route-code-display.js,sha256=NECC1OGcPCdDfbghtRJEnmFM6HmH5J3win2ibapWPeA,2649
|
|
20
20
|
fastapi_voyager/web/component/schema-code-display.js,sha256=oOusgTvCaWGnoKb-NBwu0SXqJJf2PTUtp3lUczokTBM,5515
|
|
@@ -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.7.1.dist-info/METADATA,sha256=jMXu2Uz4UHAhDY8AaG6cnQlV3n8cgkFm6M4hX9Pm6eY,7068
|
|
30
|
+
fastapi_voyager-0.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
31
|
+
fastapi_voyager-0.7.1.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
|
|
32
|
+
fastapi_voyager-0.7.1.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
|
|
33
|
+
fastapi_voyager-0.7.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|