fastapi-voyager 0.15.0__py3-none-any.whl → 0.15.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.
@@ -15,138 +15,86 @@ from fastapi_voyager.type import (
15
15
  ModuleNode,
16
16
  SchemaNode,
17
17
  )
18
+ from fastapi_voyager.render import Renderer
19
+ from fastapi_voyager.render_style import RenderConfig
18
20
  from pydantic import BaseModel
19
21
  from pydantic_resolve import ErDiagram, Entity, Relationship, MultipleRelationship
20
22
  from logging import getLogger
21
- from fastapi_voyager.module import build_module_schema_tree
22
23
 
23
24
  logger = getLogger(__name__)
24
25
 
25
26
 
26
- class DiagramRenderer:
27
+ class DiagramRenderer(Renderer):
28
+ """
29
+ Renderer for Entity-Relationship diagrams.
30
+
31
+ Inherits from Renderer to reuse template system and styling.
32
+ ER diagrams have simpler structure (no tags/routes), so we only
33
+ need to customize the top-level DOT structure.
34
+ """
35
+
27
36
  def __init__(
28
37
  self,
29
38
  *,
30
39
  show_fields: FieldType = 'single',
31
40
  show_module: bool = True
32
41
  ) -> None:
33
- self.show_fields = show_fields if show_fields in ('single', 'object', 'all') else 'single'
34
- self.show_module = show_module
35
-
42
+ # Initialize parent Renderer with shared config
43
+ super().__init__(
44
+ show_fields=show_fields,
45
+ show_module=show_module,
46
+ config=RenderConfig() # Use unified style configuration
47
+ )
36
48
  logger.info(f'show_module: {self.show_module}')
37
49
 
38
- def render_schema_label(self, node: SchemaNode, color: str | None=None) -> str:
39
- has_base_fields = any(f.from_base for f in node.fields)
40
- fields = [n for n in node.fields if n.from_base is False]
41
-
42
- if self.show_fields == 'all':
43
- _fields = fields
44
- elif self.show_fields == 'object':
45
- _fields = [f for f in fields if f.is_object is True]
46
- else: # 'single'
47
- _fields = []
48
-
49
- fields_parts: list[str] = []
50
- if self.show_fields == 'all' and has_base_fields:
51
- fields_parts.append('<tr><td align="left" cellpadding="8"><font color="#999"> Inherited Fields ... </font></td></tr>')
52
-
53
- for field in _fields:
54
- type_name = field.type_name[:25] + '..' if len(field.type_name) > 25 else field.type_name
55
- display_xml = f'<s align="left">{field.name}: {type_name}</s>' if field.is_exclude else f'{field.name}: {type_name}'
56
- field_str = f"""<tr><td align="left" port="f{field.name}" cellpadding="8"><font> {display_xml} </font></td></tr>"""
57
- fields_parts.append(field_str)
58
-
59
- header_color = '#009485' if color is None else color
60
- header = f"""<tr><td cellpadding="6" bgcolor="{header_color}" align="center" colspan="1" port="{PK}"> <font color="white"> {node.name} </font></td> </tr>"""
61
- field_content = ''.join(fields_parts) if fields_parts else ''
62
- return f"""<<table border="0" cellborder="1" cellpadding="0" cellspacing="0" bgcolor="white"> {header} {field_content} </table>>"""
63
-
64
- def _handle_schema_anchor(self, source: str) -> str:
65
- if '::' in source:
66
- a, b = source.split('::', 1)
67
- return f'"{a}":{b}'
68
- return f'"{source}"'
69
-
70
50
  def render_link(self, link: Link) -> str:
71
- h = self._handle_schema_anchor
72
- if link.type == 'schema':
73
- return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "{link.style}", label = "{link.label}", minlen=3];"""
51
+ """Override to increase link length by 40% for ER diagrams."""
52
+ source = self._handle_schema_anchor(link.source)
53
+ target = self._handle_schema_anchor(link.target)
54
+
55
+ # Build link attributes
56
+ if link.style is not None:
57
+ attrs = {'style': link.style}
58
+ if link.label:
59
+ attrs['label'] = link.label
60
+ # Increase minlen by 40% (3 * 1.4 = 4.2, round to 4)
61
+ attrs['minlen'] = 4
74
62
  else:
75
- raise ValueError(f'Unknown link type: {link.type}')
63
+ attrs = self.style.get_link_attributes(link.type)
64
+ if link.label:
65
+ attrs['label'] = link.label
76
66
 
77
- def render_module_schema_content(self, nodes: list[SchemaNode]) -> str:
78
- def render_node(node: SchemaNode, color: str | None=None) -> str:
79
- return f'''
80
- "{node.id}" [
81
- label = {self.render_schema_label(node, color)}
82
- shape = "plain"
83
- margin="0.5,0.1"
84
- ];'''
85
-
86
- def render_module_schema(mod: ModuleNode, show_cluster:bool=True) -> str:
87
- inner_nodes = [ render_node(node) for node in mod.schema_nodes ]
88
- inner_nodes_str = '\n'.join(inner_nodes)
89
- child_str = '\n'.join(render_module_schema(mod=m, show_cluster=show_cluster) for m in mod.modules)
90
-
91
- if show_cluster:
92
- return f'''
93
- subgraph cluster_module_{mod.fullname.replace('.', '_')} {{
94
- tooltip="{mod.fullname}"
95
- color = "#666"
96
- style="rounded"
97
- label = " {mod.name}"
98
- labeljust = "l"
99
- pencolor="#ccc"
100
- penwidth=""
101
- {inner_nodes_str}
102
- {child_str}
103
- }}'''
104
- else:
105
- return f'''
106
- {inner_nodes_str}
107
- {child_str}
108
- '''
109
-
110
- # if self.show_module:
111
- module_schemas = build_module_schema_tree(nodes)
112
- return '\n'.join(render_module_schema(mod=m, show_cluster=self.show_module) for m in module_schemas)
67
+ return self.template_renderer.render_template(
68
+ 'dot/link.j2',
69
+ source=source,
70
+ target=target,
71
+ attributes=self._format_link_attributes(attrs)
72
+ )
113
73
 
114
74
  def render_dot(self, nodes: list[SchemaNode], links: list[Link], spline_line=False) -> str:
115
- module_schemas_str = self.render_module_schema_content(nodes)
116
- link_str = '\n'.join(self.render_link(link) for link in links)
75
+ """
76
+ Render ER diagram as DOT format.
117
77
 
118
- dot_str = f'''
119
- digraph world {{
120
- pad="0.5"
121
- nodesep=0.8
122
- {'splines=line' if spline_line else ''}
123
- fontname="Helvetica,Arial,sans-serif"
124
- node [fontname="Helvetica,Arial,sans-serif"]
125
- edge [
126
- fontname="Helvetica,Arial,sans-serif"
127
- color="gray"
128
- ]
129
- graph [
130
- rankdir = "LR"
131
- ];
132
- node [
133
- fontsize = "16"
134
- ];
78
+ Reuses parent's render_module_schema_content and render_link methods.
79
+ Only customizes the top-level digraph structure.
80
+ """
81
+ # Reuse parent's module schema rendering
82
+ module_schemas_str = self.render_module_schema_content(nodes)
135
83
 
136
- subgraph cluster_schema {{
137
- color = "#aaa"
138
- margin=18
139
- style="dashed"
140
- label=" ER Diagram"
141
- labeljust="l"
142
- fontsize="20"
143
- {module_schemas_str}
144
- }}
84
+ # Reuse parent's link rendering
85
+ link_str = '\n'.join(self.render_link(link) for link in links)
145
86
 
146
- {link_str}
147
- }}
148
- '''
149
- return dot_str
87
+ # Render using ER diagram template
88
+ return self.template_renderer.render_template(
89
+ 'dot/er_diagram.j2',
90
+ pad=self.style.pad,
91
+ nodesep=self.style.nodesep,
92
+ font=self.style.font,
93
+ node_fontsize=self.style.node_fontsize,
94
+ spline='line' if spline_line else None,
95
+ er_cluster=module_schemas_str,
96
+ links=link_str
97
+ )
150
98
 
151
99
 
152
100
  class VoyagerErDiagram:
