fastapi-voyager 0.10.5__py3-none-any.whl → 0.11.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fastapi_voyager/filter.py CHANGED
@@ -192,4 +192,94 @@ def filter_subgraph_by_module_prefix(
192
192
 
193
193
  filtered_links = tag_route_links + merged_links + module_prefix_links
194
194
 
195
- return tags, routes, filtered_nodes, filtered_links
195
+ return tags, routes, filtered_nodes, filtered_links
196
+
197
+
198
+ def filter_subgraph_from_tag_to_schema_by_module_prefix(
199
+ *,
200
+ tags: list[Tag],
201
+ routes: list[Route],
202
+ links: list[Link],
203
+ nodes: list[SchemaNode],
204
+ module_prefix: str
205
+ ) -> Tuple[list[Tag], list[Route], list[SchemaNode], list[Link]]:
206
+ """Collapse schema graph so routes link directly to nodes whose module matches ``module_prefix``.
207
+
208
+ The routine keeps tag→route links untouched, prunes schema nodes whose module does not start
209
+ with ``module_prefix``, and merges the remaining schema relationships so each route connects
210
+ directly to the surviving schema nodes. Traversal stops once a qualifying node is reached and
211
+ guards against cycles in the schema graph.
212
+ """
213
+
214
+ if not module_prefix:
215
+ # empty prefix keeps existing graph structure, so simply reuse incoming data
216
+ return tags, routes, nodes, [lk for lk in links if lk.type in ("tag_route", "route_to_schema")]
217
+
218
+ route_links = [lk for lk in links if lk.type == "route_to_schema"]
219
+ schema_links = [lk for lk in links if lk.type in {"schema", "parent", "subset"}]
220
+ tag_route_links = [lk for lk in links if lk.type == "tag_route"]
221
+
222
+ node_lookup: dict[str, SchemaNode] = {node.id: node for node in (nodes + routes)}
223
+
224
+ filtered_nodes = [node for node in nodes if node_lookup[node.id].module.startswith(module_prefix)]
225
+ filtered_node_ids = {node.id for node in filtered_nodes}
226
+
227
+ adjacency: dict[str, list[str]] = {}
228
+ for link in (schema_links + route_links):
229
+ if link.source_origin not in node_lookup or link.target_origin not in node_lookup:
230
+ continue
231
+ adjacency.setdefault(link.source_origin, [])
232
+ if link.target_origin not in adjacency[link.source_origin]:
233
+ adjacency[link.source_origin].append(link.target_origin)
234
+
235
+ merged_links: list[Link] = []
236
+ seen_pairs: set[tuple[str, str]] = set()
237
+
238
+ for link in tag_route_links:
239
+ # print(link)
240
+ tag_id = link.source_origin
241
+ start_node_id = link.target_origin
242
+ if tag_id is None or start_node_id is None:
243
+ continue
244
+ if start_node_id not in node_lookup:
245
+ continue
246
+
247
+ visited: set[str] = set()
248
+ queue: deque[str] = deque([start_node_id])
249
+
250
+ while queue:
251
+ current = queue.popleft()
252
+ if current in visited:
253
+ continue
254
+ visited.add(current)
255
+
256
+ if current in filtered_node_ids:
257
+ key = (tag_id, current)
258
+ if key not in seen_pairs:
259
+ seen_pairs.add(key)
260
+ merged_links.append(
261
+ Link(
262
+ source=link.source,
263
+ source_origin=tag_id,
264
+ target=f"{current}::{PK}",
265
+ target_origin=current,
266
+ type="tag_to_schema",
267
+ )
268
+ )
269
+ # stop traversing past a qualifying node
270
+ continue
271
+
272
+ for next_node in adjacency.get(current, () ):
273
+ if next_node not in visited:
274
+ queue.append(next_node)
275
+
276
+ module_prefix_links = [
277
+ lk
278
+ for lk in links
279
+ if (lk.source_origin or "").startswith(module_prefix)
280
+ and (lk.target_origin or "").startswith(module_prefix)
281
+ ]
282
+
283
+ filtered_links = merged_links + module_prefix_links
284
+
285
+ return tags, [], filtered_nodes, filtered_links # route is skipped
fastapi_voyager/render.py CHANGED
@@ -10,10 +10,12 @@ class Renderer:
10
10
  show_fields: FieldType = 'single',
11
11
  module_color: dict[str, str] | None = None,
12
12
  schema: str | None = None,
13
+ show_module: bool = True
13
14
  ) -> None:
