fastapi-voyager 0.14.1__py3-none-any.whl → 0.15.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.
Files changed (35) hide show
  1. fastapi_voyager/er_diagram.py +57 -109
  2. fastapi_voyager/render.py +433 -203
  3. fastapi_voyager/render_style.py +105 -0
  4. fastapi_voyager/server.py +1 -0
  5. fastapi_voyager/templates/dot/cluster.j2 +10 -0
  6. fastapi_voyager/templates/dot/cluster_container.j2 +9 -0
  7. fastapi_voyager/templates/dot/digraph.j2 +25 -0
  8. fastapi_voyager/templates/dot/er_diagram.j2 +29 -0
  9. fastapi_voyager/templates/dot/link.j2 +1 -0
  10. fastapi_voyager/templates/dot/route_node.j2 +5 -0
  11. fastapi_voyager/templates/dot/schema_node.j2 +5 -0
  12. fastapi_voyager/templates/dot/tag_node.j2 +5 -0
  13. fastapi_voyager/templates/html/colored_text.j2 +1 -0
  14. fastapi_voyager/templates/html/pydantic_meta.j2 +1 -0
  15. fastapi_voyager/templates/html/schema_field_row.j2 +1 -0
  16. fastapi_voyager/templates/html/schema_header.j2 +2 -0
  17. fastapi_voyager/templates/html/schema_table.j2 +4 -0
  18. fastapi_voyager/version.py +1 -1
  19. fastapi_voyager/web/component/demo.js +5 -5
  20. fastapi_voyager/web/component/render-graph.js +60 -61
  21. fastapi_voyager/web/component/route-code-display.js +35 -37
  22. fastapi_voyager/web/component/schema-code-display.js +50 -53
  23. fastapi_voyager/web/graph-ui.js +90 -101
  24. fastapi_voyager/web/graphviz.svg.css +10 -10
  25. fastapi_voyager/web/graphviz.svg.js +306 -316
  26. fastapi_voyager/web/icon/site.webmanifest +11 -1
  27. fastapi_voyager/web/index.html +225 -109
  28. fastapi_voyager/web/store.js +107 -111
  29. fastapi_voyager/web/vue-main.js +287 -258
  30. {fastapi_voyager-0.14.1.dist-info → fastapi_voyager-0.15.1.dist-info}/METADATA +18 -5
  31. fastapi_voyager-0.15.1.dist-info/RECORD +50 -0
  32. fastapi_voyager-0.14.1.dist-info/RECORD +0 -36
  33. {fastapi_voyager-0.14.1.dist-info → fastapi_voyager-0.15.1.dist-info}/WHEEL +0 -0
  34. {fastapi_voyager-0.14.1.dist-info → fastapi_voyager-0.15.1.dist-info}/entry_points.txt +0 -0
  35. {fastapi_voyager-0.14.1.dist-info → fastapi_voyager-0.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,105 @@
1
+ """
2
+ Style constants and configuration for rendering DOT graphs and HTML tables.
3
+ """
4
+ from dataclasses import dataclass, field
5
+ from typing import Literal
6
+
7
+
8
+ @dataclass
9
+ class ColorScheme:
10
+ """Color scheme for graph visualization."""
11
+
12
+ # Node colors
13
+ primary: str = '#009485'
14
+ highlight: str = 'tomato'
15
+
16
+ # Pydantic-resolve metadata colors
17
+ resolve: str = '#47a80f'
18
+ post: str = '#427fa4'
19
+ expose_as: str = '#895cb9'
20
+ send_to: str = '#ca6d6d'
21
+ collector: str = '#777'
22
+
23
+ # Link colors
24
+ inherit: str = 'purple'
25
+ subset: str = 'orange'
26
+
27
+ # Border colors
28
+ border: str = '#666'
29
+ cluster_border: str = '#ccc'
30
+
31
+ # Text colors
32
+ text_gray: str = '#999'
33
+
34
+
35
+ @dataclass
36
+ class GraphvizStyle:
37
+ """Graphviz DOT style configuration."""
38
+
39
+ # Font settings
40
+ font: str = 'Helvetica,Arial,sans-serif'
41
+ node_fontsize: str = '16'
42
+ cluster_fontsize: str = '20'
43
+
44
+ # Layout settings
45
+ nodesep: str = '0.8'
46
+ pad: str = '0.5'
47
+ node_margin: str = '0.5,0.1'
48
+ cluster_margin: str = '18'
49
+
50
+ # Link styles configuration
51
+ LINK_STYLES: dict[str, dict] = field(default_factory=lambda: {
52
+ 'tag_route': {
53
+ 'style': 'solid',
54
+ 'minlen': 3,
55
+ },
56
+ 'route_to_schema': {
57
+ 'style': 'solid',
58
+ 'dir': 'back',
59
+ 'arrowtail': 'odot',
60
+ 'minlen': 3,
61
+ },
62
+ 'schema': {
63
+ 'style': 'solid',
64
+ 'label': '',
65
+ 'dir': 'back',
66
+ 'minlen': 3,
67
+ 'arrowtail': 'odot',
68
+ },
69
+ 'parent': {
70
+ 'style': 'solid,dashed',
71
+ 'dir': 'back',
72
+ 'minlen': 3,
73
+ 'taillabel': '< inherit >',
74
+ 'color': 'purple',
75
+ 'tailport': 'n',
76
+ },
77
+ 'subset': {
78
+ 'style': 'solid,dashed',
79
+ 'dir': 'back',
80
+ 'minlen': 3,
81
+ 'taillabel': '< subset >',
82
+ 'color': 'orange',
83
+ 'tailport': 'n',
84
+ },
85
+ 'tag_to_schema': {
86
+ 'style': 'solid',
87
+ 'minlen': 3,
88
+ },
89
+ })
90
+
91
+ def get_link_attributes(self, link_type: str) -> dict:
92
+ """Get link style attributes for a given link type."""
93
+ return self.LINK_STYLES.get(link_type, {})
94
+
95
+
96
+ @dataclass
97
+ class RenderConfig:
98
+ """Complete rendering configuration."""
99
+
100
+ colors: ColorScheme = field(default_factory=ColorScheme)
101
+ style: GraphvizStyle = field(default_factory=GraphvizStyle)
102
+
103
+ # Field display settings
104
+ max_type_length: int = 25
105
+ type_suffix: str = '..'
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,10 @@
1
+ subgraph cluster_{{ cluster_id }} {
2
+ tooltip="{{ tooltip }}"
3
+ color = "{{ border_color }}"
4
+ style="rounded"
5
+ label = " {{ label }}"
6
+ labeljust = "l"
7
+ {% if pen_color %}pencolor = "{{ pen_color }}"{% endif %}
8
+ {% if pen_width %}penwidth = {{ pen_width }}{% endif %}
9
+ {{ content }}
10
+ }
@@ -0,0 +1,9 @@
1
+ subgraph cluster_{{ name }} {
2
+ color = "{{ border_color }}"
3
+ margin={{ margin }}
4
+ style="dashed"
5
+ label = " {{ label }}"
6
+ labeljust = "l"
7
+ fontsize = {{ fontsize }}
8
+ {{ content }}
9
+ }
@@ -0,0 +1,25 @@
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
+ {{ tags_cluster }}
19
+
20
+ {{ routes_cluster }}
21
+
22
+ {{ schemas_cluster }}
23
+
24
+ {{ links }}
25
+ }
@@ -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
+ }
@@ -0,0 +1 @@
1
+ {{ source }}:e -> {{ target }}:w [{{ attributes }}];
@@ -0,0 +1,5 @@
1
+ "{{ id }}" [
2
+ label = " {{ name }} | {{ response_schema }} "
3
+ margin="{{ margin }}"
4
+ shape = "record"
5
+ ];
@@ -0,0 +1,5 @@
1
+ "{{ id }}" [
2
+ label = {{ label }}
3
+ shape = "plain"
4
+ margin="{{ margin }}"
5
+ ];
@@ -0,0 +1,5 @@
1
+ "{{ id }}" [
2
+ label = " {{ name }} "
3
+ shape = "record"
4
+ margin="{{ margin }}"
5
+ ];
@@ -0,0 +1 @@
1
+ <font color="{{ color }}">{% if strikethrough %}<s>{{ text }}</s>{% else %}{{ text }}{% endif %}</font>
@@ -0,0 +1 @@
1
+ {% if meta_parts %}<br align="left"/><br align="left"/>{{ meta_parts | join('<br align="left"/>') }}<br align="left"/>{% endif %}
@@ -0,0 +1 @@
1
+ <tr><td align="{{ align }}" {% if port %}port="f{{ port }}"{% endif %} cellpadding="8">{{ content }}</td></tr>
@@ -0,0 +1,2 @@
1
+ <tr><td cellpadding="6" bgcolor="{{ bg_color }}" align="center" colspan="1" {% if port %}port="{{ port }}"{% endif %}>
2
+ <font color="white"> {{ text }} </font></td></tr>
@@ -0,0 +1,4 @@
1
+ <<table border="0" cellborder="1" cellpadding="0" cellspacing="0" bgcolor="white">
2
+ {{ header }}
3
+ {{ rows }}
4
+ </table>>
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.14.1"
2
+ __version__ = "0.15.1"
@@ -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
+ })