fastapi_voyager/render.py CHANGED
@@ -262,8 +262,18 @@ class Renderer:
262
262
  source = self._handle_schema_anchor(link.source)
263
263
  target = self._handle_schema_anchor(link.target)
264
264
 
265
- # Get link style attributes
266
- attrs = self.style.get_link_attributes(link.type)
265
+ # Build link attributes
266
+ # If link.style is explicitly set (e.g., 'solid, dashed' for ER diagrams), use it
267
+ # Otherwise, get default style from configuration based on link.type
268
+ if link.style is not None:
269
+ attrs = {'style': link.style}
270
+ if link.label:
271
+ attrs['label'] = link.label
272
+ # attrs['minlen'] = 3
273
+ else:
274
+ attrs = self.style.get_link_attributes(link.type)
275
+ if link.label:
276
+ attrs['label'] = link.label
267
277
 
268
278
  return self.template_renderer.render_template(
269
279
  'dot/link.j2',
fastapi_voyager/server.py CHANGED
@@ -71,6 +71,7 @@ class SchemaSearchPayload(BaseModel): # leave tag, route out
71
71
  brief: bool = False
72
72
  hide_primitive_route: bool = False
73
73
  show_module: bool = True
74
+ show_pydantic_resolve_meta: bool = False
74
75
 
75
76
 
76
77
  # ---------- er diagram ----------
@@ -0,0 +1,29 @@
1
+ digraph world {
2
+ pad="{{ pad }}"
3
+ nodesep={{ nodesep }}
4
+ {% if spline %}splines={{ spline }}{% endif %}
5
+ fontname="{{ font }}"
6
+ node [fontname="{{ font }}"]
7
+ edge [
8
+ fontname="{{ font }}"
9
+ color="gray"
10
+ ]
11
+ graph [
12
+ rankdir = "LR"
13
+ ];
14
+ node [
15
+ fontsize = {{ node_fontsize }}
16
+ ];
17
+
18
+ subgraph cluster_schema {
19
+ color = "#aaa"
20
+ margin=18
21
+ style="dashed"
22
+ label=" ER Diagram"
23
+ labeljust="l"
24
+ fontsize="20"
25
+ {{ er_cluster }}
26
+ }
27
+
28
+ {{ links }}
29
+ }
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.15.0"
2
+ __version__ = "0.15.2"
@@ -1,17 +1,17 @@
1
- const { defineComponent, computed } = window.Vue;
1
+ const { defineComponent, computed } = window.Vue
2
2
 
3
- import { store } from '../store.js'
3
+ import { store } from "../store.js"
4
4
 
5
5
  export default defineComponent({
6
6
  name: "Demo",
7
7
  emits: ["close"],
8
8
  setup() {
9
- return { store };
9
+ return { store }
10
10
  },
11
11
  template: `
12
12
  <div>
13
13
  <p>Count: {{ store.state.item.count }}</p>
14
14
  <button @click="store.mutations.increment()">Add</button>
15
15
  </div>
16
- `
17
- });
16
+ `,
17
+ })
@@ -1,71 +1,71 @@
1
- import { GraphUI } from "../graph-ui.js";
2
- const { defineComponent, ref, onMounted, nextTick } = window.Vue;
1
+ import { GraphUI } from "../graph-ui.js"
2
+ const { defineComponent, ref, onMounted, nextTick } = window.Vue
3
3
 
4
4
  export default defineComponent({
5
- name: "RenderGraph",
6
- props: {
7
- coreData: { type: [Object, Array], required: false, default: null },
8
- },
9
- emits: ["close"],
10
- setup(props, { emit }) {
11
- const containerId = `graph-render-${Math.random().toString(36).slice(2, 9)}`;
12
- const hasRendered = ref(false);
13
- const loading = ref(false);
14
- let graphInstance = null;
5
+ name: "RenderGraph",
6
+ props: {
7
+ coreData: { type: [Object, Array], required: false, default: null },
8
+ },
9
+ emits: ["close"],
10
+ setup(props, { emit }) {
11
+ const containerId = `graph-render-${Math.random().toString(36).slice(2, 9)}`
12
+ const hasRendered = ref(false)
13
+ const loading = ref(false)
14
+ let graphInstance = null
15
15
 
16
- async function ensureGraph() {
17
- await nextTick();
18
- if (!graphInstance) {
19
- graphInstance = new GraphUI(`#${containerId}`);
20
- }
21
- }
16
+ async function ensureGraph() {
17
+ await nextTick()
18
+ if (!graphInstance) {
19
+ graphInstance = new GraphUI(`#${containerId}`)
20
+ }
21
+ }
22
22
 
