fastapi-voyager 0.11.10__py3-none-any.whl → 0.12.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 CHANGED
@@ -1,15 +1,18 @@
1
1
  """Command line interface for fastapi-voyager."""
2
2
  import argparse
3
- import sys
4
- import importlib.util
5
3
  import importlib
4
+ import importlib.util
5
+ import logging
6
6
  import os
7
+ import sys
7
8
  from typing import Optional
8
9
 
9
10
  from fastapi import FastAPI
10
- from fastapi_voyager.voyager import Voyager
11
- from fastapi_voyager.version import __version__
12
11
  from fastapi_voyager import server as viz_server
12
+ from fastapi_voyager.version import __version__
13
+ from fastapi_voyager.voyager import Voyager
14
+
15
+ logger = logging.getLogger(__name__)
13
16
 
14
17
 
15
18
  def load_fastapi_app_from_file(module_path: str, app_name: str = "app") -> Optional[FastAPI]:
@@ -22,7 +25,7 @@ def load_fastapi_app_from_file(module_path: str, app_name: str = "app") -> Optio
22
25
  # Load the module
23
26
  spec = importlib.util.spec_from_file_location("app_module", module_path)
24
27
  if spec is None or spec.loader is None:
25
- print(f"Error: Could not load module from {module_path}")
28
+ logger.error(f"Could not load module from {module_path}")
26
29
  return None
27
30
 
28
31
  module = importlib.util.module_from_spec(spec)
@@ -34,15 +37,13 @@ def load_fastapi_app_from_file(module_path: str, app_name: str = "app") -> Optio
34
37
  app = getattr(module, app_name)
35
38
  if isinstance(app, FastAPI):
36
39
  return app
37
- else:
38
- print(f"Error: '{app_name}' is not a FastAPI instance")
39
- return None
40
- else:
41
- print(f"Error: No attribute '{app_name}' found in the module")
40
+ logger.error(f"'{app_name}' is not a FastAPI instance")
42
41
  return None
42
+ logger.error(f"No attribute '{app_name}' found in the module")
43
+ return None
43
44
 
44
45
  except Exception as e:
45
- print(f"Error loading FastAPI app: {e}")
46
+ logger.error(f"Error loading FastAPI app: {e}")
46
47
  return None
47
48
 
48
49
 
@@ -66,22 +67,20 @@ def load_fastapi_app_from_module(module_name: str, app_name: str = "app") -> Opt
66
67
  app = getattr(module, app_name)
67
68
  if isinstance(app, FastAPI):
68
69
  return app
69
- else:
70
- print(f"Error: '{app_name}' is not a FastAPI instance")
71
- return None
72
- else:
73
- print(f"Error: No attribute '{app_name}' found in module '{module_name}'")
70
+ logger.error(f"'{app_name}' is not a FastAPI instance")
74
71
  return None
72
+ logger.error(f"No attribute '{app_name}' found in module '{module_name}'")
73
+ return None
75
74
  finally:
76
75
  # Cleanup: if we added the path, remove it
77
76
  if path_added and current_dir in sys.path:
78
77
  sys.path.remove(current_dir)
79
78
 
80
79
  except ImportError as e:
81
- print(f"Error: Could not import module '{module_name}': {e}")
80
+ logger.error(f"Could not import module '{module_name}': {e}")
82
81
  return None
83
82
  except Exception as e:
84
- print(f"Error loading FastAPI app from module '{module_name}': {e}")
83
+ logger.error(f"Error loading FastAPI app from module '{module_name}': {e}")
85
84
  return None
86
85
 
87
86
 
@@ -110,9 +109,9 @@ def generate_visualization(
110
109
  # Optionally write to file
111
110
  with open(output_file, 'w', encoding='utf-8') as f:
112
111
  f.write(dot_content)
113
- print(f"DOT file generated: {output_file}")
114
- print("To render the graph, use: dot -Tpng router_viz.dot -o router_viz.png")
115
- print("Or view online: https://dreampuf.github.io/GraphvizOnline/")
112
+ logger.info(f"DOT file generated: {output_file}")
113
+ logger.info("To render the graph, use: dot -Tpng router_viz.dot -o router_viz.png")
114
+ logger.info("Or view online: https://dreampuf.github.io/GraphvizOnline/")
116
115
 
117
116
 
118
117
  def main():
@@ -217,41 +216,30 @@ Examples:
217
216
  help="Filter by route id (format: <endpoint>_<path with _>)"
218
217
  )
219
218
  parser.add_argument(
220
- "--demo",
221
- action="store_true",
222
- help="Run built-in demo (equivalent to: --module tests.demo --server --module_color=tests.service:blue --module_color=tests.demo:tomato)"
219
+ "--log-level",
220
+ dest="log_level",
221
+ default="INFO",
222
+ help="Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL (default: INFO)"
223
223
  )
224
224
 
225
225
  args = parser.parse_args()
226
226
 
227
- # Handle demo mode: override module_name and defaults
228
- if args.demo:
229
- # Force module loading path
230
- args.module_name = "tests.demo"
231
- # Ensure server mode on
232
- args.server = True
233
- # Inject default module colors if absent / merge
234
- demo_defaults = ["tests.service:blue", "tests.demo:tomato"]
235
- existing = set(args.module_color or [])
236
- for d in demo_defaults:
237
- # only add if same key not already provided
238
- key = d.split(":", 1)[0]
239
- if not any(mc.startswith(key + ":") for mc in existing):
240
- args.module_color = (args.module_color or []) + [d]
241
-
242
227
  if args.module_prefix and not args.server:
243
228
  parser.error("--module_prefix can only be used together with --server")
244
229
 
245
- # Validate required target if not demo
246
- if not args.demo and not (args.module_name or args.module):
247
- parser.error("You must provide a module file, -m module name, or use --demo")
230
+ if not (args.module_name or args.module):
231
+ parser.error("You must provide a module file, -m module name")
232
+
233
+ # Configure logging based on --log-level
234
+ level_name = (args.log_level or "INFO").upper()
235
+ logging.basicConfig(level=level_name)
248
236
 
249
237
  # Load FastAPI app based on the input method (module_name takes precedence)
250
238
  if args.module_name:
251
239
  app = load_fastapi_app_from_module(args.module_name, args.app)
252
240
  else:
253
241
  if not os.path.exists(args.module):
254
- print(f"Error: File '{args.module}' not found")
242
+ logger.error(f"File '{args.module}' not found")
255
243
  sys.exit(1)
256
244
  app = load_fastapi_app_from_file(args.module, args.app)
257
245
 
@@ -279,15 +267,15 @@ Examples:
279
267
  try:
280
268
  import uvicorn
281
269
  except ImportError:
282
- print("Error: uvicorn is required to run the server. Install via 'pip install uvicorn' or 'uv add uvicorn'.")
270
+ logger.info("uvicorn is required to run the server. Install via 'pip install uvicorn' or 'uv add uvicorn'.")
283
271
  sys.exit(1)
284
272
  app_server = viz_server.create_voyager(
285
273
  app,
286
274
  module_color=module_color,
287
275
  module_prefix=args.module_prefix,
288
276
  )
289
- print(f"Starting preview server at http://{args.host}:{args.port} ... (Ctrl+C to stop)")
290
- uvicorn.run(app_server, host=args.host, port=args.port)
277
+ logger.info(f"Starting preview server at http://{args.host}:{args.port} ... (Ctrl+C to stop)")
278
+ uvicorn.run(app_server, host=args.host, port=args.port, log_level=level_name.lower())
291
279
  else:
292
280
  # Generate and write dot file locally
293
281
  generate_visualization(
@@ -300,7 +288,7 @@ Examples:
300
288
  route_name=args.route_name,
301
289
  )
302
290
  except Exception as e:
303
- print(f"Error generating visualization: {e}")
291
+ logger.info(f"Error generating visualization: {e}")
304
292
  sys.exit(1)
305
293
 
306
294
 
fastapi_voyager/filter.py CHANGED
@@ -236,7 +236,6 @@ def filter_subgraph_from_tag_to_schema_by_module_prefix(
236
236
  seen_pairs: set[tuple[str, str]] = set()
237
237
 
238
238
  for link in tag_route_links:
239
- # print(link)
240
239
  tag_id = link.source_origin
241
240
  start_node_id = link.target_origin
242
241
  if tag_id is None or start_node_id is None:
fastapi_voyager/render.py CHANGED
@@ -1,6 +1,9 @@
1
1
  from typing import Optional
2
2
  from fastapi_voyager.type import SchemaNode, ModuleNode, Link, Tag, Route, FieldType, PK, ModuleRoute
3
3
  from fastapi_voyager.module import build_module_schema_tree, build_module_route_tree
4
+ from logging import getLogger
5
+
6
+ logger = getLogger(__name__)
4
7
 
5
8
 
6
9
  class Renderer:
@@ -17,7 +20,10 @@ class Renderer:
17
20
  self.schema = schema
18
21
  self.show_module = show_module
19
22
 
20
- def render_schema_label(self, node: SchemaNode) -> str:
23
+ logger.info(f'show_module: {self.show_module}')
24
+ logger.info(f'module_color: {self.module_color}')
25
+
26
+ def render_schema_label(self, node: SchemaNode, color: Optional[str]=None) -> str:
21
27
  has_base_fields = any(f.from_base for f in node.fields)
22
28
  fields = [n for n in node.fields if n.from_base is False]
23
29
 
@@ -38,7 +44,8 @@ class Renderer:
38
44
  field_str = f"""<tr><td align="left" port="f{field.name}" cellpadding="8"><font> {display_xml} </font></td></tr>"""
39
45
  fields_parts.append(field_str)
40
46
 
41
- header_color = 'tomato' if node.id == self.schema else '#009485'
47
+ default_color = '#009485' if color is None else color
48
+ header_color = 'tomato' if node.id == self.schema else default_color
42
49
  header = f"""<tr><td cellpadding="6" bgcolor="{header_color}" align="center" colspan="1" port="{PK}"> <font color="white"> {node.name} </font></td> </tr>"""
43
50
  field_content = ''.join(fields_parts) if fields_parts else ''
44
51
  return f"""<<table border="1" cellborder="0" cellpadding="0" bgcolor="white"> {header} {field_content} </table>>"""
@@ -67,43 +74,56 @@ class Renderer:
67
74
  raise ValueError(f'Unknown link type: {link.type}')
68
75
 
69
76
  def render_module_schema_content(self, nodes: list[SchemaNode]) -> str:
70
- def render_node(node: SchemaNode) -> str:
77
+ def render_node(node: SchemaNode, color: Optional[str]=None) -> str:
71
78
  return f'''
72
79
  "{node.id}" [
73
- label = {self.render_schema_label(node)}
80
+ label = {self.render_schema_label(node, color)}
74
81
  shape = "plain"
75
82
  margin="0.5,0.1"
76
83
  ];'''
77
- def render_module_schema(mod: ModuleNode) -> str:
78
- color: Optional[str] = None
79
84
 
85
+ def render_module_schema(mod: ModuleNode, inherit_color: Optional[str]=None, show_cluster:bool=True) -> str:
86
+ color: Optional[str] = inherit_color
87
+
88
+ # recursively vist module from short to long: 'a', 'a.b', 'a.b.c'
89
+ # color_flag: {'a', 'a.b.c'}
90
+ # at first 'a', match 'a' -> color, remove 'a' from color_flag
91
+ # at 'a.b', no match
92
+ # at 'a.b.c', match 'a.b.c' -> color, remove 'a.b.c' from color_flag
80
93
  for k in module_color_flag:
81
- if mod.fullname.startswith(k):
94
+ if mod.fullname.startswith(k):
82
95
  module_color_flag.remove(k)
83
96
  color = self.module_color[k]
84
97
  break
85
98
 
86
- inner_nodes = [ render_node(node) for node in mod.schema_nodes ]
99
+ inner_nodes = [ render_node(node, color) for node in mod.schema_nodes ]
87
100
  inner_nodes_str = '\n'.join(inner_nodes)
88
- child_str = '\n'.join(render_module_schema(m) for m in mod.modules)
89
- return f'''
90
- subgraph cluster_module_{mod.fullname.replace('.', '_')} {{
91
- tooltip="{mod.fullname}"
92
- color = "#666"
93
- style="rounded"
94
- label = " {mod.name}"
95
- labeljust = "l"
96
- {(f'pencolor = "{color}"' if color else 'pencolor="#ccc"')}
97
- {(f'penwidth = 3' if color else 'penwidth=""')}
101
+ child_str = '\n'.join(render_module_schema(mod=m, inherit_color=color, show_cluster=show_cluster) for m in mod.modules)
102
+
103
+ if show_cluster:
104
+ return f'''
105
+ subgraph cluster_module_{mod.fullname.replace('.', '_')} {{
106
+ tooltip="{mod.fullname}"
107
+ color = "#666"
108
+ style="rounded"
109
+ label = " {mod.name}"
110
+ labeljust = "l"
111
+ {(f'pencolor = "{color}"' if color else 'pencolor="#ccc"')}
112
+ {(f'penwidth = 3' if color else 'penwidth=""')}
113
+ {inner_nodes_str}
114
+ {child_str}
115
+ }}'''
116
+ else:
117
+ return f'''
98
118
  {inner_nodes_str}
99
119
  {child_str}
100
- }}'''
101
- if self.show_module:
102
- module_schemas = build_module_schema_tree(nodes)
103
- module_color_flag = set(self.module_color.keys())
104
- return '\n'.join(render_module_schema(m) for m in module_schemas)
105
- else:
106
- return '\n'.join(render_node(n) for n in nodes)
120
+ '''
121
+
122
+ # if self.show_module:
123
+ module_schemas = build_module_schema_tree(nodes)
124
+ module_color_flag = set(self.module_color.keys())
125
+ return '\n'.join(render_module_schema(mod=m, show_cluster=self.show_module) for m in module_schemas)
126
+
107
127
 
