fastapi-voyager 0.6.2__tar.gz → 0.7.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/PKG-INFO +21 -15
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/README.md +20 -14
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/cli.py +14 -1
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/filter.py +91 -1
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/server.py +8 -2
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/version.py +1 -1
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/voyager.py +13 -1
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/index.html +8 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/vue-main.js +4 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/tests/demo.py +6 -2
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/tests/service/schema.py +7 -0
- fastapi_voyager-0.7.2/tests/test_filter.py +134 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/.gitignore +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/.python-version +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/LICENSE +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/pyproject.toml +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/__init__.py +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/module.py +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/render.py +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/type.py +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/type_helper.py +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/component/render-graph.js +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/component/schema-field-filter.js +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/graph-ui.js +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/quasar.min.css +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/quasar.min.js +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/tests/__init__.py +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/tests/demo_anno.py +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/tests/programatic.py +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/tests/service/__init__.py +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/tests/test_analysis.py +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/tests/test_generic.py +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/tests/test_import.py +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/tests/test_module.py +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/tests/test_type_helper.py +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/uv.lock +0 -0
- {fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/voyager.jpg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-voyager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -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,11 @@ TODO: ...
|
|
|
203
203
|
|
|
204
204
|
## Changelog
|
|
205
205
|
|
|
206
|
-
- 0.
|
|
207
|
-
-
|
|
206
|
+
- 0.7:
|
|
207
|
+
- 0.7.2
|
|
208
|
+
- keep links inside filtered nodes.
|
|
209
|
+
- 0.7.1
|
|
210
|
+
- support brief mode, you can use `--module_prefix tests.service` to show links between routes and filtered schemas, to make the graph less complicated.
|
|
211
|
+
- 0.6:
|
|
212
|
+
- 0.6.2:
|
|
213
|
+
- fix generic related issue
|
|
@@ -119,7 +119,7 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
|
|
|
119
119
|
|
|
120
120
|
## Plan before v1.0
|
|
121
121
|
|
|
122
|
-
features
|
|
122
|
+
features:
|
|
123
123
|
- [x] group schemas by module hierarchy
|
|
124
124
|
- [x] module-based coloring via Analytics(module_color={...})
|
|
125
125
|
- [x] view in web browser
|
|
@@ -135,31 +135,31 @@ features
|
|
|
135
135
|
- [x] alt+click to show field details
|
|
136
136
|
- [x] display source code of routes (including response_model)
|
|
137
137
|
- [x] handle excluded field
|
|
138
|
-
- [
|
|
139
|
-
- [
|
|
140
|
-
- [
|
|
138
|
+
- [x] add tooltips
|
|
139
|
+
- [x] route
|
|
140
|
+
- [x] group routes by module hierarchy
|
|
141
141
|
- [ ] add response_model in route
|
|
142
|
-
- [ ] support dataclass
|
|
142
|
+
- [ ] support dataclass (pending)
|
|
143
143
|
- [ ] click field to highlight links
|
|
144
144
|
- [ ] user can generate nodes/edges manually and connect to generated ones
|
|
145
145
|
- [ ] add owner
|
|
146
146
|
- [ ] ui optimization
|
|
147
147
|
- [ ] fixed left/right bar show field information
|
|
148
|
-
- [ ] display standard ER diagram
|
|
148
|
+
- [ ] display standard ER diagram `difficult`
|
|
149
149
|
- [ ] display potential invalid links
|
|
150
150
|
- [ ] integration with pydantic-resolve
|
|
151
151
|
- [ ] show difference between resolve, post fields
|
|
152
152
|
- [x] strikethrough for excluded fields
|
|
153
153
|
- [ ] display loader as edges
|
|
154
|
+
- [x] export voyager core data into json (for better debugging)
|
|
155
|
+
- [x] add api to rebuild core data from json, and render it
|
|
156
|
+
- [x] fix Generic case `test_generic.py`
|
|
157
|
+
|
|
158
|
+
bugs & non feature:
|
|
159
|
+
- [x] fix duplicated link from class and parent class, it also break clicking highlight
|
|
154
160
|
- [ ] add tests
|
|
155
161
|
- [ ] refactor
|
|
156
162
|
- [ ] abstract render module
|
|
157
|
-
- [ ] export voyager core data into json (for better debugging)
|
|
158
|
-
- [ ] add api to rebuild core data from json, and render it
|
|
159
|
-
- [x] fix Generic case `test_generic.py`
|
|
160
|
-
|
|
161
|
-
bugs:
|
|
162
|
-
- [ ] fix duplicated link from class and parent class, it also break clicking highlight
|
|
163
163
|
|
|
164
164
|
## Using with pydantic-resolve
|
|
165
165
|
|
|
@@ -176,5 +176,11 @@ TODO: ...
|
|
|
176
176
|
|
|
177
177
|
## Changelog
|
|
178
178
|
|
|
179
|
-
- 0.
|
|
180
|
-
-
|
|
179
|
+
- 0.7:
|
|
180
|
+
- 0.7.2
|
|
181
|
+
- keep links inside filtered nodes.
|
|
182
|
+
- 0.7.1
|
|
183
|
+
- support brief mode, you can use `--module_prefix tests.service` to show links between routes and filtered schemas, to make the graph less complicated.
|
|
184
|
+
- 0.6:
|
|
185
|
+
- 0.6.2:
|
|
186
|
+
- fix generic related issue
|
|
@@ -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:
|
|
@@ -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,92 @@ 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
|
+
module_prefix_links = [
|
|
187
|
+
lk
|
|
188
|
+
for lk in links
|
|
189
|
+
if (lk.source_origin or "").startswith(module_prefix)
|
|
190
|
+
and (lk.target_origin or "").startswith(module_prefix)
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
filtered_links = tag_route_links + merged_links + module_prefix_links
|
|
194
|
+
|
|
195
|
+
return tags, routes, filtered_nodes, filtered_links
|
|
@@ -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:
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.7.2"
|
|
@@ -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)
|
|
@@ -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
|
|
|
@@ -2,7 +2,7 @@ from pydantic import BaseModel, Field
|
|
|
2
2
|
from fastapi import FastAPI
|
|
3
3
|
from typing import Optional, Generic, TypeVar
|
|
4
4
|
from pydantic_resolve import ensure_subset, Resolver
|
|
5
|
-
from tests.service.schema import Story, Task
|
|
5
|
+
from tests.service.schema import Story, Task, A
|
|
6
6
|
import tests.service.schema as serv
|
|
7
7
|
|
|
8
8
|
app = FastAPI(title="Demo API", description="A demo FastAPI application for router visualization")
|
|
@@ -73,4 +73,8 @@ type DataModelPageStory = DataModel[PageStory]
|
|
|
73
73
|
|
|
74
74
|
@app.get("/page_test_1/", tags=['for-page'], response_model=DataModelPageStory)
|
|
75
75
|
def get_page_test_1():
|
|
76
|
-
return {} # no implementation
|
|
76
|
+
return {} # no implementation
|
|
77
|
+
|
|
78
|
+
@app.get("/page_test_2/", tags=['for-page'], response_model=A)
|
|
79
|
+
def get_page_test_2():
|
|
80
|
+
return {}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from fastapi_voyager.filter import filter_subgraph
|
|
2
|
+
from fastapi_voyager.type import Tag, Route, SchemaNode, Link, PK
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _make_tag_route_link(tag: Tag, route: Route) -> Link:
|
|
6
|
+
return Link(
|
|
7
|
+
source=tag.id,
|
|
8
|
+
source_origin=tag.id,
|
|
9
|
+
target=route.id,
|
|
10
|
+
target_origin=route.id,
|
|
11
|
+
type="tag_route",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_filter_subgraph_filters_nodes_and_links():
|
|
16
|
+
tag = Tag(id="tag1", name="Tag 1", routes=[])
|
|
17
|
+
route = Route(id="route1", name="route1", module="api.routes")
|
|
18
|
+
tag.routes.append(route)
|
|
19
|
+
|
|
20
|
+
node_a = SchemaNode(id="pkg.ModelA", name="ModelA", module="pkg.moduleA")
|
|
21
|
+
node_b = SchemaNode(id="pkg.ModelB", name="ModelB", module="target.moduleB")
|
|
22
|
+
|
|
23
|
+
links = [
|
|
24
|
+
_make_tag_route_link(tag, route),
|
|
25
|
+
Link(
|
|
26
|
+
source=route.id,
|
|
27
|
+
source_origin=route.id,
|
|
28
|
+
target=f"{node_a.id}::{PK}",
|
|
29
|
+
target_origin=node_a.id,
|
|
30
|
+
type="route_to_schema",
|
|
31
|
+
),
|
|
32
|
+
Link(
|
|
33
|
+
source=f"{node_a.id}::ffield",
|
|
34
|
+
source_origin=node_a.id,
|
|
35
|
+
target=f"{node_b.id}::{PK}",
|
|
36
|
+
target_origin=node_b.id,
|
|
37
|
+
type="schema",
|
|
38
|
+
),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
tags = [tag]
|
|
42
|
+
routes = [route]
|
|
43
|
+
nodes = [node_a, node_b]
|
|
44
|
+
|
|
45
|
+
_, _, filtered_nodes, filtered_links = filter_subgraph(
|
|
46
|
+
tags=tags,
|
|
47
|
+
routes=routes,
|
|
48
|
+
links=links,
|
|
49
|
+
nodes=nodes,
|
|
50
|
+
module_prefix="target",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
assert filtered_nodes == [node_b]
|
|
54
|
+
assert any(
|
|
55
|
+
lk.type == "route_to_schema" and lk.source_origin == route.id and lk.target_origin == node_b.id
|
|
56
|
+
for lk in filtered_links
|
|
57
|
+
)
|
|
58
|
+
assert len(filtered_links) == 2 # tag -> route and merged route -> filtered node
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_filter_subgraph_handles_cycles_and_multiple_matches():
|
|
63
|
+
tag = Tag(id="tag-main", name="Tag", routes=[])
|
|
64
|
+
route = Route(id="route-main", name="route", module="api.routes")
|
|
65
|
+
tag.routes.append(route)
|
|
66
|
+
|
|
67
|
+
node_root = SchemaNode(id="pkg.Root", name="Root", module="pkg.root")
|
|
68
|
+
node_mid = SchemaNode(id="pkg.Mid", name="Mid", module="pkg.mid")
|
|
69
|
+
node_target1 = SchemaNode(id="pkg.Target1", name="Target1", module="target.mod.alpha")
|
|
70
|
+
node_target2 = SchemaNode(id="pkg.Target2", name="Target2", module="target.mod.beta")
|
|
71
|
+
|
|
72
|
+
links = [
|
|
73
|
+
_make_tag_route_link(tag, route),
|
|
74
|
+
Link(
|
|
75
|
+
source=route.id,
|
|
76
|
+
source_origin=route.id,
|
|
77
|
+
target=f"{node_root.id}::{PK}",
|
|
78
|
+
target_origin=node_root.id,
|
|
79
|
+
type="route_to_schema",
|
|
80
|
+
),
|
|
81
|
+
Link(
|
|
82
|
+
source=f"{node_root.id}::ffield",
|
|
83
|
+
source_origin=node_root.id,
|
|
84
|
+
target=f"{node_mid.id}::{PK}",
|
|
85
|
+
target_origin=node_mid.id,
|
|
86
|
+
type="schema",
|
|
87
|
+
),
|
|
88
|
+
Link(
|
|
89
|
+
source=f"{node_mid.id}::{PK}",
|
|
90
|
+
source_origin=node_mid.id,
|
|
91
|
+
target=f"{node_target1.id}::{PK}",
|
|
92
|
+
target_origin=node_target1.id,
|
|
93
|
+
type="parent",
|
|
94
|
+
),
|
|
95
|
+
Link(
|
|
96
|
+
source=f"{node_mid.id}::ffield",
|
|
97
|
+
source_origin=node_mid.id,
|
|
98
|
+
target=f"{node_target2.id}::{PK}",
|
|
99
|
+
target_origin=node_target2.id,
|
|
100
|
+
type="subset",
|
|
101
|
+
),
|
|
102
|
+
Link(
|
|
103
|
+
source=f"{node_target1.id}::ffield",
|
|
104
|
+
source_origin=node_target1.id,
|
|
105
|
+
target=f"{node_root.id}::{PK}",
|
|
106
|
+
target_origin=node_root.id,
|
|
107
|
+
type="schema",
|
|
108
|
+
),
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
nodes = [node_root, node_mid, node_target1, node_target2]
|
|
112
|
+
|
|
113
|
+
_, _, filtered_nodes, filtered_links = filter_subgraph(
|
|
114
|
+
tags=[tag],
|
|
115
|
+
routes=[route],
|
|
116
|
+
links=links,
|
|
117
|
+
nodes=nodes,
|
|
118
|
+
module_prefix="target.mod",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
assert filtered_nodes == [node_target1, node_target2]
|
|
122
|
+
|
|
123
|
+
route_to_schema_targets = {
|
|
124
|
+
(lk.source_origin, lk.target_origin)
|
|
125
|
+
for lk in filtered_links
|
|
126
|
+
if lk.type == "route_to_schema"
|
|
127
|
+
}
|
|
128
|
+
assert route_to_schema_targets == {
|
|
129
|
+
(route.id, node_target1.id),
|
|
130
|
+
(route.id, node_target2.id),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
assert all(lk.type in {"tag_route", "route_to_schema"} for lk in filtered_links)
|
|
134
|
+
assert len(filtered_links) == 3 # 1 tag_route + 2 merged links
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/component/render-graph.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/icon/apple-touch-icon.png
RENAMED
|
File without changes
|
{fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/icon/favicon-16x16.png
RENAMED
|
File without changes
|
{fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/icon/favicon-32x32.png
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.6.2 → fastapi_voyager-0.7.2}/src/fastapi_voyager/web/icon/site.webmanifest
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|