23
- async function renderFromDot(dotText) {
24
- if (!dotText) return;
25
- await ensureGraph();
26
- await graphInstance.render(dotText);
27
- hasRendered.value = true;
28
- }
23
+ async function renderFromDot(dotText) {
24
+ if (!dotText) return
25
+ await ensureGraph()
26
+ await graphInstance.render(dotText)
27
+ hasRendered.value = true
28
+ }
29
29
 
30
- async function renderFromCoreData() {
31
- if (!props.coreData) return;
32
- loading.value = true;
33
- try {
34
- const res = await fetch("dot-render-core-data", {
35
- method: "POST",
36
- headers: { "Content-Type": "application/json" },
37
- body: JSON.stringify(props.coreData),
38
- });
39
- const dotText = await res.text();
40
- await renderFromDot(dotText);
41
- if (window.Quasar?.Notify) {
42
- window.Quasar.Notify.create({ type: "positive", message: "Rendered" });
43
- }
44
- } catch (e) {
45
- console.error("Render from core data failed", e);
46
- if (window.Quasar?.Notify) {
47
- window.Quasar.Notify.create({ type: "negative", message: "Render failed" });
48
- }
49
- } finally {
50
- loading.value = false;
51
- }
52
- }
30
+ async function renderFromCoreData() {
31
+ if (!props.coreData) return
32
+ loading.value = true
33
+ try {
34
+ const res = await fetch("dot-render-core-data", {
35
+ method: "POST",
36
+ headers: { "Content-Type": "application/json" },
37
+ body: JSON.stringify(props.coreData),
38
+ })
39
+ const dotText = await res.text()
40
+ await renderFromDot(dotText)
41
+ if (window.Quasar?.Notify) {
42
+ window.Quasar.Notify.create({ type: "positive", message: "Rendered" })
43
+ }
44
+ } catch (e) {
45
+ console.error("Render from core data failed", e)
46
+ if (window.Quasar?.Notify) {
47
+ window.Quasar.Notify.create({ type: "negative", message: "Render failed" })
48
+ }
49
+ } finally {
50
+ loading.value = false
51
+ }
52
+ }
53
53
 
54
- async function reload() {
55
- await renderFromCoreData();
56
- }
54
+ async function reload() {
55
+ await renderFromCoreData()
56
+ }
57
57
 
58
- onMounted(async () => {
59
- await reload();
60
- });
58
+ onMounted(async () => {
59
+ await reload()
60
+ })
61
61
 
62
- function close() {
63
- emit("close");
64
- }
62
+ function close() {
63
+ emit("close")
64
+ }
65
65
 
66
- return { containerId, close, hasRendered, reload, loading };
67
- },
68
- template: `
66
+ return { containerId, close, hasRendered, reload, loading }
67
+ },
68
+ template: `
69
69
  <div style="height:100%; position:relative; background:#fff;">
70
70
  <q-btn
71
71
  flat dense round icon="close"
