fastapi-voyager 0.15.5__py3-none-any.whl → 0.16.0a1__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.
Files changed (31) hide show
  1. fastapi_voyager/__init__.py +2 -2
  2. fastapi_voyager/adapters/__init__.py +16 -0
  3. fastapi_voyager/adapters/base.py +44 -0
  4. fastapi_voyager/adapters/common.py +260 -0
  5. fastapi_voyager/adapters/django_ninja_adapter.py +299 -0
  6. fastapi_voyager/adapters/fastapi_adapter.py +165 -0
  7. fastapi_voyager/adapters/litestar_adapter.py +188 -0
  8. fastapi_voyager/er_diagram.py +15 -14
  9. fastapi_voyager/introspectors/__init__.py +34 -0
  10. fastapi_voyager/introspectors/base.py +81 -0
  11. fastapi_voyager/introspectors/detector.py +123 -0
  12. fastapi_voyager/introspectors/django_ninja.py +114 -0
  13. fastapi_voyager/introspectors/fastapi.py +83 -0
  14. fastapi_voyager/introspectors/litestar.py +166 -0
  15. fastapi_voyager/pydantic_resolve_util.py +4 -2
  16. fastapi_voyager/render.py +2 -2
  17. fastapi_voyager/render_style.py +0 -1
  18. fastapi_voyager/server.py +174 -295
  19. fastapi_voyager/type_helper.py +2 -2
  20. fastapi_voyager/version.py +1 -1
  21. fastapi_voyager/voyager.py +75 -47
  22. fastapi_voyager/web/graph-ui.js +102 -69
  23. fastapi_voyager/web/graphviz.svg.js +79 -30
  24. fastapi_voyager/web/index.html +11 -14
  25. fastapi_voyager/web/store.js +2 -0
  26. fastapi_voyager/web/vue-main.js +4 -0
  27. {fastapi_voyager-0.15.5.dist-info → fastapi_voyager-0.16.0a1.dist-info}/METADATA +133 -7
  28. {fastapi_voyager-0.15.5.dist-info → fastapi_voyager-0.16.0a1.dist-info}/RECORD +31 -19
  29. {fastapi_voyager-0.15.5.dist-info → fastapi_voyager-0.16.0a1.dist-info}/WHEEL +0 -0
  30. {fastapi_voyager-0.15.5.dist-info → fastapi_voyager-0.16.0a1.dist-info}/entry_points.txt +0 -0
  31. {fastapi_voyager-0.15.5.dist-info → fastapi_voyager-0.16.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,5 @@
1
1
 
2
2
  import pydantic_resolve.constant as const
3
- from fastapi import FastAPI, routing
4
3
  from pydantic import BaseModel
5
4
 
6
5
  from fastapi_voyager.filter import (
@@ -8,6 +7,7 @@ from fastapi_voyager.filter import (
8
7
  filter_subgraph_by_module_prefix,
9
8
  filter_subgraph_from_tag_to_schema_by_module_prefix,
10
9
  )
10
+ from fastapi_voyager.introspectors import AppIntrospector, RouteInfo
11
11
  from fastapi_voyager.render import Renderer
12
12
  from fastapi_voyager.type import PK, CoreData, FieldType, Link, LinkType, Route, SchemaNode, Tag
13
13
  from fastapi_voyager.type_helper import (
@@ -57,99 +57,127 @@ class Voyager:
57
57
  self.hide_primitive_route = hide_primitive_route
58
58
  self.show_module = show_module
59
59
  self.show_pydantic_resolve_meta = show_pydantic_resolve_meta
60
-
61
60
 
62
- def _get_available_route(self, app: FastAPI):
63
- for route in app.routes:
64
- if isinstance(route, routing.APIRoute):
65
- yield route
61
+ def _get_introspector(self, app) -> AppIntrospector:
62
+ """
63
+ Get the appropriate introspector for the given app.
64
+
65
+ Automatically detects the framework type and returns the matching introspector.
66
66
 
67
+ Args:
68
+ app: A web application instance or AppIntrospector
67
69
 
68
- def analysis(self, app: FastAPI):
70
+ Returns:
71
+ An AppIntrospector instance
72
+
73
+ Raises:
74
+ TypeError: If the app type is not supported
69
75
  """
76
+ from fastapi_voyager.introspectors import get_introspector
77
+
78
+ return get_introspector(app)
79
+
80
+ def analysis(self, app):
81
+ """
82
+ Analyze routes and schemas from a web application.
83
+
84
+ This method automatically detects the framework type and uses the appropriate
85
+ introspector. Supported frameworks:
86
+ - FastAPI (built-in)
87
+ - Any framework with a custom AppIntrospector implementation
88
+
89
+ Args:
90
+ app: A web application instance (FastAPI, Django Ninja API, etc.)
91
+ or an AppIntrospector instance for custom frameworks.
92
+
70
93
  1. get routes which return pydantic schema
71
94
  1.1 collect tags and routes, add links tag-> route
72
95
  1.2 collect response_model and links route -> response_model
73
96
 
74
97
  2. iterate schemas, construct the schema/model nodes and their links
75
98
  """
99
+ introspector = self._get_introspector(app)
76
100
  schemas: list[type[BaseModel]] = []
77
101
 
78
102
  # First, group all routes by tag
79
- routes_by_tag: dict[str, list] = {}
80
- for route in self._get_available_route(app):
81
- tags = getattr(route, 'tags', None)
82
-
103
+ routes_by_tag: dict[str, list[RouteInfo]] = {}
104
+ for route_info in introspector.get_routes():
83
105
  # using multiple tags is harmful, it's not recommended and will not be supported
84
- route_tag = tags[0] if tags else '__default__'
85
- routes_by_tag.setdefault(route_tag, []).append(route)
106
+ route_tag = route_info.tags[0] if route_info.tags else '__default__'
107
+ routes_by_tag.setdefault(route_tag, []).append(route_info)
86
108
 
87
109
  # Then filter by include_tags if provided
88
110
  if self.include_tags:
89
- filtered_routes_by_tag = {tag: routes for tag, routes in routes_by_tag.items()
90
- if tag in self.include_tags}
111
+ filtered_routes_by_tag = {
112
+ tag: routes
113
+ for tag, routes in routes_by_tag.items()
114
+ if tag in self.include_tags
115
+ }
91
116
  else:
92
117
  filtered_routes_by_tag = routes_by_tag
93
118
 
94
119
  # Process filtered routes
95
- for route_tag, routes in filtered_routes_by_tag.items():
96
-
120
+ for route_tag, route_infos in filtered_routes_by_tag.items():
97
121
  tag_id = f'tag__{route_tag}'
98
122
  tag_obj = Tag(id=tag_id, name=route_tag, routes=[])
99
123
  self.tags.append(tag_obj)
100
124
 
101
- for route in routes:
102
- # add route and create links
103
- route_id = full_class_name(route.endpoint)
104
- route_name = route.endpoint.__name__
105
- route_module = route.endpoint.__module__
106
-
125
+ for route_info in route_infos:
107
126
  # filter by route_name (route.id) if provided
108
- if self.route_name is not None and route_id != self.route_name:
127
+ if self.route_name is not None and route_info.id != self.route_name:
109
128
  continue
110
129
 
111
- is_primitive_response = is_non_pydantic_type(route.response_model)
130
+ is_primitive_response = is_non_pydantic_type(route_info.response_model)
112
131
  # filter primitive route if needed
113
132
  if self.hide_primitive_route and is_primitive_response:
114
133
  continue
115
134
 
116
- self.links.append(Link(
117
- source=tag_id,
118
- source_origin=tag_id,
119
- target=route_id,
120
- target_origin=route_id,
121
- type='tag_route'
122
- ))
135
+ self.links.append(
136
+ Link(
137
+ source=tag_id,
138
+ source_origin=tag_id,
139
+ target=route_info.id,
140
+ target_origin=route_info.id,
141
+ type='tag_route',
142
+ )
143
+ )
144
+
145
+ # Get unique_id from extra data if available
146
+ unique_id = route_info.operation_id
147
+ if route_info.extra and 'unique_id' in route_info.extra:
148
+ unique_id = unique_id or route_info.extra['unique_id']
123
149
 
124
150
  route_obj = Route(
125
- id=route_id,
126
- name=route_name,
127
- module=route_module,
128
- unique_id=route.operation_id or route.unique_id,
129
- response_schema=get_type_name(route.response_model),
130
- is_primitive=is_primitive_response
151
+ id=route_info.id,
152
+ name=route_info.name,
153
+ module=route_info.module,
154
+ unique_id=unique_id,
155
+ response_schema=get_type_name(route_info.response_model),
156
+ is_primitive=is_primitive_response,
131
157
  )
132
158
  self.routes.append(route_obj)
133
159
  tag_obj.routes.append(route_obj)
134
160
 
135
161
  # add response_models and create links from route -> response_model
136
- for schema in get_core_types(route.response_model):
162
+ for schema in get_core_types(route_info.response_model):
137
163
  if schema and issubclass(schema, BaseModel):
138
164
  is_primitive_response = False
139
165
  target_name = full_class_name(schema)
140
- self.links.append(Link(
141
- source=route_id,
142
- source_origin=route_id,
143
- target=self.generate_node_head(target_name),
144
- target_origin=target_name,
145
- type='route_to_schema'
146
- ))
166
+ self.links.append(
167
+ Link(
168
+ source=route_info.id,
169
+ source_origin=route_info.id,
170
+ target=self.generate_node_head(target_name),
171
+ target_origin=target_name,
172
+ type='route_to_schema',
173
+ )
174
+ )
147
175
 
148
176
  schemas.append(schema)
149
177
 
150
178
  for s in schemas:
151
179
  self.analysis_schemas(s)
152
-
180
+
153
181
  self.nodes = list(self.node_set.values())
154
182
 
155
183
 
@@ -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
@@ -4,32 +4,29 @@
4
4
  <meta name="theme-color" content="#ffffff" />
5
5
  <link
6
6
  rel="stylesheet"
7
- href="fastapi-voyager-static/graphviz.svg.css<!-- VERSION_PLACEHOLDER -->"
8
- />
9
- <link
10
- rel="stylesheet"
11
- href="fastapi-voyager-static/quasar.min.css<!-- VERSION_PLACEHOLDER -->"
7
+ href="<!-- STATIC_PATH -->/graphviz.svg.css<!-- VERSION_PLACEHOLDER -->"
12
8
  />
9
+ <link rel="stylesheet" href="<!-- STATIC_PATH -->/quasar.min.css<!-- VERSION_PLACEHOLDER -->" />
13
10
  <!-- App Icons / Favicons -->
14
11
  <link
15
12
  rel="apple-touch-icon"
16
13
  sizes="180x180"
17
- href="fastapi-voyager-static/icon/apple-touch-icon.png"
14
+ href="<!-- STATIC_PATH -->/icon/apple-touch-icon.png"
18
15
  />
19
16
  <link
20
17
  rel="icon"
21
18
  type="image/png"
22
19
  sizes="32x32"
23
- href="fastapi-voyager-static/icon/favicon-32x32.png"
20
+ href="<!-- STATIC_PATH -->/icon/favicon-32x32.png"
24
21
  />
25
22
  <link
26
23
  rel="icon"
27
24
  type="image/png"
28
25
  sizes="16x16"
29
- href="fastapi-voyager-static/icon/favicon-16x16.png"
26
+ href="<!-- STATIC_PATH -->/icon/favicon-16x16.png"
30
27
  />
31
- <link rel="icon" href="fastapi-voyager-static/icon/favicon.ico" sizes="any" />
32
- <link rel="manifest" href="fastapi-voyager-static/icon/site.webmanifest" />
28
+ <link rel="icon" href="<!-- STATIC_PATH -->/icon/favicon.ico" sizes="any" />
29
+ <link rel="manifest" href="<!-- STATIC_PATH -->/icon/site.webmanifest" />
33
30
  <link
34
31
  href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
35
32
  rel="stylesheet"
@@ -191,7 +188,7 @@
191
188
  style="font-size: 18px; font-weight: bold; display: flex; align-items: baseline"
192
189
  >
193
190
  <q-icon class="q-mr-sm" name="satellite_alt"></q-icon>
194
- <span> FastAPI Voyager </span>
191
+ <span> {{ store.state.framework_name }} Voyager </span>
195
192
  <span
196
193
  v-if="store.state.version"
197
194
  style="font-size: 12px; margin-left: 8px; font-weight: normal"
@@ -580,7 +577,7 @@
580
577
  </q-dialog>
581
578
  </div>
582
579
  <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
583
- <script src="fastapi-voyager-static/quasar.min.js<!-- VERSION_PLACEHOLDER -->"></script>
580
+ <script src="<!-- STATIC_PATH -->/quasar.min.js<!-- VERSION_PLACEHOLDER -->"></script>
584
581
  <script
585
582
  src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"
586
583
  integrity="sha512-egJ/Y+22P9NQ9aIyVCh0VCOsfydyn8eNmqBy+y2CnJG+fpRIxXMS6jbWP8tVKp0jp+NO5n8WtMUAnNnGoJKi4w=="
@@ -602,7 +599,7 @@
602
599
  ></script>
603
600
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-mousewheel/3.1.13/jquery.mousewheel.min.js"></script>
604
601
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-color/2.1.2/jquery.color.min.js"></script>
605
- <script src="fastapi-voyager-static/graphviz.svg.js<!-- VERSION_PLACEHOLDER -->"></script>
602
+ <script src="<!-- STATIC_PATH -->/graphviz.svg.js<!-- VERSION_PLACEHOLDER -->"></script>
606
603
  <!-- highlight.js minimal ES module load (python only) -->
607
604
  <link
608
605
  rel="stylesheet"
@@ -626,7 +623,7 @@
626
623
  </script>
627
624
  <script
628
625
  type="module"
629
- src="fastapi-voyager-static/vue-main.js<!-- VERSION_PLACEHOLDER -->"
626
+ src="<!-- STATIC_PATH -->/vue-main.js<!-- VERSION_PLACEHOLDER -->"
630
627
  ></script>
631
628
 
632
629
  <!-- GA_SNIPPET -->
@@ -2,6 +2,7 @@ const { reactive } = window.Vue
2
2
 
3
3
  const state = reactive({
4
4
  version: "",
5
+ framework_name: "",
5
6
  config: {
6
7
  initial_page_policy: "first",
7
8
  has_er_diagram: false,
@@ -305,6 +306,7 @@ const actions = {
305
306
  state.swagger.url = data.swagger_url || null
306
307
  state.config.has_er_diagram = data.has_er_diagram || false
307
308
  state.config.enable_pydantic_resolve_meta = data.enable_pydantic_resolve_meta || false
309
+ state.framework_name = data.framework_name || "API"
308
310
 
309
311
  this.rebuildSchemaOptions()
310
312