14
15
  self.show_fields = show_fields if show_fields in ('single', 'object', 'all') else 'single'
15
16
  self.module_color = module_color or {}
16
17
  self.schema = schema
18
+ self.show_module = show_module
17
19
 
18
20
  def render_schema_label(self, node: SchemaNode) -> str:
19
21
  has_base_fields = any(f.from_base for f in node.fields)
@@ -59,12 +61,19 @@ class Renderer:
59
61
  return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid, dashed", dir="back", minlen=3, taillabel = "< inherit >", color = "purple", tailport="n"];"""
60
62
  elif link.type == 'subset':
61
63
  return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid, dashed", dir="back", minlen=3, taillabel = "< subset >", color = "orange", tailport="n"];"""
64
+ elif link.type == 'tag_to_schema':
65
+ return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid", minlen=3];"""
62
66
  else:
63
67
  raise ValueError(f'Unknown link type: {link.type}')
64
68
 
65
- def render_module_schema_content(self, mods: list[ModuleNode]) -> str:
66
- module_color_flag = set(self.module_color.keys())
67
-
69
+ def render_module_schema_content(self, nodes: list[SchemaNode]) -> str:
70
+ def render_node(node: SchemaNode) -> str:
71
+ return f'''
72
+ "{node.id}" [
73
+ label = {self.render_schema_label(node)}
74
+ shape = "plain"
75
+ margin="0.5,0.1"
76
+ ];'''
68
77
  def render_module_schema(mod: ModuleNode) -> str:
69
78
  color: Optional[str] = None
70
79
 
@@ -74,14 +83,7 @@ class Renderer:
74
83
  color = self.module_color[k]
75
84
  break
76
85
 
77
- inner_nodes = [
78
- f'''
79
- "{node.id}" [
80
- label = {self.render_schema_label(node)}
81
- shape = "plain"
82
- margin="0.5,0.1"
83
- ];''' for node in mod.schema_nodes
84
- ]
86
+ inner_nodes = [ render_node(node) for node in mod.schema_nodes ]
85
87
  inner_nodes_str = '\n'.join(inner_nodes)
86
88
  child_str = '\n'.join(render_module_schema(m) for m in mod.modules)
87
89
  return f'''
@@ -96,34 +98,49 @@ class Renderer:
96
98
  {inner_nodes_str}
97
99
  {child_str}
98
100
  }}'''
99
- return '\n'.join(render_module_schema(m) for m in mods)
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)
100
107
 
101
- def render_module_route(self, mod: ModuleRoute) -> str:
102
- # Inner route nodes, same style as flat route_str
103
- inner_nodes = [
104
- f'''
105
- "{r.id}" [
106
- label = " {r.name} | {r.response_schema} "
107
- margin="0.5,0.1"
108
- shape = "record"
109
- ];''' for r in mod.routes
110
- ]
111
- inner_nodes_str = '\n'.join(inner_nodes)
112
- child_str = '\n'.join(self.render_module_route(m) for m in mod.modules)
113
- return f'''
114
- subgraph cluster_route_module_{mod.fullname.replace('.', '_')} {{
115
- tooltip="{mod.fullname}"
116
- color = "#666"
117
- style="rounded"
118
- label = " {mod.name}"
119
- labeljust = "l"
120
- {inner_nodes_str}
121
- {child_str}
122
- }}'''
108
+ def render_module_route_content(self, routes: list[Route]) -> str:
109
+ def render_route(route: Route) -> str:
110
+ response_schema = route.response_schema[:25] + '..' if len(route.response_schema) > 25 else route.response_schema
111
+ return f'''
112
+ "{route.id}" [
113
+ label = " {route.name} | {response_schema} "
114
+ margin="0.5,0.1"
115
+ shape = "record"
116
+ ];'''
117
+
118
+ def render_module_route(mod: ModuleRoute) -> str:
119
+ # Inner route nodes, same style as flat route_str
120
+ inner_nodes = [
121
+ render_route(r) for r in mod.routes
122
+ ]
123
+ 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"
132
+ {inner_nodes_str}
133
+ {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)
141
+
123
142
 
