fastapi-voyager 0.15.4__py3-none-any.whl → 0.15.6__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.
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.15.4"
2
+ __version__ = "0.15.6"
@@ -1,4 +1,15 @@
1
1
  export class GraphUI {
2
+ // ====================
3
+ // Constants
4
+ // ====================
5
+
6
+ static HIGHLIGHT_COLOR = "#822dba"
7
+ static HIGHLIGHT_STROKE_WIDTH = "3.0"
8
+
9
+ // ====================
10
+ // Constructor
11
+ // ====================
12
+
2
13
  constructor(selector = "#graph", options = {}) {
3
14
  this.selector = selector
4
15
  this.options = options // e.g. { onSchemaClick: (name) => {} }
@@ -9,6 +20,10 @@ export class GraphUI {
9
20
  this._init()
10
21
  }
11
22
 
23
+ // ====================
24
+ // Highlight Methods
25
+ // ====================
26
+
12
27
  _highlight(mode = "bidirectional") {
13
28
  let highlightedNodes = $()
14
29
  for (const selection of this.currentSelection) {
@@ -68,49 +83,79 @@ export class GraphUI {
68
83
  return $result
69
84
  }
70
85
 
86
+ // ====================
87
+ // Schema Banner Methods
88
+ // ====================
89
+
71
90
  highlightSchemaBanner(node) {
72
- // Get all polygons in the node
73
91
  const polygons = node.querySelectorAll("polygon")
74
-
75
- // The first polygon is typically the outer frame of the entire node
76
92
  const outerFrame = polygons[0]
77
- // The second polygon is typically the title background
78
93
  const titleBg = polygons[1]
79
94
 
80
95
  if (outerFrame) {
81
- // Save original attributes for potential restoration
82
- if (!outerFrame.hasAttribute("data-original-stroke")) {
83
- outerFrame.setAttribute("data-original-stroke", outerFrame.getAttribute("stroke") || "")
84
- outerFrame.setAttribute(
85
- "data-original-stroke-width",
86
- outerFrame.getAttribute("stroke-width") || "1"
87
- )
88
- outerFrame.setAttribute("data-original-fill", outerFrame.getAttribute("fill") || "")
89
- }
90
-
91
- // Apply bold purple border to the outer frame
92
- outerFrame.setAttribute("stroke", "#822dba")
93
- outerFrame.setAttribute("stroke-width", "3.0")
96
+ this._saveOriginalAttributes(outerFrame)
97
+ outerFrame.setAttribute("stroke", GraphUI.HIGHLIGHT_COLOR)
98
+ outerFrame.setAttribute("stroke-width", GraphUI.HIGHLIGHT_STROKE_WIDTH)
94
99
  }
95
100
 
96
101
  if (titleBg) {
97
- // Save original attributes
98
- if (!titleBg.hasAttribute("data-original-stroke")) {
99
- titleBg.setAttribute("data-original-stroke", titleBg.getAttribute("stroke") || "")
100
- titleBg.setAttribute(
101
- "data-original-stroke-width",
102
- titleBg.getAttribute("stroke-width") || "1"
103
- )
104
- titleBg.setAttribute("data-original-fill", titleBg.getAttribute("fill") || "")
105
- }
102
+ this._saveOriginalAttributes(titleBg)
103
+ titleBg.setAttribute("fill", GraphUI.HIGHLIGHT_COLOR)
104
+ titleBg.setAttribute("stroke", GraphUI.HIGHLIGHT_COLOR)
105
+ }
106
+ }
106
107
 
107
- // Apply purple background to title
108
- titleBg.setAttribute("fill", "#822dba")
109
- // Also update the stroke to match
110
- titleBg.setAttribute("stroke", "#822dba")
108
+ clearSchemaBanners() {
109
+ if (this.gv) {
110
+ this.gv.highlight()
111
+ }
112
+
113
+ const allPolygons = document.querySelectorAll("polygon[data-original-stroke]")
114
+ allPolygons.forEach((polygon) => {
115
+ polygon.removeAttribute("data-original-stroke")
116
+ polygon.removeAttribute("data-original-stroke-width")
117
+ polygon.removeAttribute("data-original-fill")
118
+ })
119
+ }
120
+
121
+ _saveOriginalAttributes(element) {
122
+ if (!element.hasAttribute("data-original-stroke")) {
123
+ element.setAttribute("data-original-stroke", element.getAttribute("stroke") || "")
124
+ element.setAttribute(
125
+ "data-original-stroke-width",
126
+ element.getAttribute("stroke-width") || "1"
127
+ )
128
+ element.setAttribute("data-original-fill", element.getAttribute("fill") || "")
111
129
  }
112
130
  }
113
131
 
132
+ _applyNodeHighlight(node) {
133
+ const set = $()
134
+ set.push(node)
135
+ const obj = { set, direction: "bidirectional" }
136
+
137
+ this.clearSchemaBanners()
138
+ this.currentSelection = [obj]
139
+ this._highlight()
140
+
141
+ return obj
142
+ }
143
+
144
+ _triggerCallback(callbackName, schemaName) {
145
+ const callback = this.options[callbackName]
146
+ if (callback && schemaName) {
147
+ try {
148
+ callback(schemaName)
149
+ } catch (e) {
150
+ console.warn(`${callbackName} callback failed`, e)
151
+ }
152
+ }
153
+ }
154
+
155
+ // ====================
156
+ // Initialization & Events
157
+ // ====================
158
+
114
159
  _init() {
115
160
  const self = this
116
161
  $(this.selector).graphviz({
@@ -127,80 +172,64 @@ export class GraphUI {
127
172
 
128
173
  nodes.on("dblclick.graphui", function (event) {
129
174
  event.stopPropagation()
175
+
176
+ self._applyNodeHighlight(this)
177
+
130
178
  try {
131
179
  self.highlightSchemaBanner(this)
132
180
  } catch (e) {
133
181
  console.log(e)
134
182
  }
135
- const set = $()
136
- set.push(this)
137
- const schemaName = event.currentTarget.dataset.name
138
- if (schemaName) {
139
- try {
140
- self.options.onSchemaClick(schemaName)
141
- } catch (e) {
142
- console.warn("onSchemaClick callback failed", e)
143
- }
144
- }
183
+
184
+ self._triggerCallback("onSchemaClick", event.currentTarget.dataset.name)
145
185
  })
146
186
 
147
187
  edges.on("click.graphui", function (event) {
188
+ const [upStreamNode, downStreamNode] = event.currentTarget.dataset.name.split("->")
189
+ const nodes = self.gv.nodesByName()
190
+
148
191
  const up = $()
149
192
  const down = $()
150
193
  const edge = $()
151
- const [upStreamNode, downStreamNode] = event.currentTarget.dataset.name.split("->")
152
- const nodes = self.gv.nodesByName()
194
+
153
195
  up.push(nodes[upStreamNode])
154
196
  down.push(nodes[downStreamNode])
155
197
  edge.push(this)
156
- const upObj = { set: up, direction: "upstream" }
157
- const downObj = { set: down, direction: "downstream" }
158
- const edgeOjb = { set: edge, direction: "single" }
159
- self.currentSelection = [upObj, downObj, edgeOjb]
198
+
199
+ self.currentSelection = [
200
+ { set: up, direction: "upstream" },
201
+ { set: down, direction: "downstream" },
202
+ { set: edge, direction: "single" },
203
+ ]
160
204
 
161
205
  self._highlightEdgeNodes()
162
206
  })
163
207
 
164
208
  nodes.on("click.graphui", function (event) {
165
- const set = $()
166
- set.push(this)
167
- const obj = { set, direction: "bidirectional" }
168
-
169
- const schemaName = event.currentTarget.dataset.name
170
- console.log("shift click detected")
171
- if (event.shiftKey && self.options.onSchemaShiftClick) {
172
- if (schemaName) {
173
- try {
174
- self.options.onSchemaShiftClick(schemaName)
175
- } catch (e) {
176
- console.warn("onSchemaShiftClick callback failed", e)
177
- }
178
- }
209
+ if (event.shiftKey) {
210
+ self._triggerCallback("onSchemaShiftClick", event.currentTarget.dataset.name)
179
211
  } else {
180
- self.currentSelection = [obj]
181
- self._highlight()
212
+ self._applyNodeHighlight(this)
182
213
  }
183
214
  })
184
215
 
185
216
  $(document)
186
217
  .off("click.graphui")
187
218
  .on("click.graphui", function (evt) {
188
- // if outside container, do nothing
189
219
  const graphContainer = $(self.selector)[0]
190
220
  if (!graphContainer || !evt.target || !graphContainer.contains(evt.target)) {
191
221
  return
192
222
  }
193
223
 
194
- let isNode = false
195
224
  const $everything = self.gv.$nodes.add(self.gv.$edges).add(self.gv.$clusters)
196
225
  const node = evt.target.parentNode
197
- $everything.each(function () {
198
- if (this === node) {
199
- isNode = true
200
- }
226
+ const isNode = $everything.is(function () {
227
+ return this === node
201
228
  })
229
+
202
230
  if (!isNode && self.gv) {
203
- self.gv.highlight()
231
+ self.clearSchemaBanners()
232
+
204
233
  if (self.options.resetCb) {
205
234
  self.options.resetCb()
206
235
  }
@@ -210,6 +239,10 @@ export class GraphUI {
210
239
  })
211
240
  }
212
241
 
242
+ // ====================
243
+ // Render Method
244
+ // ====================
245
+
213
246
  async render(dotSrc, resetZoom = true) {
214
247
  const height = this.options.height || "100%"
215
248
  return new Promise((resolve, reject) => {
@@ -17,6 +17,13 @@
17
17
 
18
18
  GraphvizSvg.GVPT_2_PX = 32.5 // used to ease removal of extra space
19
19
 
20
+ // SVG element selectors for color manipulation
21
+ // NOTE: If you need to add more element types for highlighting/dimming,
22
+ // update SHAPE_ELEMENTS and the code will automatically handle them
23
+ GraphvizSvg.SHAPE_ELEMENTS = "polygon, ellipse, path, polyline"
24
+ GraphvizSvg.TEXT_ELEMENTS = "text"
25
+ GraphvizSvg.ALL_COLOR_ELEMENTS = GraphvizSvg.SHAPE_ELEMENTS + ", " + GraphvizSvg.TEXT_ELEMENTS
26
+
20
27
  GraphvizSvg.DEFAULTS = {
21
28
  url: null,
22
29
  svg: null,
@@ -178,8 +185,8 @@
178
185
  this.setInteractiveCursor($el, type === "edge")
179
186
  }
180
187
 
181
- // save the colors of the paths, ellipses and polygons
182
- $el.find("polygon, ellipse, path").each(function () {
188
+ // Save the colors of shape elements (polygon, ellipse, path, polyline)
189
+ $el.find(GraphvizSvg.SHAPE_ELEMENTS).each(function () {
183
190
  var $this = $(this)
184
191
  if ($this.attr("data-graphviz-hitbox") === "true") {
185
192
  return
@@ -196,6 +203,20 @@
196
203
  }
197
204
  })
198
205
 
206
+ // Save the colors of text elements
207
+ $el.find(GraphvizSvg.TEXT_ELEMENTS).each(function () {
208
+ var $this = $(this)
209
+ // text elements might not have explicit fill attribute, use black as default
210
+ var fill = $this.attr("fill")
211
+ if (!fill || fill === "none") {
212
+ fill = "#000000" // default black color for text
213
+ }
214
+ $this.data("graphviz.svg.color", {
215
+ fill: fill,
216
+ stroke: $this.attr("stroke"),
217
+ })
218
+ })
219
+
199
220
  // save the node name and check if theres a comment above; save it
200
221
  var $title = $el.children("title")
201
222
  if ($title[0]) {
@@ -404,6 +425,57 @@
404
425
  return retval
405
426
  }
406
427
 
428
+ // Helper function to apply color transformation to elements
429
+ GraphvizSvg.prototype._applyColorToElements = function (
430
+ $elements,
431
+ colorTransformer,
432
+ bgColor,
433
+ setStrokeWidth
434
+ ) {
435
+ var that = this
436
+ $elements.each(function () {
437
+ var $this = $(this)
438
+ if ($this.attr("data-graphviz-hitbox") === "true") {
439
+ return
440
+ }
441
+ var color = $this.data("graphviz.svg.color")
442
+ if (color) {
443
+ if (color.fill && color.fill != "none") {
444
+ $this.attr("fill", colorTransformer(color.fill, bgColor))
445
+ }
446
+ if (color.stroke && color.stroke != "none") {
447
+ $this.attr("stroke", colorTransformer(color.stroke, bgColor))
448
+ }
449
+ if (setStrokeWidth !== undefined) {
450
+ $this.attr("stroke-width", setStrokeWidth)
451
+ }
452
+ }
453
+ })
454
+ }
455
+
456
+ // Helper function to restore original colors
457
+ GraphvizSvg.prototype._restoreElementColors = function ($elements, setStrokeWidth) {
458
+ var that = this
459
+ $elements.each(function () {
460
+ var $this = $(this)
461
+ if ($this.attr("data-graphviz-hitbox") === "true") {
462
+ return
463
+ }
464
+ var color = $this.data("graphviz.svg.color")
465
+ if (color) {
466
+ if (color.fill && color.fill != "none") {
467
+ $this.attr("fill", color.fill)
468
+ }
469
+ if (color.stroke && color.stroke != "none") {
470
+ $this.attr("stroke", color.stroke)
471
+ }
472
+ if (setStrokeWidth !== undefined) {
473
+ $this.attr("stroke-width", setStrokeWidth)
474
+ }
475
+ }
476
+ })
477
+ }
478
+
407
479
  GraphvizSvg.prototype.findEdge = function (nodeName, testEdge, $retval) {
408
480
  var retval = []
409
481
  for (var name in this._edgesByName) {
@@ -439,37 +511,14 @@
439
511
 
440
512
  GraphvizSvg.prototype.colorElement = function ($el, getColor) {
441
513
  var bg = this.$element.css("background")
442
- $el.find("polygon, ellipse, path").each(function () {
443
- var $this = $(this)
444
- if ($this.attr("data-graphviz-hitbox") === "true") {
445
- return
446
- }
447
- var color = $this.data("graphviz.svg.color")
448
- if (color.fill && color.fill != "none") {
449
- $this.attr("fill", getColor(color.fill, bg)) // don't set fill if it's a path
450
- }
451
- if (color.stroke && color.stroke != "none") {
452
- $this.attr("stroke", getColor(color.stroke, bg))
453
- }
454
- $this.attr("stroke-width", 1.6)
455
- })
514
+
515
+ // Apply color transformation to all elements (shapes + text)
516
+ this._applyColorToElements($el.find(GraphvizSvg.ALL_COLOR_ELEMENTS), getColor, bg)
456
517
  }
457
518
 
458
519
  GraphvizSvg.prototype.restoreElement = function ($el) {
459
- $el.find("polygon, ellipse, path").each(function () {
460
- var $this = $(this)
461
- if ($this.attr("data-graphviz-hitbox") === "true") {
462
- return
463
- }
464
- var color = $this.data("graphviz.svg.color")
465
- if (color.fill && color.fill != "none") {
466
- $this.attr("fill", color.fill) // don't set fill if it's a path
467
- }
468
- if (color.stroke && color.stroke != "none") {
469
- $this.attr("stroke", color.stroke)
470
- }
471
- $this.attr("stroke-width", 1)
472
- })
520
+ // Restore original colors for all elements (shapes + text)
521
+ this._restoreElementColors($el.find(GraphvizSvg.ALL_COLOR_ELEMENTS), 1)
473
522
  }
474
523
 
475
524
  // methods users can actually call
@@ -325,7 +325,7 @@ const actions = {
325
325
  onGenerate()
326
326
  return
327
327
  }
328
- renderBasedOnInitialPolicy()
328
+ renderBasedOnInitialPolicy(onGenerate)
329
329
  }
330
330
 
331
331
  // default route options placeholder
@@ -0,0 +1,303 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-voyager
3
+ Version: 0.15.6
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.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: fastapi>=0.110
22
+ Requires-Dist: jinja2>=3.0.0
23
+ Requires-Dist: pydantic-resolve>=2.4.3
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
+ # FastAPI Voyager
36
+
37
+ Visualize your FastAPI endpoints and explore them interactively.
38
+
39
+ Its vision is to make code easier to read and understand, serving as an ideal documentation tool.
40
+
41
+ > This repo is still in early stage, it supports Pydantic v2 only.
42
+
43
+ - **Live Demo**: https://www.newsyeah.fun/voyager/
44
+ - **Example Source**: [composition-oriented-development-pattern](https://github.com/allmonday/composition-oriented-development-pattern)
45
+
46
+ <img width="1597" height="933" alt="fastapi-voyager overview" src="https://github.com/user-attachments/assets/020bf5b2-6c69-44bf-ba1f-39389d388d27" />
47
+
48
+ ## Table of Contents
49
+
50
+ - [Quick Start](#quick-start)
51
+ - [Installation](#installation)
52
+ - [Features](#features)
53
+ - [Command Line Usage](#command-line-usage)
54
+ - [About pydantic-resolve](#about-pydantic-resolve)
55
+ - [Development](#development)
56
+ - [Dependencies](#dependencies)
57
+ - [Credits](#credits)
58
+
59
+ ## Quick Start
60
+
61
+ With simple configuration, fastapi-voyager can be embedded into FastAPI:
62
+
63
+ ```python
64
+ from fastapi import FastAPI
65
+ from fastapi_voyager import create_voyager
66
+
67
+ app = FastAPI()
68
+
69
+ app.mount('/voyager',
70
+ create_voyager(
71
+ app,
72
+ module_color={'src.services': 'tomato'},
73
+ module_prefix='src.services',
74
+ swagger_url="/docs",
75
+ ga_id="G-XXXXXXXXVL",
76
+ initial_page_policy='first',
77
+ online_repo_url='https://github.com/allmonday/composition-oriented-development-pattern/blob/master',
78
+ enable_pydantic_resolve_meta=True))
79
+ ```
80
+
81
+ Visit `http://localhost:8000/voyager` to explore your API visually.
82
+
83
+ [View full example](https://github.com/allmonday/composition-oriented-development-pattern/blob/master/src/main.py#L48)
84
+
85
+ ## Installation
86
+
87
+ ### Install via pip
88
+
89
+ ```bash
90
+ pip install fastapi-voyager
91
+ ```
92
+
93
+ ### Install via uv
94
+
95
+ ```bash
96
+ uv add fastapi-voyager
97
+ ```
98
+
99
+ ### Run with CLI
100
+
101
+ ```bash
102
+ voyager -m path.to.your.app.module --server
103
+ ```
104
+
105
+ For sub-application scenarios (e.g., `app.mount("/api", api)`), specify the app name:
106
+
107
+ ```bash
108
+ voyager -m path.to.your.app.module --server --app api
109
+ ```
110
+
111
+ > **Note**: [Sub-Application mounts](https://fastapi.tiangolo.com/advanced/sub-applications/) are not supported yet, but you can specify the name of the FastAPI application with `--app`. Only a single application (default: `app`) can be selected.
112
+
113
+ ## Features
114
+
115
+ fastapi-voyager is designed for scenarios using FastAPI as internal API integration endpoints. It helps visualize dependencies and serves as an architecture tool to identify implementation issues such as wrong relationships, overfetching, and more.
116
+
117
+ **Best Practice**: When building view models following the ER model pattern, fastapi-voyager can fully realize its potential - quickly identifying which APIs use specific entities and vice versa.
118
+
119
+ ### Highlight Nodes and Links
120
+
121
+ Click a node to highlight its upstream and downstream nodes. Figure out the related models of one page, or how many pages are related with one model.
122
+
123
+ <img width="1100" height="700" alt="highlight nodes and dependencies" src="https://github.com/user-attachments/assets/3e0369ea-5fa4-469a-82c1-ed57d407e53d" />
124
+
125
+ ### View Source Code
126
+
127
+ Double-click a node or route to show source code or open the file in VSCode.
128
+
129
+ <img width="1297" height="940" alt="view source code" src="https://github.com/user-attachments/assets/c8bb2e7d-b727-42a6-8c9e-64dce297d2d8" />
130
+
131
+ ### Quick Search
132
+
133
+ Search schemas by name and display their upstream and downstream dependencies. Use `Shift + Click` on any node to quickly search for it.
134
+
135
+ <img width="1587" height="873" alt="quick search functionality" src="https://github.com/user-attachments/assets/ee4716f3-233d-418f-bc0e-3b214d1498f7" />
136
+
137
+ ### Display ER Diagram
138
+
139
+ ER diagram is a feature from pydantic-resolve which provides a solid expression for business descriptions. You can visualize application-level entity relationship diagrams.
140
+
141
+ ```python
142
+ from pydantic_resolve import ErDiagram, Entity, Relationship
143
+
144
+ diagram = ErDiagram(
145
+ configs=[
146
+ Entity(
147
+ kls=Team,
148
+ relationships=[
149
+ Relationship(field='id', target_kls=list[Sprint], loader=sprint_loader.team_to_sprint_loader),
150
+ Relationship(field='id', target_kls=list[User], loader=user_loader.team_to_user_loader)
151
+ ]
152
+ ),
153
+ Entity(
154
+ kls=Sprint,
155
+ relationships=[
156
+ Relationship(field='id', target_kls=list[Story], loader=story_loader.sprint_to_story_loader)
157
+ ]
158
+ ),
159
+ Entity(
160
+ kls=Story,
161
+ relationships=[
162
+ Relationship(field='id', target_kls=list[Task], loader=task_loader.story_to_task_loader),
163
+ Relationship(field='owner_id', target_kls=User, loader=user_loader.user_batch_loader)
164
+ ]
165
+ ),
166
+ Entity(
167
+ kls=Task,
168
+ relationships=[
169
+ Relationship(field='owner_id', target_kls=User, loader=user_loader.user_batch_loader)
170
+ ]
171
+ )
172
+ ]
173
+ )
174
+
175
+ # Display in voyager
176
+ app.mount('/voyager',
177
+ create_voyager(
178
+ app,
179
+ er_diagram=diagram
180
+ ))
181
+ ```
182
+
183
+ <img width="1276" height="613" alt="ER diagram visualization" src="https://github.com/user-attachments/assets/ea0091bb-ee11-4f71-8be3-7129d956c910" />
184
+
185
+ ### Show Pydantic Resolve Meta Info
186
+
187
+ Set `enable_pydantic_resolve_meta=True` in `create_voyager`, then toggle the "pydantic resolve meta" button to visualize resolve/post/expose/collect operations.
188
+
189
+ <img width="1604" height="535" alt="pydantic resolve meta information" src="https://github.com/user-attachments/assets/d1639555-af41-4a08-9970-4b8ef314596a" />
190
+
191
+ ## Command Line Usage
192
+
193
+ ### Start Server
194
+
195
+ ```bash
196
+ # Open in browser (default port 8000)
197
+ voyager -m tests.demo --server
198
+
199
+ # Custom port
200
+ voyager -m tests.demo --server --port=8002
201
+
202
+ # Specify app name
203
+ voyager -m tests.demo --server --app my_app
204
+ ```
205
+
206
+ ### Generate DOT File
207
+
208
+ ```bash
209
+ # Generate .dot file
210
+ voyager -m tests.demo
211
+
212
+ # Specify app
213
+ voyager -m tests.demo --app my_app
214
+
215
+ # Filter by schema
216
+ voyager -m tests.demo --schema Task
217
+
218
+ # Show all fields
219
+ voyager -m tests.demo --show_fields all
220
+
221
+ # Custom module colors
222
+ voyager -m tests.demo --module_color=tests.demo:red --module_color=tests.service:tomato
223
+
224
+ # Output to file
225
+ voyager -m tests.demo -o my_visualization.dot
226
+
227
+ # Version and help
228
+ voyager --version
229
+ voyager --help
230
+ ```
231
+
232
+ ## About pydantic-resolve
233
+
234
+ pydantic-resolve is a lightweight tool designed to build complex, nested data in a simple, declarative way. In v2, it introduced an important feature: **ER Diagram**, and fastapi-voyager has supported this feature, allowing for a clearer understanding of business relationships.
235
+
236
+ The ~~`@ensure_subset` decorator~~ `DefineSubset` metaclass helps safely pick fields from the 'source class' while **indicating the reference** from the current class to the base class.
237
+
238
+ Developers can use fastapi-voyager without needing to know anything about pydantic-resolve, but I still highly recommend everyone to give it a try.
239
+
240
+ ## Development
241
+
242
+ ### Setup Development Environment
243
+
244
+ ```bash
245
+ # Fork and clone the repository
246
+ git clone https://github.com/your-username/fastapi-voyager.git
247
+ cd fastapi-voyager
248
+
249
+ # Install uv
250
+ curl -LsSf https://astral.sh/uv/install.sh | sh
251
+
252
+ # Create virtual environment and install dependencies
253
+ uv venv
254
+ source .venv/bin/activate
255
+ uv pip install ".[dev]"
256
+
257
+ # Run development server
258
+ uvicorn tests.programatic:app --reload
259
+ ```
260
+
261
+ Visit `http://localhost:8000/voyager` to see changes.
262
+
263
+ ### Setup Git Hooks (Optional)
264
+
265
+ Enable automatic code formatting before commits:
266
+
267
+ ```bash
268
+ ./setup-hooks.sh
269
+ # or manually:
270
+ git config core.hooksPath .githooks
271
+ ```
272
+
273
+ This will run Prettier automatically before each commit. See [`.githooks/README.md`](./.githooks/README.md) for details.
274
+
275
+ ### Project Structure
276
+
277
+ **Frontend:**
278
+ - `src/fastapi_voyager/web/vue-main.js` - Main JavaScript entry
279
+
280
+ **Backend:**
281
+ - `voyager.py` - Main entry point
282
+ - `render.py` - Generate DOT files
283
+ - `server.py` - Server mode
284
+
285
+ ## Roadmap
286
+
287
+ - [Ideas](./docs/idea.md)
288
+ - [Changelog & Roadmap](./docs/changelog.md)
289
+
290
+ ## Dependencies
291
+
292
+ - [FastAPI](https://fastapi.tiangolo.com/)
293
+ - [pydantic-resolve](https://github.com/allmonday/pydantic-resolve)
294
+ - [Quasar Framework](https://quasar.dev/)
295
+
296
+ ## Credits
297
+
298
+ - [graphql-voyager](https://apis.guru/graphql-voyager/) - Thanks for inspiration
299
+ - [vscode-interactive-graphviz](https://github.com/tintinweb/vscode-interactive-graphviz) - Thanks for web visualization
300
+
301
+ ## License
302
+
303
+ MIT License
@@ -9,7 +9,7 @@ fastapi_voyager/render_style.py,sha256=mPOuChEl71-3agCbPwkMt2sFmax2AEKDI6dK90eFP
9
9
  fastapi_voyager/server.py,sha256=E0gGU7D1pBvnV7gFLBUvnkwtiBFbU1PbxEXHHSIA1oA,9115
10
10
  fastapi_voyager/type.py,sha256=zluWvh5vpnjXJ9aAmyNJTSmXZPjAHCvgRT5oQRAjHrg,2104
11
11
  fastapi_voyager/type_helper.py,sha256=FmfrZAI3Z4uDdh3sH_kH7UGoY6yNVPapneSN86qY_wo,10209
12
- fastapi_voyager/version.py,sha256=AgJGrnhFIPGdn_tW4I4oEPuyTOgMXEkK0I5qVj2kag0,49
12
+ fastapi_voyager/version.py,sha256=ILB2INUiq4EGupYfMT2L0gsR_3dHm9fPDuoUnU5BSf0,49
13
13
  fastapi_voyager/voyager.py,sha256=4vonmL-xt54C5San-DRBq4mjoV8Q96eoWRy68MJ1IJw,14169
14
14
  fastapi_voyager/templates/dot/cluster.j2,sha256=I2z9KkfCzmAtqXe0gXBnxnOfBXUSpdlATs3uf-O8_B8,307
15
15
  fastapi_voyager/templates/dot/cluster_container.j2,sha256=2tH1mOJvPoVKE_aHVMR3t06TfH_dYa9OeH6DBqSHt_A,204
@@ -24,13 +24,13 @@ fastapi_voyager/templates/html/pydantic_meta.j2,sha256=_tsSqjucs_QrAlPIVRy9u6I2-
24
24
  fastapi_voyager/templates/html/schema_field_row.j2,sha256=KfKexHO_QJV-OIJS0eiY_7fqA8031fWpD2g2wTv4BuE,111
25
25
  fastapi_voyager/templates/html/schema_header.j2,sha256=9WpuHLy3Zbv5GHG08qqaj5Xf-gaR-79ErBYuANZp7iA,179
26
26
  fastapi_voyager/templates/html/schema_table.j2,sha256=rzphiGk1il7uv4Gr2p_HLPHqyLZk63vLrGAmIduTdSE,117
27
- fastapi_voyager/web/graph-ui.js,sha256=4gEkXTgbA6CouD4IDMW5yKYfJTxHN2vL9G0CAr6w4qA,7662
27
+ fastapi_voyager/web/graph-ui.js,sha256=9b2auyGWEpUcF65YV231GNojQ9Uk6FsT1SlRR3lSYnc,7664
28
28
  fastapi_voyager/web/graphviz.svg.css,sha256=K218ov_mdSe3ga4KwhiBB92ynVvm5zaAk9_D9a3d8hE,1546
29
- fastapi_voyager/web/graphviz.svg.js,sha256=deI815RgxpZ3_MpELeV-TBYy2MVuUvZtQOHfS3aeXHY,18203
29
+ fastapi_voyager/web/graphviz.svg.js,sha256=VokgCghvP4zm3SFiFVPIikdW6XzjkZJXQkBbCrEitug,19885
30
30
  fastapi_voyager/web/index.html,sha256=wM9vJ_UfHR8p98F6SEMCKKjJcBEl0EyosWuPqVZYXvA,23496
31
31
  fastapi_voyager/web/quasar.min.css,sha256=F5jQe7X2XT54VlvAaa2V3GsBFdVD-vxDZeaPLf6U9CU,203145
32
32
  fastapi_voyager/web/quasar.min.js,sha256=h0ftyPMW_CRiyzeVfQqiup0vrVt4_QWojpqmpnpn07E,502974
33
- fastapi_voyager/web/store.js,sha256=7anY7HGF7aEx1xpXWz58Ca5WbnYCRnzdj7kqNjcaT_M,15103
33
+ fastapi_voyager/web/store.js,sha256=vwiqeLw7DfTOUDu9oN7b6nSBw8KG6W-5Dc-voQ1leL8,15113
34
34
  fastapi_voyager/web/vue-main.js,sha256=4lJi6ADrcaOzjXcQePkp7CyiCkAnvhnO-nkF3E6G3s4,10650
35
35
  fastapi_voyager/web/component/demo.js,sha256=sAklFGhKGmMy9-ofgOw2oPIidAoIOgHu6yvV51L_MAA,350
36
36
  fastapi_voyager/web/component/render-graph.js,sha256=9wnO70n3eyPKTpa744idgs5PSwgvzbfv4InZ68eEOKs,2454
@@ -43,8 +43,8 @@ fastapi_voyager/web/icon/favicon-16x16.png,sha256=JC07jEzfIYxBIoQn_FHXvyHuxESdhW
43
43
  fastapi_voyager/web/icon/favicon-32x32.png,sha256=C7v1h58cfWOsiLp9yOIZtlx-dLasBcq3NqpHVGRmpt4,1859
44
44
  fastapi_voyager/web/icon/favicon.ico,sha256=tZolYIXkkBcFiYl1A8ksaXN2VjGamzcSdes838dLvNc,15406
45
45
  fastapi_voyager/web/icon/site.webmanifest,sha256=GRozZ5suTykYcPMap1QhjrAB8PLW0mbT_phhzw_utvQ,316
46
- fastapi_voyager-0.15.4.dist-info/METADATA,sha256=6BftnkJpr0w8cKiAubLyJ6ZiOeGmvlkOwE9jZLZ5UMM,8513
47
- fastapi_voyager-0.15.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
- fastapi_voyager-0.15.4.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
49
- fastapi_voyager-0.15.4.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
50
- fastapi_voyager-0.15.4.dist-info/RECORD,,
46
+ fastapi_voyager-0.15.6.dist-info/METADATA,sha256=D-09okYHB5uaatoDc5SqmOVr_jTbw18o4_76pmxO4_g,9870
47
+ fastapi_voyager-0.15.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
+ fastapi_voyager-0.15.6.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
49
+ fastapi_voyager-0.15.6.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
50
+ fastapi_voyager-0.15.6.dist-info/RECORD,,
@@ -1,256 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: fastapi-voyager
3
- Version: 0.15.4
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.10
16
- Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.12
18
- Classifier: Programming Language :: Python :: 3.13
19
- Classifier: Programming Language :: Python :: 3.14
20
- Requires-Python: >=3.10
21
- Requires-Dist: fastapi>=0.110
22
- Requires-Dist: jinja2>=3.0.0
23
- Requires-Dist: pydantic-resolve>=2.4.3
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
- Visualize your FastAPI endpoints, and explore them interactively.
36
-
37
- Its vision is to make code easier to read and understand, serving as an ideal documentation tool.
38
-
39
- > This repo is still in early stage, it supports pydantic v2 only
40
-
41
- visit [live demo](https://www.newsyeah.fun/voyager/)
42
- source code:[composition oriented development pattern](https://github.com/allmonday/composition-oriented-development-pattern)
43
-
44
- <img width="1597" height="933" alt="image" src="https://github.com/user-attachments/assets/020bf5b2-6c69-44bf-ba1f-39389d388d27" />
45
-
46
- with simple configuration it can be embedded into FastAPI.
47
-
48
- ```python
49
- app.mount('/voyager',
50
- create_voyager(
51
- app,
52
- module_color={'src.services': 'tomato'},
53
- module_prefix='src.services',
54
- swagger_url="/docs",
55
- ga_id="G-XXXXXXXXVL",
56
- initial_page_policy='first',
57
- online_repo_url='https://github.com/allmonday/composition-oriented-development-pattern/blob/master',
58
- enable_pydantic_resolve_meta=True))
59
- ```
60
-
61
- https://github.com/allmonday/composition-oriented-development-pattern/blob/master/src/main.py#L48
62
-
63
- ## Plan & Raodmap
64
- - [ideas](./docs/idea.md)
65
- - [changelog & roadmap](./docs/changelog.md)
66
-
67
- ## Installation
68
-
69
- ```bash
70
- pip install fastapi-voyager
71
- # or
72
- uv add fastapi-voyager
73
- ```
74
-
75
- run with cli:
76
-
77
- ```shell
78
- voyager -m path.to.your.app.module --server
79
- ```
80
-
81
- > [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:
82
-
83
- ```shell
84
- voyager -m path.to.your.app.module --server --app api
85
- ```
86
-
87
-
88
- ## Features
89
-
90
- For scenarios of using FastAPI as internal API integration endpoints, `fastapi-voyager` helps to visualize the dependencies.
91
-
92
- It is also an architecture tool that can identify issues inside implementation, finding out wrong relationships, overfetchs, or anything else.
93
-
94
- **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
95
-
96
- Given ErDiagram defined by pydantic-resolve, application level entity relationship diagram can be visualized too.
97
-
98
-
99
- ### highlight nodes and links
100
- 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.
101
-
102
- <img width="1100" height="700" alt="image" src="https://github.com/user-attachments/assets/3e0369ea-5fa4-469a-82c1-ed57d407e53d" />
103
-
104
- ### view source code
105
-
106
- double click a node or route to show source code or open file in vscode.
107
-
108
- <img width="1297" height="940" alt="image" src="https://github.com/user-attachments/assets/c8bb2e7d-b727-42a6-8c9e-64dce297d2d8" />
109
-
110
- ### quick search
111
-
112
- seach schemas by name and dispaly it's upstream and downstreams.
113
-
114
- shift + click can quickly search current one
115
-
116
- <img width="1587" height="873" alt="image" src="https://github.com/user-attachments/assets/ee4716f3-233d-418f-bc0e-3b214d1498f7" />
117
-
118
- ### display ER diagram
119
-
120
- ER diagram is a new feature from pydantic-resolve which provide a solid expression for business descritpions.
121
-
122
- ```python
123
- diagram = ErDiagram(
124
- configs=[
125
- Entity(
126
- kls=Team,
127
- relationships=[
128
- Relationship( field='id', target_kls=list[Sprint], loader=sprint_loader.team_to_sprint_loader),
129
- Relationship( field='id', target_kls=list[User], loader=user_loader.team_to_user_loader)
130
- ]
131
- ),
132
- Entity(
133
- kls=Sprint,
134
- relationships=[
135
- Relationship( field='id', target_kls=list[Story], loader=story_loader.sprint_to_story_loader)
136
- ]
137
- ),
138
- Entity(
139
- kls=Story,
140
- relationships=[
141
- Relationship( field='id', target_kls=list[Task], loader=task_loader.story_to_task_loader),
142
- Relationship( field='owner_id', target_kls=User, loader=user_loader.user_batch_loader)
143
- ]
144
- ),
145
- Entity(
146
- kls=Task,
147
- relationships=[
148
- Relationship( field='owner_id', target_kls=User, loader=user_loader.user_batch_loader)
149
- ]
150
- )
151
- ]
152
- )
153
-
154
- # display in voyager
155
- app.mount('/voyager',
156
- create_voyager(
157
- app,
158
- er_diagram=diagram)
159
- ```
160
-
161
- <img width="1276" height="613" alt="image" src="https://github.com/user-attachments/assets/ea0091bb-ee11-4f71-8be3-7129d956c910" />
162
-
163
- ### Show pydantic resolve meta info
164
-
165
- setting `enable_pydantic_resolve_meta=True` in `create_voyager`, toggle `pydantic resolve meta`.
166
-
167
- <img width="1604" height="535" alt="image" src="https://github.com/user-attachments/assets/d1639555-af41-4a08-9970-4b8ef314596a" />
168
-
169
-
170
- ## Command Line Usage
171
-
172
- ### open in browser
173
-
174
- ```bash
175
- # open in browser
176
- voyager -m tests.demo --server
177
-
178
- voyager -m tests.demo --server --port=8002
179
- ```
180
-
181
- ### generate the dot file
182
- ```bash
183
- # generate .dot file
184
- voyager -m tests.demo
185
-
186
- voyager -m tests.demo --app my_app
187
-
188
- voyager -m tests.demo --schema Task
189
-
190
- voyager -m tests.demo --show_fields all
191
-
192
- voyager -m tests.demo --module_color=tests.demo:red --module_color=tests.service:tomato
193
-
194
- voyager -m tests.demo -o my_visualization.dot
195
-
196
- voyager --version
197
-
198
- voyager --help
199
- ```
200
-
201
- ## About pydantic-resolve
202
-
203
- pydantic-resolve is a lightweight tool designed to build complex, nested data in a simple, declarative way. In v2 it introduced an important feature: ER Diagram, and fastapi-voyager has supported this feature, allowing for a clearer understanding of the business relationships.
204
-
205
- pydantic-resolve's ~~`@ensure_subset` decorator~~ `DefineSubset` metaclass helps safely pick fields from the 'source class' while **indicating the reference** from the current class to the base class.
206
-
207
- Developers can use fastapi-voyager without needing to know anything about pydantic-resolve, but I still highly recommend everyone to give it a try.
208
-
209
- ## Dependencies
210
-
211
- - FastAPI
212
- - [pydantic-resolve](https://github.com/allmonday/pydantic-resolve)
213
- - Quasar
214
-
215
-
216
- ## Credits
217
-
218
- - https://apis.guru/graphql-voyager/, thanks for inspiration.
219
- - https://github.com/tintinweb/vscode-interactive-graphviz, thanks for web visualization.
220
-
221
-
222
- ## How to develop & contribute?
223
-
224
- fork, clone.
225
-
226
- install uv.
227
-
228
- ```shell
229
- uv venv
230
- source .venv/bin/activate
231
- uv pip install ".[dev]"
232
- uvicorn tests.programatic:app --reload
233
- ```
234
-
235
- ### Setup Git Hooks (Optional)
236
-
237
- Enable automatic code formatting before commits:
238
-
239
- ```shell
240
- ./setup-hooks.sh
241
- # or manually:
242
- git config core.hooksPath .githooks
243
- ```
244
-
245
- This will run Prettier automatically before each commit. See [`.githooks/README.md`](./.githooks/README.md) for details.
246
-
247
- open `localhost:8000/voyager`
248
-
249
-
250
- frontend:
251
- - `src/fastapi_voyager/web/vue-main.js`: main js
252
-
253
- backend:
254
- - `voyager.py`: main entry
255
- - `render.py`: generate dot file
256
- - `server.py`: serve mode