108
128
  def render_module_route_content(self, routes: list[Route]) -> str:
109
129
  def render_route(route: Route) -> str:
@@ -115,29 +135,32 @@ class Renderer:
115
135
  shape = "record"
116
136
  ];'''
117
137
 
118
- def render_module_route(mod: ModuleRoute) -> str:
138
+ def render_module_route(mod: ModuleRoute, show_cluster: bool=True) -> str:
119
139
  # Inner route nodes, same style as flat route_str
120
140
  inner_nodes = [
121
141
  render_route(r) for r in mod.routes
122
142
  ]
123
143
  inner_nodes_str = '\n'.join(inner_nodes)
124
- child_str = '\n'.join(render_module_route(m) for m in mod.modules)
125
- return f'''
126
- subgraph cluster_route_module_{mod.fullname.replace('.', '_')} {{
127
- tooltip="{mod.fullname}"
128
- color = "#666"
129
- style="rounded"
130
- label = " {mod.name}"
131
- labeljust = "l"
144
+ child_str = '\n'.join(render_module_route(m, show_cluster=show_cluster) for m in mod.modules)
145
+ if show_cluster:
146
+ return f'''
147
+ subgraph cluster_route_module_{mod.fullname.replace('.', '_')} {{
148
+ tooltip="{mod.fullname}"
149
+ color = "#666"
150
+ style="rounded"
151
+ label = " {mod.name}"
152
+ labeljust = "l"
153
+ {inner_nodes_str}
154
+ {child_str}
155
+ }}'''
156
+ else:
157
+ return f'''
132
158
  {inner_nodes_str}
133
159
  {child_str}
134
- }}'''
135
- if self.show_module:
136
- module_routes = build_module_route_tree(routes)
137
- module_routes_str = '\n'.join(render_module_route(m) for m in module_routes)
138
- return module_routes_str
139
- else:
140
- return '\n'.join(render_route(r) for r in routes)
160
+ '''
161
+ module_routes = build_module_route_tree(routes)
162
+ module_routes_str = '\n'.join(render_module_route(m, show_cluster=self.show_module) for m in module_routes)
163
+ return module_routes_str
141
164
 
142
165
 
143
166
  def render_dot(self, tags: list[Tag], routes: list[Route], nodes: list[SchemaNode], links: list[Link], spline_line=False) -> str:
fastapi_voyager/server.py CHANGED
@@ -57,6 +57,9 @@ def create_voyager(
57
57
 
58
58
  # include tags and their routes
59
59
  tags = voyager.tags
60
+ for t in tags:
61
+ t.routes.sort(key=lambda r: r.name)
62
+ tags.sort(key=lambda t: t.name)
60
63
 
61
64
  schemas = voyager.nodes[:]
62
65
  schemas.sort(key=lambda s: s.name)
@@ -1,4 +1,5 @@
1
1
  import inspect
2
+ import logging
2
3
  import os
3
4
  from pydantic import BaseModel
4
5
  from typing import get_origin, get_args, Union, Annotated, Any, Type, Generic, Optional
@@ -6,6 +7,8 @@ from fastapi_voyager.type import FieldInfo
6
7
  from types import UnionType
7
8
  import pydantic_resolve.constant as const
8
9
 
10
+ logger = logging.getLogger(__name__)
11
+
9
12
  # Python <3.12 compatibility: TypeAliasType exists only from 3.12 (PEP 695)
10
13
  try: # pragma: no cover - import guard
11
14
  from typing import TypeAliasType # type: ignore
@@ -228,12 +231,11 @@ def get_source(kls):
228
231
  return "failed to get source"
229
232
 
230
233
 
231
- def safe_issubclass(kls, classinfo):
234
+ def safe_issubclass(kls, target_kls):
232
235
  try:
233
- return issubclass(kls, classinfo)
236
+ return issubclass(kls, target_kls)
234
237
  except TypeError:
235
- # may raise error for corner case such as ForwardRef
236
- print(str(kls), 'is not a target class')
238
+ logger.debug(f'{kls.__module__}:{kls.__qualname__} is not subclass of {target_kls.__module__}:{target_kls.__qualname__}')
237
239
  return False
238
240
 
239
241
 
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.11.10"
2
+ __version__ = "0.12.1"
@@ -157,6 +157,7 @@ export default defineComponent({
157
157
  <q-linear-progress indeterminate color="primary" size="2px"/>
158
158
  </div>
159
159
  <div class="q-ml-lg q-mt-md">
160
+ <p style="font-size: 16px;"> {{ schemaName }} </p>
160
161
  <a :href="link" target="_blank" rel="noopener" style="font-size:12px; color:#3b82f6;">
161
162
  Open in VSCode
162
163
  </a>
@@ -58,9 +58,10 @@ export class GraphUI {
58
58
  }
59
59
 
60
60
  highlightSchemaBanner(node) {
61
- const ele = node.querySelector("polygon[fill='#009485']")
61
+ const polygons = node.querySelectorAll("polygon");
62
+ const ele = polygons[2]; // select the second polygon
62
63
  if (ele) {
63
- ele.setAttribute('fill', 'tomato');
64
+ ele.setAttribute('stroke-width', '3.5');
64
65
  }
65
66
  }
66
67
 
@@ -160,12 +161,6 @@ export class GraphUI {
160
161
  }
161
162
  }
162
163
  });
163
-
164
- $(document).on("keydown.graphui", function (evt) {
165
- if (evt.keyCode === 27 && self.gv) {
166
- self.gv.highlight();
167
- }
168
- });
169
164
  },
170
165
  });
171
166
  }
@@ -59,6 +59,9 @@
59
59
  .adjust-fit {
60
60
  height: calc(100vh - 54px);
61
61
  }
62
+
63
+ .github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}
64
+
62
65
  /* App boot loading overlay & gating */
63
66
  #app-loading-overlay {
64
67
  position: fixed;
@@ -167,9 +170,8 @@
167
170
  </div>
168
171
  </q-tooltip>
169
172
  </q-btn>
170
- <a href="https://github.com/allmonday/fastapi-voyager" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="52" height="52" viewBox="0 0 250 250" style="fill:#009485; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"/><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"/><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"/></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style></div>
171
- <!-- <q-toolbar-title class="text-primary row">
172
- </q-toolbar-title> -->
173
+ <a href="https://github.com/allmonday/fastapi-voyager" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="52" height="52" viewBox="0 0 250 250" style="fill:#009485; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"/><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"/><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"/></svg></a>
174
+ </div>
173
175
  </q-toolbar>
174
176
  </q-header>
175
177
 
@@ -59,6 +59,66 @@ const app = createApp({
59
59
  showDetail.value = false;
60
60
  }
61
61
 
62
+ function readQuerySelection() {
63
+ if (typeof window === "undefined") {
64
+ return { tag: null, route: null };
65
+ }
66
+ const params = new URLSearchParams(window.location.search);
67
+ return {
68
+ tag: params.get("tag") || null,
69
+ route: params.get("route") || null,
70
+ };
71
+ }
72
+
73
+ function findTagByRoute(routeId) {
74
+ return (
75
+ state.rawTags.find((tag) =>
76
+ (tag.routes || []).some((route) => route.id === routeId)
77
+ )?.name || null
78
+ );
79
+ }
80
+
81
+ function syncSelectionToUrl() {
82
+ if (typeof window === "undefined") {
83
+ return;
84
+ }
85
+ const params = new URLSearchParams(window.location.search);
86
+ if (state.tag) {
87
+ params.set("tag", state.tag);
88
+ } else {
89
+ params.delete("tag");
90
+ }
91
+ if (state.routeId) {
92
+ params.set("route", state.routeId);
93
+ } else {
94
+ params.delete("route");
95
+ }
96
+ const hash = window.location.hash || "";
97
+ const search = params.toString();
98
+ const base = window.location.pathname;
99
+ const newUrl = search ? `${base}?${search}${hash}` : `${base}${hash}`;
100
+ window.history.replaceState({}, "", newUrl);
101
+ }
102
+
103
+ function applySelectionFromQuery(selection) {
104
+ let applied = false;
105
+ if (selection.tag && state.rawTags.some((tag) => tag.name === selection.tag)) {
106
+ state.tag = selection.tag;
107
+ state._tag = selection.tag;
108
+ applied = true;
109
+ }
110
+ if (selection.route && state.routeItems?.[selection.route]) {
111
+ state.routeId = selection.route;
112
+ applied = true;
113
+ const inferredTag = findTagByRoute(selection.route);
114
+ if (inferredTag) {
115
+ state.tag = inferredTag;
116
+ state._tag = inferredTag;
117
+ }
118
+ }
119
+ return applied;
120
+ }
121
+
62
122
  async function loadInitial() {
63
123
  state.initializing = true;
64
124
  try {
@@ -82,6 +142,14 @@ const app = createApp({
82
142
  state.version = data.version || "";
83
143
  state.swaggerUrl = data.swagger_url || null
84
144
 
145
+ const querySelection = readQuerySelection();
146
+ const restoredFromQuery = applySelectionFromQuery(querySelection);
147
+ if (restoredFromQuery) {
148
+ syncSelectionToUrl();
149
+ onGenerate();
150
+ return;
151
+ }
152
+
85
153
  switch (data.initial_page_policy) {
86
154
  case "full":
87
155
  onGenerate()
@@ -246,6 +314,7 @@ const app = createApp({
246
314
  state.focus = false;
247
315
  schemaCodeName.value = "";
248
316
  onGenerate();
317
+ syncSelectionToUrl();
249
318
  }
250
319
 
251
320
  function toggleTag(tagName, expanded = null) {
@@ -262,6 +331,7 @@ const app = createApp({
262
331
 
263
332
  state.detailDrawer = false;
264
333
  showRouteDetail.value = false;
334
+ syncSelectionToUrl();
265
335
  }
266
336
 
267
337
  function selectRoute(routeId) {
@@ -275,6 +345,7 @@ const app = createApp({
275
345
  state.focus = false;
276
346
  schemaCodeName.value = "";
277
347
  onGenerate();
348
+ syncSelectionToUrl();
278
349
  }
279
350
 
280
351
  function toggleShowModule(val) {
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-voyager
3
+ Version: 0.12.1
4
+ Summary: Visualize FastAPI application's routing tree and dependencies
5
+ Project-URL: Homepage, https://github.com/allmonday/fastapi-voyager
6
+ Project-URL: Source, https://github.com/allmonday/fastapi-voyager
7
+ Author-email: Tangkikodo <allmonday@126.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: fastapi,openapi,routing,visualization
11
+ Classifier: Framework :: FastAPI
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: fastapi>=0.110
23
+ Requires-Dist: pydantic-resolve>=1.13.2
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest; extra == 'dev'
26
+ Requires-Dist: ruff; extra == 'dev'
27
+ Requires-Dist: uvicorn; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ [![pypi](https://img.shields.io/pypi/v/fastapi-voyager.svg)](https://pypi.python.org/pypi/fastapi-voyager)
31
+ ![Python Versions](https://img.shields.io/pypi/pyversions/fastapi-voyager)
32
+ [![PyPI Downloads](https://static.pepy.tech/badge/fastapi-voyager/month)](https://pepy.tech/projects/fastapi-voyager)
33
+
34
+
35
+ > This repo is still in early stage, it supports pydantic v2 only
36
+
37
+ Visualize your FastAPI endpoints, and explore them interactively.
38
+
39
+ [visit online demo](https://www.newsyeah.fun/voyager/) of project: [composition oriented development pattern](https://github.com/allmonday/composition-oriented-development-pattern)
40
+
41
+ <img width="1600" height="986" alt="image" src="https://github.com/user-attachments/assets/8829cda0-f42d-4c84-be2f-b019bb5fe7e1" />
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install fastapi-voyager
47
+ # or
48
+ uv add fastapi-voyager
49
+ ```
50
+
51
+ ```shell
52
+ voyager -m path.to.your.app.module --server
53
+ ```
54
+
55
+ > [Sub-Application mounts](https://fastapi.tiangolo.com/advanced/sub-applications/) are not supported yet, but you can specify the name of the FastAPI application used with `--app`. Only a single application (default: 'app') can be selected, but in a scenario where `api` is attached through `app.mount("/api", api)`, you can select `api` like this:
56
+
57
+ ```shell
58
+ voyager -m path.to.your.app.module --server --app api
59
+ ```
60
+
61
+ ## Mount into project
62
+
63
+ ```python
64
+ from fastapi import FastAPI
65
+ from fastapi_voyager import create_voyager
66
+ from tests.demo import app
67
+
68
+ app.mount('/voyager', create_voyager(
69
+ app,
70
+ module_color={"tests.service": "red"},
71
+ module_prefix="tests.service"),
72
+ swagger_url="/docs")
73
+ ```
74
+
75
+ ## Features
76
+
77
+ For scenarios of using FastAPI as internal API integration endpoints, `fastapi-voyager` helps to visualize the dependencies.
78
+
79
+ It is also an architecture inspection tool that can identify issues in data relationships during design phase before turly implemtatioin.
80
+
81
+ If the process of building the view model follows the ER model, the full potential of fastapi-voyager can be realized. It allows for quick identification of APIs that use entities, as well as which entities are used by a specific API
82
+
83
+ ### highlight nodes and links
84
+ 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
+
86
+
87
+ <img width="1100" height="700" alt="image" src="https://github.com/user-attachments/assets/3e0369ea-5fa4-469a-82c1-ed57d407e53d" />
88
+
89
+ ### focus on nodes
90
+
91
+ Double click a node, and then toggle focus to hide irrelevant nodes.
92
+
93
+ <img width="1061" height="937" alt="image" src="https://github.com/user-attachments/assets/79709b02-7571-43fc-abc9-17a287a97515" />
94
+
95
+ ### view source code
96
+
97
+ double click a node or route to show source code or open file in vscode.
98
+
99
+ <img width="1297" height="940" alt="image" src="https://github.com/user-attachments/assets/c8bb2e7d-b727-42a6-8c9e-64dce297d2d8" />
100
+
101
+ <img width="1132" height="824" alt="image" src="https://github.com/user-attachments/assets/b706e879-e4fc-48dd-ace1-99bf97e3ed6a" />
102
+
103
+
104
+ ## Command Line Usage
105
+
106
+ ### open in browser
107
+
108
+ ```bash
109
+ # open in browser
110
+ voyager -m tests.demo --server
111
+
112
+ voyager -m tests.demo --server --port=8002
113
+ ```
114
+
115
+ ### generate the dot file
116
+ ```bash
117
+ # generate .dot file
118
+ voyager -m tests.demo
119
+
120
+ voyager -m tests.demo --app my_app
121
+
122
+ voyager -m tests.demo --schema Task
123
+
124
+ voyager -m tests.demo --show_fields all
125
+
126
+ voyager -m tests.demo --module_color=tests.demo:red --module_color=tests.service:tomato
127
+
128
+ voyager -m tests.demo -o my_visualization.dot
129
+
130
+ voyager --version
131
+
132
+ voyager --help
133
+ ```
134
+
135
+ ## About pydantic-resolve
136
+
137
+ pydantic-resolve's `@ensure_subset` decorator helps safely pick fields from the 'source class' while **indicating the reference** from the current class to the base class.
138
+
139
+ pydantic-resolve is a lightweight tool designed to build complex, nested data in a simple, declarative way. In version 2.0.0alpha, it will introduce an important feature: ER Diagram, and fastapi-voyager will support this feature, allowing for a clearer understanding of the business relationships between the data.
140
+
141
+ Developers can use fastapi-voyager without needing to know anything about pydantic-resolve, but I still highly recommend everyone to give it a try.
142
+
143
+ ## Dependencies
144
+
145
+ - FastAPI
146
+ - [pydantic-resolve](https://github.com/allmonday/pydantic-resolve)
147
+ - Quasar
148
+
149
+
150
+ ## Credits
151
+
152
+ - https://apis.guru/graphql-voyager/, thanks for inspiration.
153
+ - https://github.com/tintinweb/vscode-interactive-graphviz, thanks for web visualization.
154
+
155
+
156
+ ## How to develop & contribute?
157
+
158
+ fork, clone.
159
+
160
+ install uv.
161
+
162
+ ```shell
163
+ uv venv
164
+ source .venv/bin/activate
165
+ uv pip install ".[dev]"
166
+ uvicorn tests.programatic:app --reload
167
+ ```
168
+
169
+ open `localhost:8000/voyager`
170
+
171
+
172
+ frontend:
173
+ - `src/web/vue-main.js`: main js
174
+
175
+ backend:
176
+ - `voyager.py`: main entry
177
+ - `render.py`: generate dot file
178
+ - `server.py`: serve mode
179
+
180
+
181
+ ## Plan & Raodmap
182
+ - [ideas](./docs/idea.md)
183
+ - [changelog & roadmap](./docs/changelog.md)
@@ -1,23 +1,23 @@
1
1
  fastapi_voyager/__init__.py,sha256=tZy0Nkj8kTaMgbvHy-mGxVcFGVX0Km-36dnzsAIG2uk,230
2
- fastapi_voyager/cli.py,sha256=kQb4g6JEGZR99e5r8LyFFEeb_-uT-n_gp_sDoYG3R7k,11118
3
- fastapi_voyager/filter.py,sha256=GY2J9Vfsf_wbFwC-0t74-Lf-OlO77PnhEXD_rmgkfSw,11574
2
+ fastapi_voyager/cli.py,sha256=xK8DT-m2qP38FK2dGhLP-sHEuS29SBw6ACrnX9w85P0,10521
3
+ fastapi_voyager/filter.py,sha256=9Y-NepveIiCLOI-5eXk6DNK9H3dr5_h4xUbWYHkbo7M,11552
4
4
  fastapi_voyager/module.py,sha256=Z2QHNmiLk6ZAJlm2nSmO875Q33TweSg8UxZSzIpU9zY,3499
5
- fastapi_voyager/render.py,sha256=vdwqIync2wsP8gMPY0v_XjRhdPBtbKyRT8yTBa_Ep3Y,8744
6
- fastapi_voyager/server.py,sha256=G48St-leUcEwshIhTYVotxuFWXDAvzjthOCK9AKh9dI,6497
5
+ fastapi_voyager/render.py,sha256=8hVsEDQi2-aP3QKN6KI3RnNz0uG-FuDr_k4D7QNsdQQ,9823
6
+ fastapi_voyager/server.py,sha256=XEWEfW02eha07piXfEOYeVC1lD6sMgzsCpRcNWgs_HE,6587
7
7
  fastapi_voyager/type.py,sha256=VmcTB1G-LOT70EWCzi4LU_FUkSGWUIBJX15T_J5HnOo,1764
8
- fastapi_voyager/type_helper.py,sha256=CxlUIDlM9fo2MP9UDPNkgFOYTeycBAVk3WRYanBMy6I,9448
9
- fastapi_voyager/version.py,sha256=-MakJnXzJx3P6EPKM_MRAeT0qupzHiUMfwjS2gq2lvY,50
8
+ fastapi_voyager/type_helper.py,sha256=TqtYP2_54aar_iQjD0XhjJPXYhfi6icnPPrxkj0a4sk,9523
9
+ fastapi_voyager/version.py,sha256=RTrsxEThIEJd6F22bbMAXSloKYEyLTUiKljxyyCXfjQ,49
10
10
  fastapi_voyager/voyager.py,sha256=nioo56oFDeZ8nwwPWDtaQbkpe4pVssFoBVHCWFhs0K4,13549
11
- fastapi_voyager/web/graph-ui.js,sha256=DTedkpZNbtufexONVkJ8mOwF_-VnvxoReYHtox6IKR4,5842
11
+ fastapi_voyager/web/graph-ui.js,sha256=9ONPxQHvk4HxYq6KtKc_2VbJmUgd-gh7i3Biv1rkqC4,5734
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=FJtx1_SiNq7tTMRXMKIx7zMInhEMCusw6VFTYBPtpak,19444
14
+ fastapi_voyager/web/index.html,sha256=JgkoMbFTKuufSVVLs9_6mNP2_fDCrQPgTjN3EymBK3Y,19352
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=D1H6UhwAlQta73shjZLmQAKbss7X2Y471fjC1H58p9g,11156
17
+ fastapi_voyager/web/vue-main.js,sha256=m6U24ythzjQuAYXUm9BsTdFrApFNqW26B0Bf7TsybqQ,13275
18
18
  fastapi_voyager/web/component/render-graph.js,sha256=e8Xgh2Kl-nYU0P1gstEmAepCgFnk2J6UdxW8TlMafGs,2322
19
19
  fastapi_voyager/web/component/route-code-display.js,sha256=8NJPPjNRUC21gjpY8XYEQs4RBbhX1pCiqEhJp39ku6k,3678
20
- fastapi_voyager/web/component/schema-code-display.js,sha256=FjoD3CLl967VMfbZNQMdbiEnS0z-doLJDJIeDCmKGew,6983
20
+ fastapi_voyager/web/component/schema-code-display.js,sha256=qKUMV2RFQzR8deof2iC4vyp65UaWadtVsDAXjY-i3vE,7042
21
21
  fastapi_voyager/web/component/schema-field-filter.js,sha256=c--XiXJrhIS7sYo1x8ZwMoqak0k9xLkNYTWoli-zd38,6253
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
@@ -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.11.10.dist-info/METADATA,sha256=X_RI_ODfUGnK0XWGptR1coXHtV4ojNMQiyyNJiBfzjk,10713
30
- fastapi_voyager-0.11.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
- fastapi_voyager-0.11.10.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
32
- fastapi_voyager-0.11.10.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
33
- fastapi_voyager-0.11.10.dist-info/RECORD,,
29
+ fastapi_voyager-0.12.1.dist-info/METADATA,sha256=huz9XjY9D2KrxDQFgnq1ZJgANwmKZyT0qRRI9btk8uA,6060
30
+ fastapi_voyager-0.12.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
+ fastapi_voyager-0.12.1.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
32
+ fastapi_voyager-0.12.1.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
33
+ fastapi_voyager-0.12.1.dist-info/RECORD,,
@@ -1,336 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: fastapi-voyager
3
- Version: 0.11.10
4
- Summary: Visualize FastAPI application's routing tree and dependencies
5
- Project-URL: Homepage, https://github.com/allmonday/fastapi-voyager
6
- Project-URL: Source, https://github.com/allmonday/fastapi-voyager
7
- Author-email: Tangkikodo <allmonday@126.com>
8
- License: MIT
9
- License-File: LICENSE
10
- Keywords: fastapi,openapi,routing,visualization
11
- Classifier: Framework :: FastAPI
12
- Classifier: Intended Audience :: Developers
13
- Classifier: License :: OSI Approved :: MIT License
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.9
16
- Classifier: Programming Language :: Python :: 3.10
17
- Classifier: Programming Language :: Python :: 3.11
18
- Classifier: Programming Language :: Python :: 3.12
19
- Classifier: Programming Language :: Python :: 3.13
20
- Classifier: Programming Language :: Python :: 3.14
21
- Requires-Python: >=3.10
22
- Requires-Dist: fastapi>=0.110
23
- Requires-Dist: pydantic-resolve>=1.13.2
24
- Provides-Extra: dev
25
- Requires-Dist: pytest; extra == 'dev'
26
- Requires-Dist: ruff; extra == 'dev'
27
- Requires-Dist: uvicorn; extra == 'dev'
28
- Description-Content-Type: text/markdown
29
-
30
- [![pypi](https://img.shields.io/pypi/v/fastapi-voyager.svg)](https://pypi.python.org/pypi/fastapi-voyager)
31
- ![Python Versions](https://img.shields.io/pypi/pyversions/fastapi-voyager)
32
- [![PyPI Downloads](https://static.pepy.tech/badge/fastapi-voyager/month)](https://pepy.tech/projects/fastapi-voyager)
33
-
34
-
35
- > This repo is still in early stage, it supports pydantic v2 only
36
-
37
- Visualize your FastAPI endpoints, and explore them interactively.
38
-
39
- [visit online demo](https://www.newsyeah.fun/voyager/) of project: [composition oriented development pattern](https://github.com/allmonday/composition-oriented-development-pattern)
40
-
41
- <img width="1600" height="986" alt="image" src="https://github.com/user-attachments/assets/8829cda0-f42d-4c84-be2f-b019bb5fe7e1" />
42
-
43
- ## Installation
44
-
45
- ```bash
46
- pip install fastapi-voyager
47
- # or
48
- uv add fastapi-voyager
49
- ```
50
-
51
- ```shell
52
- voyager -m path.to.your.app.module --server
53
- ```
54
-
55
- > *sub_app* is not supported yet.
56
-
57
-
58
- ## Mount into project
59
-
60
- ```python
61
- from fastapi import FastAPI
62
- from fastapi_voyager import create_voyager
63
- from tests.demo import app
64
-
65
- app.mount('/voyager', create_voyager(
66
- app,
67
- module_color={"tests.service": "red"},
68
- module_prefix="tests.service"),
69
- swagger_url="/docs")
70
- ```
71
-
72
- more about [sub application](https://fastapi.tiangolo.com/advanced/sub-applications/?h=sub)
73
-
74
-
75
- ## Feature
76
-
77
- For scenarios of using FastAPI as internal API integration endpoints, `fastapi-voyager` helps to visualize the dependencies.
78
-
79
- It is also an architecture inspection tool that can identify issues in data relationships through visualization during the design phase.
80
-
81
- If the process of building the view model follows the ER model, the full potential of fastapi-voyager can be realized. It allows for quick identification of APIs that use entities, as well as which entities are used by a specific API
82
-
83
-
84
-
85
- ```shell
86
- git clone https://github.com/allmonday/fastapi-voyager.git
87
- cd fastapi-voyager
88
-
89
- voyager -m tests.demo
90
- --server --port=8001
91
- --module_color=tests.service:blue
92
- --module_color=tests.demo:tomato
93
- ```
94
-
95
- ### highlight
96
- 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.
97
-
98
- <img width="1100" height="700" alt="image" src="https://github.com/user-attachments/assets/3e0369ea-5fa4-469a-82c1-ed57d407e53d" />
99
-
100
- ### focus on nodes
101
- toggle focus to hide nodes not related with current picked one.
102
-
103
- before:
104
- <img width="1066" height="941" alt="image" src="https://github.com/user-attachments/assets/39f30817-899a-4289-93f4-a1646d3441c1" />
105
- after:
106
- <img width="1061" height="937" alt="image" src="https://github.com/user-attachments/assets/79709b02-7571-43fc-abc9-17a287a97515" />
107
-
108
- ### view source code
109
- double click a node to show source code or open file in vscode.
110
- <img width="1297" height="940" alt="image" src="https://github.com/user-attachments/assets/c8bb2e7d-b727-42a6-8c9e-64dce297d2d8" />
111
-
112
- double click a route to show source code or open file in vscode
113
- <img width="1132" height="824" alt="image" src="https://github.com/user-attachments/assets/b706e879-e4fc-48dd-ace1-99bf97e3ed6a" />
114
-
115
-
116
-
117
- ## Command Line Usage
118
-
119
- ### open in browser
120
-
121
- ```bash
122
- # open in browser
123
- voyager -m tests.demo --server
124
-
125
- voyager -m tests.demo --server --port=8002
126
- ```
127
-
128
- ### generate the dot file
129
- ```bash
130
- # generate .dot file
131
- voyager -m tests.demo
132
-
133
- voyager -m tests.demo --app my_app
134
-
135
- voyager -m tests.demo --schema Task
136
-
137
- voyager -m tests.demo --show_fields all
138
-
139
- voyager -m tests.demo --module_color=tests.demo:red --module_color=tests.service:tomato
140
-
141
- voyager -m tests.demo -o my_visualization.dot
142
-
143
- voyager --version
144
- ```
145
-
146
- The tool will generate a DOT file that you can render using Graphviz:
147
-
148
- ```bash
149
- # Install graphviz
150
- brew install graphviz # macOS
151
- apt-get install graphviz # Ubuntu/Debian
152
-
153
- # Render the graph
154
- dot -Tpng router_viz.dot -o router_viz.png
155
-
156
- # Or view online at: https://dreampuf.github.io/GraphvizOnline/
157
- ```
158
-
159
- or you can open router_viz.dot with vscode extension `graphviz interactive preview`
160
-
161
-
162
- ## Plan before v1.0
163
-
164
-
165
- ### backlog
166
- - [ ] user can generate nodes/edges manually and connect to generated ones
167
- - [ ] eg: add owner
168
- - [ ] add extra info for schema
169
- - [ ] display standard ER diagram `hard`
170
- - [ ] display potential invalid links
171
- - [ ] optimize static resource (allow manually config url)
172
- - [ ] improve search dialog
173
- - [ ] add route/tag list
174
- - [ ] type alias should not be kept as node instead of compiling to original type
175
- - [ ] how to correctly handle the generic type ?
176
- - [ ] support Google analysis config
177
-
178
- ### in analysis
179
- - [ ] click field to highlight links
180
- - [ ] animation effect for edges
181
- - [ ] customrized right click panel
182
- - [ ] show own dependencies
183
- - [ ] sort field name
184
- - [ ] set max limit for fields
185
- - [ ] logging information
186
-
187
- ### plan:
188
- #### <0.9:
189
- - [x] group schemas by module hierarchy
190
- - [x] module-based coloring via Analytics(module_color={...})
191
- - [x] view in web browser
192
- - [x] config params
193
- - [x] make a explorer dashboard, provide list of routes, schemas, to make it easy to switch and search
194
- - [x] support programmatic usage
195
- - [x] better schema /router node appearance
196
- - [x] hide fields duplicated with parent's (show `parent fields` instead)
197
- - [x] refactor the frontend to vue, and tweak the build process
198
- - [x] find dependency based on picked schema and it's field.
199
- - [x] optimize static resource (cdn -> local)
200
- - [x] add configuration for highlight (optional)
201
- - [x] alt+click to show field details
202
- - [x] display source code of routes (including response_model)
203
- - [x] handle excluded field
204
- - [x] add tooltips
205
- - [x] route
206
- - [x] group routes by module hierarchy
207
- - [x] add response_model in route
208
- - [x] fixed left bar show tag/ route
209
- - [x] export voyager core data into json (for better debugging)
210
- - [x] add api to rebuild core data from json, and render it
211
- - [x] fix Generic case `test_generic.py`
212
- - [x] show tips for routes not return pydantic type.
213
- - [x] fix duplicated link from class and parent class, it also break clicking highlight
214
- - [x] refactor: abstract render module
215
-
216
- #### 0.9
217
- - [x] refactor: server.py
218
- - [x] rename create_app_with_fastapi -> create_voyager
219
- - [x] add doc for parameters
220
- - [x] improve initialization time cost
221
- - [x] query route / schema info through realtime api
222
- - [x] adjust fe
223
- - 0.9.3
224
- - [x] adjust layout
225
- - [x] show field detail in right panel
226
- - [x] show route info in bottom
227
- - 0.9.4
228
- - [x] close schema sidebar when switch tag/route
229
- - [x] schema detail panel show fields by default
230
- - [x] adjust schema panel's height
231
- - [x] show from base information in subset case
232
- - 0.9.5
233
- - [x] route list should have a max height
234
-
235
- #### 0.10
236
- - 0.10.1
237
- - [x] refactor voyager.py tag -> route structure
238
- - [x] fix missing route (tag has only one route which return primitive value)
239
- - [x] make right panel resizable by dragging
240
- - [x] allow closing tag expansion item
241
- - [x] hide brief mode if not configured
242
- - [x] add focus button to only show related nodes under current route/tag graph in dialog
243
- - 0.10.2
244
- - [x] fix graph height
245
- - [x] show version in title
246
- - 0.10.3
247
- - [x] fix focus in brief-mode
248
- - [x] ui: adjust focus position
249
- - [x] refactor naming
250
- - [x] fix layout issue when rendering huge graph
251
- - 0.10.4
252
- - [x] fix: when focus is on, should ensure changes from other params not broken.
253
- - 0.10.5
254
- - [x] double click to show details, and highlight as tomato
255
-
256
-
257
- #### 0.11
258
- - 0.11.1
259
- - [x] support opening route in swagger
260
- - [x] config docs path
261
- - [x] provide option to hide routes in brief mode (auto hide in full graph mode)
262
- - 0.11.2
263
- - [x] enable/disable module cluster (to save space)
264
- - 0.11.3
265
- - [x] support online repo url
266
- - 0.11.4
267
- - [x] add loading for field detail panel
268
- - 0.11.5
269
- - [x] optimize open in swagger link
270
- - [x] change jquery cdn
271
- - 0.11.6
272
- - [x] flag of loading full graph in first render or not
273
- - [x] optimize loading static resource
274
- - 0.11.7
275
- - [x] fix swagger link
276
- - 0.11.8
277
- - [x] fix swagger link in another way
278
- - 0.11.9
279
- - [x] replace issubclass with safe_issubclass to prevent exception.
280
- - 0.11.10
281
- - [x] fix bug during updating forward refs
282
-
283
- #### 0.12
284
- - [ ] add tests
285
- - [ ] integration with pydantic-resolve
286
- - [ ] show hint for resolve, post fields
287
- - [ ] display loader as edges
288
-
289
- #### 0.13
290
- - [ ] config release pipeline
291
-
292
-
293
- ## About pydantic-resolve
294
-
295
- pydantic-resolve's `@ensure_subset` decorator helps safely pick fields from the 'source class' while indicating the reference from the current class to the base class.
296
-
297
- pydantic-resolve is a lightweight tool designed to build complex, nested data in a simple, declarative way. In version 2, it will introduce an important feature: ER model definition, and fastapi-voyager will support and visualize these diagrams.
298
-
299
- Developers can use fastapi-voyager without needing to know about pydantic-resolve.
300
-
301
-
302
- ## Credits
303
-
304
- - https://apis.guru/graphql-voyager/, thanks for inspiration.
305
- - https://github.com/tintinweb/vscode-interactive-graphviz, thanks for web visualization.
306
-
307
-
308
- ## Dependencies
309
-
310
- - FastAPI
311
- - [pydantic-resolve](https://github.com/allmonday/pydantic-resolve)
312
- - Quasar
313
-
314
-
315
- ## How to develop & contribute?
316
-
317
- fork, clone.
318
-
319
- install uv.
320
-
321
- ```shell
322
- uv venv
323
- source .venv/bin/activate
324
- uv pip install ".[dev]"
325
- uvicorn tests.programatic:app --reload
326
- ```
327
-
328
- open `localhost:8000/voyager`
329
-
330
-
331
- frontend: `src/web/vue-main.js`
332
- backend: `voyager.py`, `render.py`, `server.py`
333
-
334
- ## Branch and Release flow
335
-
336
- TODO