124
143
  def render_dot(self, tags: list[Tag], routes: list[Route], nodes: list[SchemaNode], links: list[Link], spline_line=False) -> str:
125
- module_schemas = build_module_schema_tree(nodes)
126
- module_routes = build_module_route_tree(routes)
127
144
 
128
145
  tag_str = '\n'.join([
129
146
  f'''
@@ -134,9 +151,8 @@ class Renderer:
134
151
  ];''' for t in tags
135
152
  ])
136
153
 
137
-
138
- module_schemas_str = self.render_module_schema_content(module_schemas)
139
- module_routes_str = '\n'.join(self.render_module_route(m) for m in module_routes)
154
+ module_routes_str = self.render_module_route_content(routes)
155
+ module_schemas_str = self.render_module_schema_content(nodes)
140
156
  link_str = '\n'.join(self.render_link(link) for link in links)
141
157
 
142
158
  dot_str = f'''
fastapi_voyager/server.py CHANGED
@@ -22,6 +22,7 @@ class OptionParam(BaseModel):
22
22
  dot: str
23
23
  enable_brief_mode: bool
24
24
  version: str
25
+ swagger_url: Optional[str] = None
25
26
 
26
27
  class Payload(BaseModel):
27
28
  tags: Optional[list[str]] = None
@@ -32,10 +33,13 @@ class Payload(BaseModel):
32
33
  show_meta: bool = False
33
34
  brief: bool = False
34
35
  hide_primitive_route: bool = False
36
+ show_module: bool = True
37
+
35
38
 
36
39
  def create_route(
37
40
  target_app: FastAPI,
38
41
  module_color: dict[str, str] | None = None,
42
+ swagger_url: Optional[str] = None,
39
43
  module_prefix: Optional[str] = None,
40
44
  ):
41
45
  """
@@ -56,7 +60,13 @@ def create_route(
56
60
  schemas = voyager.nodes[:]
57
61
  schemas.sort(key=lambda s: s.name)
58
62
 
59
- return OptionParam(tags=tags, schemas=schemas, dot=dot, enable_brief_mode=bool(module_prefix), version=__version__)
63
+ return OptionParam(
64
+ tags=tags,
65
+ schemas=schemas,
66
+ dot=dot,
67
+ enable_brief_mode=bool(module_prefix),
68
+ version=__version__,
69
+ swagger_url=swagger_url)
60
70
 
61
71
  @router.post("/dot", response_class=PlainTextResponse)
62
72
  def get_filtered_dot(payload: Payload) -> str:
@@ -68,10 +78,14 @@ def create_route(
68
78
  module_color=module_color,
69
79
  route_name=payload.route_name,
70
80
  hide_primitive_route=payload.hide_primitive_route,
81
+ show_module=payload.show_module,
71
82
  )
72
83
  voyager.analysis(target_app)
73
84
  if payload.brief:
74
- return voyager.render_brief_dot(module_prefix=module_prefix)
85
+ if payload.tags:
86
+ return voyager.render_tag_level_brief_dot(module_prefix=module_prefix)
87
+ else:
88
+ return voyager.render_overall_brief_dot(module_prefix=module_prefix)
75
89
  else:
76
90
  return voyager.render_dot()
77
91
 
@@ -196,8 +210,9 @@ def create_voyager(
196
210
  module_color: dict[str, str] | None = None,
197
211
  gzip_minimum_size: int | None = 500,
198
212
  module_prefix: Optional[str] = None,
213
+ swagger_url: Optional[str] = None,
199
214
  ) -> FastAPI:
200
- router = create_route(target_app, module_color=module_color, module_prefix=module_prefix)
215
+ router = create_route(target_app, module_color=module_color, module_prefix=module_prefix, swagger_url=swagger_url)
201
216
 
202
217
  app = FastAPI(title="fastapi-voyager demo server")
203
218
  if gzip_minimum_size is not None and gzip_minimum_size >= 0:
fastapi_voyager/type.py CHANGED
@@ -22,6 +22,7 @@ class Tag(NodeBase):
22
22
  @dataclass
23
23
  class Route(NodeBase):
24
24
  module: str
25
+ unique_id: str = ''
25
26
  response_schema: str = ''
26
27
  is_primitive: bool = True
27
28
 
@@ -51,7 +52,8 @@ class ModuleNode:
51
52
  # - subset: schema -> schema (subset)
52
53
  # - parent: schema -> schema (inheritance)
53
54
  # - schema: schema -> schema (field reference)
54
- LinkType = Literal['schema', 'parent', 'tag_route', 'subset', 'route_to_schema']
55
+ # - tag_to_schema: tag -> schema (only happens in module prefix filtering, aka brief mode)
56
+ LinkType = Literal['schema', 'parent', 'tag_route', 'subset', 'route_to_schema', 'tag_to_schema']
55
57
 
56
58
  @dataclass
57
59
  class Link:
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.10.5"
2
+ __version__ = "0.11.2"
@@ -6,15 +6,13 @@ from fastapi_voyager.type_helper import (
6
6
  get_bases_fields,
7
7
  is_inheritance_of_pydantic_base,
8
8
  get_pydantic_fields,
9
- get_vscode_link,
10
- get_source,
11
9
  get_type_name,
12
10
  update_forward_refs,
13
11
  is_non_pydantic_type
14
12
  )
15
13
  from pydantic import BaseModel
16
14
  from fastapi_voyager.type import Route, SchemaNode, Link, Tag, LinkType, FieldType, PK, CoreData
17
- from fastapi_voyager.filter import filter_graph, filter_subgraph_by_module_prefix
15
+ from fastapi_voyager.filter import filter_graph, filter_subgraph_from_tag_to_schema_by_module_prefix, filter_subgraph_by_module_prefix
18
16
  from fastapi_voyager.render import Renderer
19
17
  import pydantic_resolve.constant as const
20
18
 
@@ -29,6 +27,7 @@ class Voyager:
29
27
  module_color: dict[str, str] | None = None,
30
28
  route_name: str | None = None,
31
29
  hide_primitive_route: bool = False,
30
+ show_module: bool = True
32
31
  ):
33
32
 
34
33
  self.routes: list[Route] = []
@@ -50,6 +49,7 @@ class Voyager:
50
49
  self.module_color = module_color or {}
51
50
  self.route_name = route_name
52
51
  self.hide_primitive_route = hide_primitive_route
52
+ self.show_module = show_module
53
53
 
54
54
 
55
55
  def _get_available_route(self, app: FastAPI):
@@ -123,6 +123,7 @@ class Voyager:
123
123
  id=route_id,
124
124
  name=route_name,
125
125
  module=route_module,
126
+ unique_id=route.unique_id,
126
127
  response_schema=get_type_name(route.response_model),
127
128
  is_primitive=is_primitive_response
128
129
  )
@@ -302,12 +303,13 @@ class Voyager:
302
303
  node_set=self.node_set,
303
304
  )
304
305
 
305
- renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema)
306
+ renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema, show_module=self.show_module)
306
307
 
307
308
  _tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
308
309
  return renderer.render_dot(_tags, _routes, _nodes, _links)
309
310
 
310
- def render_brief_dot(self, module_prefix: str | None = None):
311
+
312
+ def render_tag_level_brief_dot(self, module_prefix: str | None = None):
311
313
  _tags, _routes, _nodes, _links = filter_graph(
312
314
  schema=self.schema,
313
315
  schema_field=self.schema_field,
@@ -326,7 +328,31 @@ class Voyager:
326
328
  links=_links,
327
329
  )
328
330
 
329
- renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema)
331
+ renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema, show_module=self.show_module)
332
+
333
+ _tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
334
+ return renderer.render_dot(_tags, _routes, _nodes, _links, True)
335
+
336
+ def render_overall_brief_dot(self, module_prefix: str | None = None):
337
+ _tags, _routes, _nodes, _links = filter_graph(
338
+ schema=self.schema,
339
+ schema_field=self.schema_field,
340
+ tags=self.tags,
341
+ routes=self.routes,
342
+ nodes=self.nodes,
343
+ links=self.links,
344
+ node_set=self.node_set,
345
+ )
346
+
347
+ _tags, _routes, _nodes, _links = filter_subgraph_from_tag_to_schema_by_module_prefix(
348
+ module_prefix=module_prefix,
349
+ tags=_tags,
350
+ routes=_routes,
351
+ nodes=_nodes,
352
+ links=_links,
353
+ )
354
+
355
+ renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema, show_module=self.show_module)
330
356
 
331
357
  _tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
332
358
  return renderer.render_dot(_tags, _routes, _nodes, _links, True)
@@ -148,6 +148,7 @@
148
148
  side="right"
149
149
  style="border-left: 1px solid #888;"
150
150
  bordered
151
+ overlay
151
152
  >
152
153
  <!-- 可拖拽的调整栏 -->
153
154
  <div
@@ -218,6 +219,9 @@
218
219
  :name="state.tag == tag.name ? 'folder' : 'folder_open'"
219
220
  ></q-icon>
220
221
  <span>{{ tag.name }} <q-chip style="position:relative; top: -1px;" class="q-ml-sm" dense>{{ tag.routes.length }}</q-chip></span>
222
+ <a target="_blank" class="q-ml-sm" v-if="state.swaggerUrl" :href="state.swaggerUrl + '#/' + tag.name">
223
+ <q-icon size="small" name="link"></q-icon>
224
+ </a>
221
225
  </div>
222
226
  </template>
223
227
  <q-list separator style="overflow: auto; max-height: 60vh;">
@@ -237,6 +241,9 @@
237
241
  name="data_object"
238
242
  ></q-icon>
239
243
  {{ route.name }}
244
+ <a target="_blank" class="q-ml-sm" v-if="state.swaggerUrl" :href="state.swaggerUrl + '#/' + tag.name + '/' + route.unique_id">
245
+ <q-icon size="small" name="link"></q-icon>
246
+ </a>
240
247
  </span>
241
248
  </q-item-section>
242
249
  </q-item>
@@ -259,32 +266,44 @@
259
266
  <div style="position: relative; width: 100%; height: 100%;">
260
267
  <div id="graph" class="adjust-fit"></div>
261
268
  <div style="position: absolute; left: 8px; bottom: 8px; z-index: 10; background: rgba(255,255,255,0.85); border-radius: 4px; padding: 2px 8px;">
262
- <div>
269
+ <div class="q-mt-sm">
263
270
  <q-toggle
264
271
  v-model="state.focus"
265
272
  v-show="schemaCodeName"
266
273
  @update:model-value="val => onFocusChange(val)"
267
274
  label="Focus"
275
+ dense
268
276
  title="pick a schema and toggle focus on to display related nodes only"
269
277
  />
270
278
  </div>
271
- <div>
279
+ <div class="q-mt-sm">
272
280
  <q-toggle
273
281
  v-if="state.enableBriefMode"
282
+ dense
274
283
  v-model="state.brief"
275
284
  label="Brief Mode"
276
285
  @update:model-value="(val) => toggleBrief(val)"
277
286
  title="skip middle classes, config module_prefix to enable it"
278
287
  />
279
288
  </div>
280
- <div>
289
+ <div class="q-mt-sm">
281
290
  <q-toggle
282
291
  v-model="state.hidePrimitiveRoute"
283
292
  @update:model-value="(val) => toggleHidePrimitiveRoute(val)"
284
293
  label="Hide Primitive"
294
+ dense
285
295
  title="hide routes return primitive"
286
296
  />
287
297
  </div>
298
+ <div class="q-mt-sm">
299
+ <q-toggle
300
+ v-model="state.showModule"
301
+ @update:model-value="(val) => toggleShowModule(val)"
302
+ label="Show Module Cluster"
303
+ dense
304
+ title="show module cluster"
305
+ />
306
+ </div>
288
307
  </div>
289
308
  </div>
290
309
  </template>
@@ -24,6 +24,7 @@ const app = createApp({
24
24
  focus: false,
25
25
  hidePrimitiveRoute: false,
26
26
  generating: false,
27
+ swaggerUrl: null,
27
28
  rawTags: [], // [{ name, routes: [{ id, name }] }]
28
29
  rawSchemas: new Set(), // [{ name, id }]
29
30
  rawSchemasFull: {}, // full schemas dict: { [schema.id]: schema }
@@ -33,6 +34,7 @@ const app = createApp({
33
34
  detailDrawer: false,
34
35
  drawerWidth: 300, // drawer 宽度
35
36
  version: "", // version from backend
37
+ showModule: true
36
38
  });
37
39
 
38
40
  const showDetail = ref(false);
@@ -78,6 +80,7 @@ const app = createApp({
78
80
  }, {});
79
81
  state.enableBriefMode = data.enable_brief_mode || false;
80
82
  state.version = data.version || "";
83
+ state.swaggerUrl = data.swagger_url || null
81
84
 
82
85
  // default route options placeholder
83
86
  } catch (e) {
@@ -110,6 +113,7 @@ const app = createApp({
110
113
  show_fields: state.showFields,
111
114
  brief: state.brief,
112
115
  hide_primitive_route: state.hidePrimitiveRoute,
116
+ show_module: state.showModule,
113
117
  };
114
118
  const res = await fetch("dot", {
115
119
  method: "POST",
@@ -225,7 +229,6 @@ const app = createApp({
225
229
  state.routeId = "";
226
230
  state.schemaId = null;
227
231
  // state.showFields = "object";
228
- state.brief = false;
229
232
  state.focus = false;
230
233
  schemaCodeName.value = "";
231
234
  onGenerate();
@@ -260,6 +263,11 @@ const app = createApp({
260
263
  onGenerate();
261
264
  }
262
265
 
266
+ function toggleShowModule(val) {
267
+ state.showModule = val;
268
+ onGenerate()
269
+ }
270
+
263
271
  function toggleShowField(field) {
264
272
  state.showFields = field;
265
273
  onGenerate(false);
@@ -336,6 +344,7 @@ const app = createApp({
336
344
  toggleShowField,
337
345
  startDragDrawer,
338
346
  onFocusChange,
347
+ toggleShowModule
339
348
  };
340
349
  },
341
350
  });
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.10.5
3
+ Version: 0.11.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
@@ -16,6 +16,8 @@ Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
19
21
  Requires-Python: >=3.10
20
22
  Requires-Dist: fastapi>=0.110
21
23
  Requires-Dist: pydantic-resolve>=1.13.2
@@ -32,7 +34,14 @@ Description-Content-Type: text/markdown
32
34
 
33
35
  > This repo is still in early stage, it supports pydantic v2 only
34
36
 
35
- Inspect your API interactively!
37
+ FastAPI can help you:
38
+
39
+ - design your API
40
+ - inspect your API
41
+ - refactor your API
42
+
43
+ interactively !!
44
+
36
45
 
37
46
  [visit online demo](https://www.newsyeah.fun/voyager/) of project: [composition oriented development pattern](https://github.com/allmonday/composition-oriented-development-pattern)
38
47
 
@@ -51,6 +60,22 @@ uv add fastapi-voyager
51
60
  voyager -m path.to.your.app.module --server
52
61
  ```
53
62
 
63
+ ## Mount into project
64
+
65
+ ```python
66
+ from fastapi import FastAPI
67
+ from fastapi_voyager import create_voyager
68
+ from tests.demo import app
69
+
70
+ app.mount('/voyager', create_voyager(
71
+ app,
72
+ module_color={"tests.service": "red"},
73
+ module_prefix="tests.service"),
74
+ swagger_url="/docs")
75
+ ```
76
+
77
+ more about [sub application](https://fastapi.tiangolo.com/advanced/sub-applications/?h=sub)
78
+
54
79
 
55
80
  ## Feature
56
81
 
@@ -99,20 +124,6 @@ click a node to highlight it's upperstream and downstream nodes. figure out the
99
124
  <img width="882" height="445" alt="image" src="https://github.com/user-attachments/assets/158560ef-63ca-4991-9b7d-587be4fa04e4" />
100
125
 
101
126
 
102
- ## Mount to target project
103
-
104
- ```python
105
- from fastapi import FastAPI
106
- from fastapi_voyager import create_voyager
107
- from tests.demo import app
108
-
109
- app.mount('/voyager', create_voyager(
110
- app,
111
- module_color={"tests.service": "red"},
112
- module_prefix="tests.service"))
113
- ```
114
-
115
- more about [sub application](https://fastapi.tiangolo.com/advanced/sub-applications/?h=sub)
116
127
 
117
128
 
118
129
  ## Command Line Usage
@@ -244,6 +255,7 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
244
255
  - [x] fix focus in brief-mode
245
256
  - [x] ui: adjust focus position
246
257
  - [x] refactor naming
258
+ - [x] fix layout issue when rendering huge graph
247
259
  - 0.10.4
248
260
  - [x] fix: when focus is on, should ensure changes from other params not broken.
249
261
  - 0.10.5
@@ -251,19 +263,20 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
251
263
 
252
264
 
253
265
  #### 0.11
254
- - [ ] enable/disable module cluster (to save space)
255
- - [ ] fix layout issue when rendering huge graph
256
- - [ ] logging information
257
- - [ ] support opening route in swagger
258
- - config docs path
259
- - [ ] add tests
260
- - [ ] fix: focus should reset zoom
261
- - [ ] sort field name
262
- - [ ] set max limit for fields
263
- - [ ] add info icon alone with schema node
264
- - [ ] add loading for field detail panel
266
+ - 0.11.1
267
+ - [x] support opening route in swagger
268
+ - [x] config docs path
269
+ - [x] provide option to hide routes in brief mode (auto hide in full graph mode)
270
+ - 0.11.2
271
+ - [x] enable/disable module cluster (to save space)
272
+ - 0.11.3
273
+ - [ ] add loading for field detail panel
274
+ - [ ] logging information
275
+ - [ ] sort field name
276
+ - [ ] set max limit for fields
265
277
 
266
278
  #### 0.12
279
+ - [ ] add tests
267
280
  - [ ] integration with pydantic-resolve
268
281
  - [ ] show hint for resolve, post fields
269
282
  - [ ] display loader as edges
@@ -1,20 +1,20 @@
1
1
  fastapi_voyager/__init__.py,sha256=tZy0Nkj8kTaMgbvHy-mGxVcFGVX0Km-36dnzsAIG2uk,230
2
2
  fastapi_voyager/cli.py,sha256=kQb4g6JEGZR99e5r8LyFFEeb_-uT-n_gp_sDoYG3R7k,11118
3
- fastapi_voyager/filter.py,sha256=C718iwMjnnFXtqZISe9tzDRLAaY2FjbcE2LgkauEwnw,8095
3
+ fastapi_voyager/filter.py,sha256=GY2J9Vfsf_wbFwC-0t74-Lf-OlO77PnhEXD_rmgkfSw,11574
4
4
  fastapi_voyager/module.py,sha256=Z2QHNmiLk6ZAJlm2nSmO875Q33TweSg8UxZSzIpU9zY,3499
5
- fastapi_voyager/render.py,sha256=vvJja8iNdmTA2Rpjo27a2efNtXmDy0M5sxXArMc3huY,7801
6
- fastapi_voyager/server.py,sha256=6kCj906N4PVpKbUI8eq7bJ4RoET1kIQgUQUf-oMFdSY,6326
7
- fastapi_voyager/type.py,sha256=pWYKmgb9e0W_JeD7k54Mr2lxUZV_Ir9TNpewGRwHyHQ,1629
5
+ fastapi_voyager/render.py,sha256=vdwqIync2wsP8gMPY0v_XjRhdPBtbKyRT8yTBa_Ep3Y,8744
6
+ fastapi_voyager/server.py,sha256=TOzGEyzE_lviWMcKuSYHDaoArMApENIfvzHdAzOuKY4,6679
7
+ fastapi_voyager/type.py,sha256=VmcTB1G-LOT70EWCzi4LU_FUkSGWUIBJX15T_J5HnOo,1764
8
8
  fastapi_voyager/type_helper.py,sha256=hjBC4E0tgBpQDlYxGg74uK07SXjsrAgictEETJfIpYM,9231
9
- fastapi_voyager/version.py,sha256=wULyXwCchfYy1WR_Ks9GdSJBTrJSOiEpDh_5kS02PC8,49
10
- fastapi_voyager/voyager.py,sha256=r-_V0EPWwBM7N4hMlo-Si48mHVDc3zc9oGgTPCoEW_8,12377
9
+ fastapi_voyager/version.py,sha256=0HoMD0zNdXsQi-ZwAZecr3qNO9tE7hFLleGl1vf67AA,49
10
+ fastapi_voyager/voyager.py,sha256=hNal25S5Hi_ZRe-gnmdUKt8tnRd-BRCrzaybAEJ_1HI,13498
11
11
  fastapi_voyager/web/graph-ui.js,sha256=DTedkpZNbtufexONVkJ8mOwF_-VnvxoReYHtox6IKR4,5842
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=1W0QJUrQ1d-q9qdSD9Qax9uX6EJDNPUDGZsoO8T5I7g,16807
14
+ fastapi_voyager/web/index.html,sha256=9zp6id31DB_cWdAoCkO5NkeHCMTnNaz2VOdJO24__eM,17837
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=zVE2vJJTe6BQ0rK2POiawu55MNIxnkEWQ4EYK1bBYlQ,10434
17
+ fastapi_voyager/web/vue-main.js,sha256=sUjLmn06jYy1guwxAcouHwkcjxGD2UgZgkiiIEDbk04,10663
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
20
  fastapi_voyager/web/component/schema-code-display.js,sha256=UgFotzvqSuhnPXNOr6w_r1fV2_savRiCdokEvferutE,6244
@@ -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.10.5.dist-info/METADATA,sha256=V7jML-nsztkhELLUDX6RNxGw6tjcovVKprCa1aMetXw,9659
30
- fastapi_voyager-0.10.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
- fastapi_voyager-0.10.5.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
32
- fastapi_voyager-0.10.5.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
33
- fastapi_voyager-0.10.5.dist-info/RECORD,,
29
+ fastapi_voyager-0.11.2.dist-info/METADATA,sha256=ImBnVBoy2rqVkdOrd037aIgj9V8pRc0cN7dl5aXv3dM,9917
30
+ fastapi_voyager-0.11.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
+ fastapi_voyager-0.11.2.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
32
+ fastapi_voyager-0.11.2.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
33
+ fastapi_voyager-0.11.2.dist-info/RECORD,,