@@ -83,5 +83,4 @@ export default defineComponent({
83
83
  <div :id="containerId" style="width:100%; height:100%; overflow:auto; background:#fafafa"></div>
84
84
  </div>
85
85
  `,
86
- });
87
-
86
+ })
@@ -1,4 +1,4 @@
1
- const { defineComponent, ref, watch, onMounted } = window.Vue;
1
+ const { defineComponent, ref, watch, onMounted } = window.Vue
2
2
 
3
3
  // Component: RouteCodeDisplay
4
4
  // Props:
@@ -10,45 +10,43 @@ export default defineComponent({
10
10
  },
11
11
  emits: ["close"],
12
12
  setup(props, { emit }) {
13
- const loading = ref(false);
14
- const code = ref("");
15
- const error = ref("");
16
- const link = ref("");
13
+ const loading = ref(false)
14
+ const code = ref("")
15
+ const error = ref("")
16
+ const link = ref("")
17
17
 
18
18
  function close() {
19
- emit("close");
19
+ emit("close")
20
20
  }
21
21
 
22
22
  function highlightLater() {
23
23
  requestAnimationFrame(() => {
24
24
  try {
25
25
  if (window.hljs) {
26
- const block = document.querySelector(
27
- ".frv-route-code-display pre code.language-python"
28
- );
26
+ const block = document.querySelector(".frv-route-code-display pre code.language-python")
29
27
  if (block) {
30
- window.hljs.highlightElement(block);
28
+ window.hljs.highlightElement(block)
31
29
  }
32
30
  }
33
31
  } catch (e) {
34
- console.warn("highlight failed", e);
32
+ console.warn("highlight failed", e)
35
33
  }
36
- });
34
+ })
37
35
  }
38
36
 
39
37
  async function load() {
40
38
  if (!props.routeId) {
41
- code.value = "";
42
- return;
39
+ code.value = ""
40
+ return
43
41
  }
44
42
 
45
- loading.value = true;
46
- error.value = null;
47
- code.value = "";
48
- link.value = "";
43
+ loading.value = true
44
+ error.value = null
45
+ code.value = ""
46
+ link.value = ""
49
47
 
50
48
  // try to fetch from server: POST /source with { schema_name: routeId }
51
- const payload = { schema_name: props.routeId };
49
+ const payload = { schema_name: props.routeId }
52
50
  try {
53
51
  const resp = await fetch(`source`, {
54
52
  method: "POST",
@@ -57,18 +55,18 @@ export default defineComponent({
57
55
  "Content-Type": "application/json",
58
56
  },
59
57
  body: JSON.stringify(payload),
60
- });
58
+ })
61
59
 
62
- const data = await resp.json().catch(() => ({}));
60
+ const data = await resp.json().catch(() => ({}))
63
61
  if (resp.ok) {
64
- code.value = data.source_code || "// no source code available";
62
+ code.value = data.source_code || "// no source code available"
65
63
  } else {
66
- error.value = (data && data.error) || "Failed to load source";
64
+ error.value = (data && data.error) || "Failed to load source"
67
65
  }
68
66
  } catch (e) {
69
- error.value = e && e.message ? e.message : "Failed to load source";
67
+ error.value = e && e.message ? e.message : "Failed to load source"
70
68
  } finally {
71
- loading.value = false;
69
+ loading.value = false
72
70
  }
73
71
 
74
72
  try {
@@ -79,36 +77,36 @@ export default defineComponent({
79
77
  "Content-Type": "application/json",
80
78
  },
81
79
  body: JSON.stringify(payload),
82
- });
80
+ })
83
81
 
84
- const data = await resp.json().catch(() => ({}));
82
+ const data = await resp.json().catch(() => ({}))
85
83
  if (resp.ok) {
86
- link.value = data.link || "// no source code available";
84
+ link.value = data.link || "// no source code available"
87
85
  } else {
88
- error.value += (data && data.error) || "Failed to load vscode link";
86
+ error.value += (data && data.error) || "Failed to load vscode link"
89
87
  }
90
88
  } catch (e) {
91
89
  } finally {
92
- loading.value = false;
90
+ loading.value = false
93
91
  }
94
92
 
95
93
  if (!error.value) {
96
- highlightLater();
94
+ highlightLater()
97
95
  }
98
96
  }
99
97
 
100
98
  watch(
101
99
  () => props.routeId,
102
100
  () => {
103
- load();
101
+ load()
104
102
  }
105
- );
103
+ )
106
104
 
107
105
  onMounted(() => {
108
- load();
109
- });
106
+ load()
107
+ })
110
108
 
111
- return { loading, code, error, close, link };
109
+ return { loading, code, error, close, link }
112
110
  },
113
111
  template: `
114
112
  <div class="frv-route-code-display" style="border:1px solid #ccc; position:relative; background:#fff;">
@@ -122,4 +120,4 @@ export default defineComponent({
122
120
  <pre v-else style="margin:0;"><code class="language-python">{{ code }}</code></pre>
123
121
  </div>
124
122
  </div>`,
125
- });
123
+ })