fastapi-voyager 0.10.5__py3-none-any.whl → 0.11.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/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
@@ -59,6 +59,8 @@ class Renderer:
59
59
  return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid, dashed", dir="back", minlen=3, taillabel = "< inherit >", color = "purple", tailport="n"];"""
60
60
  elif link.type == 'subset':
61
61
  return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid, dashed", dir="back", minlen=3, taillabel = "< subset >", color = "orange", tailport="n"];"""
62
+ elif link.type == 'tag_to_schema':
63
+ return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid", dir="back", arrowtail="odot", minlen=3];"""
62
64
  else:
63
65
  raise ValueError(f'Unknown link type: {link.type}')
64
66
 
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
@@ -33,9 +34,11 @@ class Payload(BaseModel):
33
34
  brief: bool = False
34
35
  hide_primitive_route: bool = False
35
36
 
37
+
36
38
  def create_route(
37
39
  target_app: FastAPI,
38
40
  module_color: dict[str, str] | None = None,
41
+ swagger_url: Optional[str] = None,
39
42
  module_prefix: Optional[str] = None,
40
43
  ):
41
44
  """
@@ -56,7 +59,13 @@ def create_route(
56
59
  schemas = voyager.nodes[:]
57
60
  schemas.sort(key=lambda s: s.name)
58
61
 
59
- return OptionParam(tags=tags, schemas=schemas, dot=dot, enable_brief_mode=bool(module_prefix), version=__version__)
62
+ return OptionParam(
63
+ tags=tags,
64
+ schemas=schemas,
65
+ dot=dot,
66
+ enable_brief_mode=bool(module_prefix),
67
+ version=__version__,
68
+ swagger_url=swagger_url)
60
69
 
61
70
  @router.post("/dot", response_class=PlainTextResponse)
62
71
  def get_filtered_dot(payload: Payload) -> str:
@@ -71,7 +80,10 @@ def create_route(
71
80
  )
72
81
  voyager.analysis(target_app)
73
82
  if payload.brief:
74
- return voyager.render_brief_dot(module_prefix=module_prefix)
83
+ if payload.tags:
84
+ return voyager.render_tag_level_brief_dot(module_prefix=module_prefix)
85
+ else:
86
+ return voyager.render_overall_brief_dot(module_prefix=module_prefix)
75
87
  else:
76
88
  return voyager.render_dot()
77
89
 
@@ -196,8 +208,9 @@ def create_voyager(
196
208
  module_color: dict[str, str] | None = None,
197
209
  gzip_minimum_size: int | None = 500,
198
210
  module_prefix: Optional[str] = None,
211
+ swagger_url: Optional[str] = None,
199
212
  ) -> FastAPI:
200
- router = create_route(target_app, module_color=module_color, module_prefix=module_prefix)
213
+ router = create_route(target_app, module_color=module_color, module_prefix=module_prefix, swagger_url=swagger_url)
201
214
 
202
215
  app = FastAPI(title="fastapi-voyager demo server")
203
216
  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.1"
@@ -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
 
@@ -123,6 +121,7 @@ class Voyager:
123
121
  id=route_id,
124
122
  name=route_name,
125
123
  module=route_module,
124
+ unique_id=route.unique_id,
126
125
  response_schema=get_type_name(route.response_model),
127
126
  is_primitive=is_primitive_response
128
127
  )
@@ -307,7 +306,8 @@ class Voyager:
307
306
  _tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
308
307
  return renderer.render_dot(_tags, _routes, _nodes, _links)
309
308
 
310
- def render_brief_dot(self, module_prefix: str | None = None):
309
+
310
+ def render_tag_level_brief_dot(self, module_prefix: str | None = None):
311
311
  _tags, _routes, _nodes, _links = filter_graph(
312
312
  schema=self.schema,
313
313
  schema_field=self.schema_field,
@@ -328,5 +328,29 @@ class Voyager:
328
328
 
329
329
  renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema)
330
330
 
331
+ _tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
332
+ return renderer.render_dot(_tags, _routes, _nodes, _links, True)
333
+
334
+ def render_overall_brief_dot(self, module_prefix: str | None = None):
335
+ _tags, _routes, _nodes, _links = filter_graph(
336
+ schema=self.schema,
337
+ schema_field=self.schema_field,
338
+ tags=self.tags,
339
+ routes=self.routes,
340
+ nodes=self.nodes,
341
+ links=self.links,
342
+ node_set=self.node_set,
343
+ )
344
+
345
+ _tags, _routes, _nodes, _links = filter_subgraph_from_tag_to_schema_by_module_prefix(
346
+ module_prefix=module_prefix,
347
+ tags=_tags,
348
+ routes=_routes,
349
+ nodes=_nodes,
350
+ links=_links,
351
+ )
352
+
353
+ renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema)
354
+
331
355
  _tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
332
356
  return renderer.render_dot(_tags, _routes, _nodes, _links, True)
@@ -218,6 +218,9 @@
218
218
  :name="state.tag == tag.name ? 'folder' : 'folder_open'"
219
219
  ></q-icon>
220
220
  <span>{{ tag.name }} <q-chip style="position:relative; top: -1px;" class="q-ml-sm" dense>{{ tag.routes.length }}</q-chip></span>
221
+ <a target="_blank" class="q-ml-sm" v-if="state.swaggerUrl" :href="state.swaggerUrl + '#/' + tag.name">
222
+ <q-icon size="small" name="link"></q-icon>
223
+ </a>
221
224
  </div>
222
225
  </template>
223
226
  <q-list separator style="overflow: auto; max-height: 60vh;">
@@ -237,6 +240,9 @@
237
240
  name="data_object"
238
241
  ></q-icon>
239
242
  {{ route.name }}
243
+ <a target="_blank" class="q-ml-sm" v-if="state.swaggerUrl" :href="state.swaggerUrl + '#/' + tag.name + '/' + route.unique_id">
244
+ <q-icon size="small" name="link"></q-icon>
245
+ </a>
240
246
  </span>
241
247
  </q-item-section>
242
248
  </q-item>
@@ -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 }
@@ -78,6 +79,7 @@ const app = createApp({
78
79
  }, {});
79
80
  state.enableBriefMode = data.enable_brief_mode || false;
80
81
  state.version = data.version || "";
82
+ state.swaggerUrl = data.swagger_url || null
81
83
 
82
84
  // default route options placeholder
83
85
  } catch (e) {
@@ -225,7 +227,6 @@ const app = createApp({
225
227
  state.routeId = "";
226
228
  state.schemaId = null;
227
229
  // state.showFields = "object";
228
- state.brief = false;
229
230
  state.focus = false;
230
231
  schemaCodeName.value = "";
231
232
  onGenerate();
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.10.5
3
+ Version: 0.11.1
4
4
  Summary: Visualize FastAPI application's routing tree and dependencies
5
5
  Project-URL: Homepage, https://github.com/allmonday/fastapi-voyager
6
6
  Project-URL: Source, https://github.com/allmonday/fastapi-voyager
@@ -244,6 +244,7 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
244
244
  - [x] fix focus in brief-mode
245
245
  - [x] ui: adjust focus position
246
246
  - [x] refactor naming
247
+ - [x] fix layout issue when rendering huge graph
247
248
  - 0.10.4
248
249
  - [x] fix: when focus is on, should ensure changes from other params not broken.
249
250
  - 0.10.5
@@ -251,19 +252,20 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
251
252
 
252
253
 
253
254
  #### 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
255
+ - 0.11.1
256
+ - [x] support opening route in swagger
257
+ - [x] config docs path
258
+ - [x] provide option to hide routes in brief mode (auto hide in full graph mode)
259
+ - 0.11.2
260
+ - [ ] enable/disable module cluster (to save space)
261
+ - [ ] logging information
262
+ - [ ] sort field name
263
+ - [ ] set max limit for fields
264
+ - [ ] add info icon alone with schema node
265
+ - [ ] add loading for field detail panel
265
266
 
266
267
  #### 0.12
268
+ - [ ] add tests
267
269
  - [ ] integration with pydantic-resolve
268
270
  - [ ] show hint for resolve, post fields
269
271
  - [ ] 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=EtSkQWb3k3g6T6pnDW2GF9BM-NkDkbDjhlsnF0u-IG0,7970
6
+ fastapi_voyager/server.py,sha256=UkTeMFduq4Ebt6eNisfPVUiy04XzHmOaZkBuwEk5mus,6617
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=mqCYzIn6okf6Z1_ZJPQL0K0ySha6En_onGIReGtmFxM,49
10
+ fastapi_voyager/voyager.py,sha256=etSIrqJJQXArxYLp_45wa0kKdqSebYy9TBYbuyK0iC4,13332
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=1WkNQ_xuS-oy4WkRChk53b9mZjATC5XZbIhPZ_pVBJQ,17317
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=ZM1BmFcA9hz6FmP5W-J1xxNE8mE_Rsj-5-dlh43zfcQ,10483
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.1.dist-info/METADATA,sha256=K47oTptR1SQCs7hgvlhJSX9FeDO0knh-QFtgjjfe8mY,9767
30
+ fastapi_voyager-0.11.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
+ fastapi_voyager-0.11.1.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
32
+ fastapi_voyager-0.11.1.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
33
+ fastapi_voyager-0.11.1.dist-info/RECORD,,