fastapi-voyager 0.8.3__py3-none-any.whl → 0.9.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.
@@ -3,3 +3,7 @@
3
3
  Utilities to introspect a FastAPI application and visualize its routing tree.
4
4
  """
5
5
  from .version import __version__ # noqa: F401
6
+
7
+ from .server import create_voyager
8
+
9
+ __all__ = ["__version__", "create_voyager"]
fastapi_voyager/cli.py CHANGED
@@ -281,7 +281,7 @@ Examples:
281
281
  except ImportError:
282
282
  print("Error: uvicorn is required to run the server. Install via 'pip install uvicorn' or 'uv add uvicorn'.")
283
283
  sys.exit(1)
284
- app_server = viz_server.create_app_with_fastapi(
284
+ app_server = viz_server.create_voyager(
285
285
  app,
286
286
  module_color=module_color,
287
287
  module_prefix=args.module_prefix,
fastapi_voyager/server.py CHANGED
@@ -6,23 +6,18 @@ from pydantic import BaseModel
6
6
  from fastapi.responses import HTMLResponse, PlainTextResponse, JSONResponse
7
7
  from fastapi.staticfiles import StaticFiles
8
8
  from fastapi_voyager.voyager import Voyager
9
- from fastapi_voyager.type import Tag, FieldInfo, CoreData
9
+ from fastapi_voyager.type import Tag, FieldInfo, CoreData, SchemaNode
10
10
  from fastapi_voyager.render import Renderer
11
+ from fastapi_voyager.type_helper import get_source, get_vscode_link
11
12
 
12
13
 
13
14
  WEB_DIR = Path(__file__).parent / "web"
14
15
  WEB_DIR.mkdir(exist_ok=True)
15
16
 
16
- class SchemaType(BaseModel):
17
- name: str
18
- fullname: str
19
- source_code: str
20
- vscode_link: str
21
- fields: list[FieldInfo]
22
17
 
23
18
  class OptionParam(BaseModel):
24
19
  tags: list[Tag]
25
- schemas: list[SchemaType]
20
+ schemas: list[SchemaNode]
26
21
  dot: str
27
22
 
28
23
  class Payload(BaseModel):
@@ -40,26 +35,22 @@ def create_route(
40
35
  module_color: dict[str, str] | None = None,
41
36
  module_prefix: Optional[str] = None,
42
37
  ):
38
+ """
39
+ module_color: dict mapping module name to color string, e.g. {'models': 'lightblue'}
40
+ module_prefix: prefix string to define schemas show in brief mode
41
+ """
43
42
  router = APIRouter(tags=['fastapi-voyager'])
44
43
 
45
44
  @router.get("/dot", response_model=OptionParam)
46
45
  def get_dot() -> str:
47
- voyager = Voyager(module_color=module_color, load_meta=True)
46
+ voyager = Voyager(module_color=module_color)
48
47
  voyager.analysis(target_app)
49
48
  dot = voyager.render_dot()
50
49
 
51
50
  # include tags and their routes
52
51
  tags = voyager.tags
53
52
 
54
- schemas = [
55
- SchemaType(
56
- name=s.name,
57
- fullname=s.id,
58
- fields=s.fields,
59
- source_code=s.source_code,
60
- vscode_link=s.vscode_link
61
- ) for s in voyager.nodes
62
- ]
53
+ schemas = voyager.nodes[:]
63
54
  schemas.sort(key=lambda s: s.name)
64
55
 
65
56
  return OptionParam(tags=tags, schemas=schemas, dot=dot)
@@ -74,7 +65,6 @@ def create_route(
74
65
  show_fields=payload.show_fields,
75
66
  module_color=module_color,
76
67
  route_name=payload.route_name,
77
- load_meta=False,
78
68
  hide_primitive_route=payload.hide_primitive_route,
79
69
  )
80
70
  voyager.analysis(target_app)
@@ -92,7 +82,6 @@ def create_route(
92
82
  show_fields=payload.show_fields,
93
83
  module_color=module_color,
94
84
  route_name=payload.route_name,
95
- load_meta=False,
96
85
  )
97
86
  voyager.analysis(target_app)
98
87
  return voyager.dump_core_data()
@@ -117,11 +106,90 @@ def create_route(
117
106
  </body>
118
107
  </html>
119
108
  """
109
+
110
+ class SourcePayload(BaseModel):
111
+ schema_name: str
120
112
 
113
+ @router.post("/source")
114
+ def get_object_by_module_name(payload: SourcePayload):
115
+ """
116
+ input: __module__ + __name__, eg: tests.demo.PageStories
117
+ output: source code of the object
118
+ """
119
+ try:
120
+ components = payload.schema_name.split('.')
121
+ if len(components) < 2:
122
+ return JSONResponse(
123
+ status_code=400,
124
+ content={"error": "Invalid schema name format. Expected format: module.ClassName"}
125
+ )
126
+
127
+ module_name = '.'.join(components[:-1])
128
+ class_name = components[-1]
129
+
130
+ mod = __import__(module_name, fromlist=[class_name])
131
+ obj = getattr(mod, class_name)
132
+ source_code = get_source(obj)
133
+
134
+ return JSONResponse(content={"source_code": source_code})
135
+ except ImportError as e:
136
+ return JSONResponse(
137
+ status_code=404,
138
+ content={"error": f"Module not found: {e}"}
139
+ )
140
+ except AttributeError as e:
141
+ return JSONResponse(
142
+ status_code=404,
143
+ content={"error": f"Class not found: {e}"}
144
+ )
145
+ except Exception as e:
146
+ return JSONResponse(
147
+ status_code=500,
148
+ content={"error": f"Internal error: {str(e)}"}
149
+ )
150
+
151
+ @router.post("/vscode-link")
152
+ def get_vscode_link_by_module_name(payload: SourcePayload):
153
+ """
154
+ input: __module__ + __name__, eg: tests.demo.PageStories
155
+ output: source path of the object
156
+ """
157
+ try:
158
+ components = payload.schema_name.split('.')
159
+ if len(components) < 2:
160
+ return JSONResponse(
161
+ status_code=400,
162
+ content={"error": "Invalid schema name format. Expected format: module.ClassName"}
163
+ )
164
+
165
+ module_name = '.'.join(components[:-1])
166
+ class_name = components[-1]
167
+
168
+ mod = __import__(module_name, fromlist=[class_name])
169
+ obj = getattr(mod, class_name)
170
+ link = get_vscode_link(obj)
171
+
172
+ return JSONResponse(content={"link": link})
173
+ except ImportError as e:
174
+ return JSONResponse(
175
+ status_code=404,
176
+ content={"error": f"Module not found: {e}"}
177
+ )
178
+ except AttributeError as e:
179
+ return JSONResponse(
180
+ status_code=404,
181
+ content={"error": f"Class not found: {e}"}
182
+ )
183
+ except Exception as e:
184
+ return JSONResponse(
185
+ status_code=500,
186
+ content={"error": f"Internal error: {str(e)}"}
187
+ )
188
+
121
189
  return router
122
190
 
123
191
 
124
- def create_app_with_fastapi(
192
+ def create_voyager(
125
193
  target_app: FastAPI,
126
194
  module_color: dict[str, str] | None = None,
127
195
  gzip_minimum_size: int | None = 500,
fastapi_voyager/type.py CHANGED
@@ -22,8 +22,6 @@ class Tag(NodeBase):
22
22
  @dataclass
23
23
  class Route(NodeBase):
24
24
  module: str
25
- source_code: str = ''
26
- vscode_link: str = '' # optional vscode deep link
27
25
  response_schema: str = ''
28
26
  is_primitive: bool = True
29
27
 
@@ -37,8 +35,6 @@ class ModuleRoute:
37
35
  @dataclass
38
36
  class SchemaNode(NodeBase):
39
37
  module: str
40
- source_code: str = '' # optional for tests / backward compatibility
41
- vscode_link: str = '' # optional vscode deep link
42
38
  fields: list[FieldInfo] = field(default_factory=list)
43
39
 
44
40
  @dataclass
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.8.3"
2
+ __version__ = "0.9.1"
@@ -29,7 +29,6 @@ class Voyager:
29
29
  module_color: dict[str, str] | None = None,
30
30
  route_name: str | None = None,
31
31
  hide_primitive_route: bool = False,
32
- load_meta: bool = False
33
32
  ):
34
33
 
35
34
  self.routes: list[Route] = []
@@ -50,7 +49,6 @@ class Voyager:
50
49
  self.show_fields = show_fields if show_fields in ('single','object','all') else 'object'
51
50
  self.module_color = module_color or {}
52
51
  self.route_name = route_name
53
- self.load_meta = load_meta
54
52
  self.hide_primitive_route = hide_primitive_route
55
53
 
56
54
 
@@ -85,7 +83,7 @@ class Voyager:
85
83
  self.tags.append(tag_obj)
86
84
 
87
85
  # add route and create links
88
- route_id = f'{route.endpoint.__name__}_{route.path.replace("/", "_")}'
86
+ route_id = full_class_name(route.endpoint)
89
87
  route_name = route.endpoint.__name__
90
88
  route_module = route.endpoint.__module__
91
89
 
@@ -110,8 +108,6 @@ class Voyager:
110
108
  id=route_id,
111
109
  name=route_name,
112
110
  module=route_module,
113
- vscode_link=get_vscode_link(route.endpoint) if self.load_meta else '',
114
- source_code=inspect.getsource(route.endpoint) if self.load_meta else '',
115
111
  response_schema=get_type_name(route.response_model),
116
112
  is_primitive=is_primitive_response
117
113
  )
@@ -154,8 +150,6 @@ class Voyager:
154
150
  id=full_name,
155
151
  module=schema.__module__,
156
152
  name=schema.__name__,
157
- source_code=get_source(schema) if self.load_meta else '',
158
- vscode_link=get_vscode_link(schema) if self.load_meta else '',
159
153
  fields=get_pydantic_fields(schema, bases_fields)
160
154
  )
161
155
  return full_name
@@ -6,58 +6,119 @@ const { defineComponent, ref, watch, onMounted } = window.Vue;
6
6
  // modelValue: dialog visibility
7
7
  // routes: object map { id: { id, name, source_code } }
8
8
  export default defineComponent({
9
- name: 'RouteCodeDisplay',
9
+ name: "RouteCodeDisplay",
10
10
  props: {
11
11
  routeId: { type: String, required: true },
12
12
  modelValue: { type: Boolean, default: false },
13
13
  routes: { type: Object, default: () => ({}) },
14
14
  },
15
- emits: ['close'],
15
+ emits: ["close"],
16
16
  setup(props, { emit }) {
17
- const code = ref('');
18
- const error = ref(null);
19
- const link = ref('');
17
+ const loading = ref(false);
18
+ const code = ref("");
19
+ const error = ref("");
20
+ const link = ref("");
20
21
 
21
- function close() { emit('close'); }
22
+ function close() {
23
+ emit("close");
24
+ }
22
25
 
23
26
  function highlightLater() {
24
27
  requestAnimationFrame(() => {
25
28
  try {
26
29
  if (window.hljs) {
27
- const block = document.querySelector('.frv-route-code-display pre code.language-python');
30
+ const block = document.querySelector(
31
+ ".frv-route-code-display pre code.language-python"
32
+ );
28
33
  if (block) {
29
34
  window.hljs.highlightElement(block);
30
35
  }
31
36
  }
32
37
  } catch (e) {
33
- console.warn('highlight failed', e);
38
+ console.warn("highlight failed", e);
34
39
  }
35
40
  });
36
41
  }
37
42
 
38
- function load() {
43
+ async function load() {
44
+ if (!props.routeId) {
45
+ code.value = "";
46
+ return;
47
+ }
48
+
49
+ loading.value = true;
39
50
  error.value = null;
40
- if (!props.routeId) { code.value=''; return; }
41
- const item = props.routes[props.routeId];
42
- if (item && item.source_code) {
43
- code.value = item.source_code;
44
- link.value = item.vscode_link || '';
51
+ code.value = "";
52
+ link.value = "";
53
+
54
+ // try to fetch from server: POST /source with { schema_name: routeId }
55
+ const payload = { schema_name: props.routeId };
56
+ try {
57
+ const resp = await fetch(`source`, {
58
+ method: "POST",
59
+ headers: {
60
+ Accept: "application/json",
61
+ "Content-Type": "application/json",
62
+ },
63
+ body: JSON.stringify(payload),
64
+ });
65
+
66
+ const data = await resp.json().catch(() => ({}));
67
+ if (resp.ok) {
68
+ code.value = data.source_code || "// no source code available";
69
+ } else {
70
+ error.value = (data && data.error) || "Failed to load source";
71
+ }
72
+ } catch (e) {
73
+ error.value = e && e.message ? e.message : "Failed to load source";
74
+ } finally {
75
+ loading.value = false;
76
+ }
77
+
78
+ try {
79
+ const resp = await fetch(`vscode-link`, {
80
+ method: "POST",
81
+ headers: {
82
+ Accept: "application/json",
83
+ "Content-Type": "application/json",
84
+ },
85
+ body: JSON.stringify(payload),
86
+ });
87
+
88
+ const data = await resp.json().catch(() => ({}));
89
+ if (resp.ok) {
90
+ link.value = data.link || "// no source code available";
91
+ } else {
92
+ error.value += (data && data.error) || "Failed to load vscode link";
93
+ }
94
+ } catch (e) {
95
+ } finally {
96
+ loading.value = false;
97
+ }
98
+
99
+ if (!error.value) {
45
100
  highlightLater();
46
- } else if (item) {
47
- code.value = '// no source code available';
48
- link.value = item.vscode_link || '';
49
- } else {
50
- error.value = 'Route not found';
51
- link.value = '';
52
101
  }
53
102
  }
54
103
 
55
- watch(() => props.modelValue, (v) => { if (v) load(); });
56
- watch(() => props.routeId, () => { if (props.modelValue) load(); });
104
+ watch(
105
+ () => props.modelValue,
106
+ (v) => {
107
+ if (v) load();
108
+ }
109
+ );
110
+ watch(
111
+ () => props.routeId,
112
+ () => {
113
+ if (props.modelValue) load();
114
+ }
115
+ );
57
116
 
58
- onMounted(() => { if (props.modelValue) load(); });
117
+ onMounted(() => {
118
+ if (props.modelValue) load();
119
+ });
59
120
 
60
- return { code, error, close, link };
121
+ return { loading, code, error, close, link };
61
122
  },
62
123
  template: `
63
124
  <div class="frv-route-code-display" style="border:1px solid #ccc; position:relative; width:50vw; max-width:50vw; height:100%; background:#fff;">
@@ -66,8 +127,9 @@ export default defineComponent({
66
127
  <a :href="link" target="_blank" rel="noopener" style="font-size:12px; color:#3b82f6;">Open in VSCode</a>
67
128
  </div>
68
129
  <div style="padding:40px 16px 16px 16px; height:100%; box-sizing:border-box; overflow:auto;">
69
- <div v-if="error" style="color:#c10015; font-family:Menlo, monospace; font-size:12px;">{{ error }}</div>
130
+ <div v-if="loading" style="font-family:Menlo, monospace; font-size:12px;">Loading source...</div>
131
+ <div v-else-if="error" style="color:#c10015; font-family:Menlo, monospace; font-size:12px;">{{ error }}</div>
70
132
  <pre v-else style="margin:0;"><code class="language-python">{{ code }}</code></pre>
71
133
  </div>
72
- </div>`
134
+ </div>`,
73
135
  });
@@ -14,7 +14,6 @@ export default defineComponent({
14
14
  props: {
15
15
  schemaName: { type: String, required: true },
16
16
  modelValue: { type: Boolean, default: false },
17
- source: { type: String, default: null },
18
17
  schemas: { type: Array, default: () => [] },
19
18
  },
20
19
  emits: ["close"],
@@ -22,9 +21,9 @@ export default defineComponent({
22
21
  const loading = ref(false);
23
22
  const code = ref("");
24
23
  const link = ref("");
25
- const error = ref(null);
24
+ const error = ref("");
26
25
  const fields = ref([]); // schema fields list
27
- const tab = ref('source');
26
+ const tab = ref("source");
28
27
 
29
28
  function close() {
30
29
  emit("close");
@@ -48,38 +47,71 @@ export default defineComponent({
48
47
  });
49
48
  }
50
49
 
51
- function loadSource() {
50
+ async function loadSource() {
52
51
  if (!props.schemaName) return;
53
- if (props.source) {
54
- code.value = props.source;
55
- highlightLater();
56
- return;
57
- }
52
+
58
53
  loading.value = true;
59
54
  error.value = null;
55
+ code.value = "";
56
+ link.value = "";
57
+
58
+ // try to fetch from server: /source/{schema_name}
59
+ const payload = { schema_name: props.schemaName };
60
60
  try {
61
- const item = props.schemas.find((s) => s.fullname === props.schemaName);
62
- if (item) {
63
- link.value = item.vscode_link || "";
64
- code.value = item.source_code || "// no source code available";
65
- // capture fields if provided
66
- fields.value = Array.isArray(item.fields) ? item.fields : [];
67
- highlightLater();
61
+ // validate input: ensure we have a non-empty schemaName
62
+ const resp = await fetch(`source`, {
63
+ method: "POST",
64
+ headers: {
65
+ Accept: "application/json",
66
+ "Content-Type": "application/json",
67
+ },
68
+ body: JSON.stringify(payload),
69
+ });
70
+ // surface server-side validation message for bad request
71
+ const data = await resp.json().catch(() => ({}));
72
+ if (resp.ok) {
73
+ code.value = data.source_code || "// no source code available";
68
74
  } else {
69
- error.value = "Schema not found";
75
+ error.value = (data && data.error) || "Failed to load source";
70
76
  }
71
77
  } catch (e) {
72
78
  error.value = "Failed to load source";
73
79
  } finally {
74
80
  loading.value = false;
75
81
  }
82
+
83
+ try {
84
+ const resp = await fetch(`vscode-link`, {
85
+ method: "POST",
86
+ headers: {
87
+ Accept: "application/json",
88
+ "Content-Type": "application/json",
89
+ },
90
+ body: JSON.stringify(payload),
91
+ });
92
+ // surface server-side validation message for bad request
93
+ const data = await resp.json().catch(() => ({}));
94
+ if (resp.ok) {
95
+ link.value = data.link || "// no vscode link available";
96
+ } else {
97
+ error.value += (data && data.error) || "Failed to load source";
98
+ }
99
+ } catch (e) {
100
+ error.value = "Failed to load source";
101
+ } finally {
102
+ loading.value = false;
103
+ }
104
+
105
+ if (!error.value && tab.value === "source") {
106
+ highlightLater();
107
+ }
76
108
  }
77
109
 
78
110
  // re-highlight when switching back to source tab
79
111
  watch(
80
112
  () => tab.value,
81
113
  (val) => {
82
- if (val === 'source') {
114
+ if (val === "source") {
83
115
  highlightLater();
84
116
  }
85
117
  }
@@ -43,8 +43,8 @@ export default defineComponent({
43
43
  state.error = null;
44
44
  state.schemas = Array.isArray(props.schemas) ? props.schemas : [];
45
45
  state.schemaOptions = state.schemas.map((s) => ({
46
- label: `${s.name} (${s.fullname})`,
47
- value: s.fullname,
46
+ label: `${s.name} (${s.id})`,
47
+ value: s.id,
48
48
  }));
49
49
  // Maintain compatibility: loadingSchemas flag toggled quickly (no async work)
50
50
  state.loadingSchemas = false;
@@ -54,8 +54,8 @@ export default defineComponent({
54
54
  const needle = (val || "").toLowerCase();
55
55
  update(() => {
56
56
  let opts = state.schemas.map((s) => ({
57
- label: `${s.name} (${s.fullname})`,
58
- value: s.fullname,
57
+ label: `${s.name} (${s.id})`,
58
+ value: s.id,
59
59
  }));
60
60
  if (needle) {
61
61
  opts = opts.filter((o) => o.label.toLowerCase().includes(needle));
@@ -67,7 +67,7 @@ export default defineComponent({
67
67
  function onSchemaChange(val) {
68
68
  state.schemaFullname = val;
69
69
  state.fieldName = null;
70
- const schema = state.schemas.find((s) => s.fullname === val);
70
+ const schema = state.schemas.find((s) => s.id === val);
71
71
  state.fieldOptions = schema ? schema.fields.map((f) => f.name) : [];
72
72
  }
73
73
 
@@ -106,9 +106,9 @@ export default defineComponent({
106
106
  function applyExternalSchema(name) {
107
107
  if (!name || !state.schemas.length) return;
108
108
  if (lastAppliedExternal === name) return; // avoid duplicate
109
- const schema = state.schemas.find((s) => s.fullname === name);
109
+ const schema = state.schemas.find((s) => s.id === name);
110
110
  if (!schema) return;
111
- state.schemaFullname = schema.fullname;
111
+ state.schemaFullname = schema.id;
112
112
  state.fieldOptions = schema.fields.map((f) => f.name);
113
113
  state.fieldName = null; // reset field for external injection
114
114
  lastAppliedExternal = name;
@@ -13,7 +13,7 @@ const app = createApp({
13
13
  tagOptions: [], // array of strings
14
14
  routeId: null,
15
15
  routeOptions: [], // [{ label, value }]
16
- schemaFullname: null,
16
+ schemaId: null,
17
17
  schemaOptions: [], // [{ label, value }]
18
18
  routeItems: {}, // { id: { label, value } }
19
19
  showFields: "object",
@@ -26,7 +26,7 @@ const app = createApp({
26
26
  hidePrimitiveRoute: false,
27
27
  generating: false,
28
28
  rawTags: [], // [{ name, routes: [{ id, name }] }]
29
- rawSchemas: [], // [{ name, fullname }]
29
+ rawSchemas: [], // [{ name, id }]
30
30
  rawSchemasFull: [], // full objects with source_code & fields
31
31
  initializing: true,
32
32
  // Splitter size (left panel width in px)
@@ -47,6 +47,7 @@ const app = createApp({
47
47
  const schemaFieldFilterSchema = ref(null); // external schemaName for schema-field-filter
48
48
  const schemaCodeName = ref("");
49
49
  const routeCodeId = ref("");
50
+
50
51
  function openDetail() {
51
52
  showDetail.value = true;
52
53
  }
@@ -70,10 +71,10 @@ const app = createApp({
70
71
  function onFilterSchemas(val, update) {
71
72
  const normalized = (val || "").toLowerCase();
72
73
  update(() => {
73
- const makeLabel = (s) => `${s.name} (${s.fullname})`;
74
+ const makeLabel = (s) => `${s.name} (${s.id})`;
74
75
  let list = state.rawSchemas.map((s) => ({
75
76
  label: makeLabel(s),
76
- value: s.fullname,
77
+ value: s.id,
77
78
  }));
78
79
  if (normalized) {
79
80
  list = list.filter((opt) =>
@@ -93,7 +94,7 @@ const app = createApp({
93
94
  state.rawSchemasFull = Array.isArray(data.schemas) ? data.schemas : [];
94
95
  state.rawSchemas = state.rawSchemasFull.map((s) => ({
95
96
  name: s.name,
96
- fullname: s.fullname,
97
+ id: s.id,
97
98
  }));
98
99
  state.routeItems = data.tags
99
100
  .map((t) => t.routes)
@@ -105,8 +106,8 @@ const app = createApp({
105
106
 
106
107
  state.tagOptions = state.rawTags.map((t) => t.name);
107
108
  state.schemaOptions = state.rawSchemas.map((s) => ({
108
- label: `${s.name} (${s.fullname})`,
109
- value: s.fullname,
109
+ label: `${s.name} (${s.id})`,
110
+ value: s.id,
110
111
  }));
111
112
  // default route options placeholder
112
113
  state.routeOptions = [];
@@ -117,16 +118,16 @@ const app = createApp({
117
118
  }
118
119
  }
119
120
 
120
- async function onGenerate(resetZoom=true) {
121
+ async function onGenerate(resetZoom = true) {
121
122
  state.generating = true;
122
123
  try {
123
124
  const payload = {
124
125
  tags: state.tag ? [state.tag] : null,
125
- schema_name: state.schemaFullname || null,
126
+ schema_name: state.schemaId || null,
126
127
  route_name: state.routeId || null,
127
128
  show_fields: state.showFields,
128
129
  brief: state.brief,
129
- hide_primitive_route: state.hidePrimitiveRoute
130
+ hide_primitive_route: state.hidePrimitiveRoute,
130
131
  };
131
132
 
132
133
  const res = await fetch("dot", {
@@ -138,21 +139,21 @@ const app = createApp({
138
139
 
139
140
  // create graph instance once
140
141
  const graphUI = new GraphUI("#graph", {
141
- onSchemaClick: (name) => {
142
- if (state.rawSchemas.find((s) => s.fullname === name)) {
143
- schemaFieldFilterSchema.value = name;
142
+ onSchemaClick: (id) => {
143
+ if (state.rawSchemas.find((s) => s.id === id)) {
144
+ schemaFieldFilterSchema.value = id;
144
145
  showSchemaFieldFilter.value = true;
145
146
  }
146
147
  },
147
- onSchemaAltClick: (name) => {
148
- // priority: schema full name; else route id
149
- if (state.rawSchemas.find((s) => s.fullname === name)) {
150
- schemaCodeName.value = name;
148
+ onSchemaAltClick: (id) => {
149
+ // priority: schema id; else route id
150
+ if (state.rawSchemas.find((s) => s.id === id)) {
151
+ schemaCodeName.value = id;
151
152
  showSchemaCode.value = true;
152
153
  return;
153
154
  }
154
- if (name in state.routeItems) {
155
- routeCodeId.value = name;
155
+ if (id in state.routeItems) {
156
+ routeCodeId.value = id;
156
157
  showRouteCode.value = true;
157
158
  return;
158
159
  }
@@ -171,7 +172,7 @@ const app = createApp({
171
172
  try {
172
173
  const payload = {
173
174
  tags: state.tag ? [state.tag] : null,
174
- schema_name: state.schemaFullname || null,
175
+ schema_name: state.schemaId || null,
175
176
  route_name: state.routeId || null,
176
177
  show_fields: state.showFields,
177
178
  brief: state.brief,
@@ -232,16 +233,16 @@ const app = createApp({
232
233
  async function onReset() {
233
234
  state.tag = null;
234
235
  state.routeId = "";
235
- state.schemaFullname = null;
236
+ state.schemaId = null;
236
237
  // state.showFields = "object";
237
238
  state.brief = false;
238
- onGenerate()
239
+ onGenerate();
239
240
  }
240
241
 
241
242
  function toggleTag(tagName, expanded = null) {
242
243
  if (expanded === true) {
243
244
  state.tag = tagName;
244
- state.routeId = ''
245
+ state.routeId = "";
245
246
  onGenerate();
246
247
  return;
247
248
  }
@@ -249,26 +250,26 @@ const app = createApp({
249
250
 
250
251
  function selectRoute(routeId) {
251
252
  if (state.routeId === routeId) {
252
- state.routeId = ''
253
+ state.routeId = "";
253
254
  } else {
254
- state.routeId = routeId
255
+ state.routeId = routeId;
255
256
  }
256
- onGenerate()
257
+ onGenerate();
257
258
  }
258
259
 
259
260
  function toggleShowField(field) {
260
261
  state.showFields = field;
261
- onGenerate(false)
262
+ onGenerate(false);
262
263
  }
263
264
 
264
265
  function toggleBrief(val) {
265
266
  state.brief = val;
266
- onGenerate()
267
+ onGenerate();
267
268
  }
268
-
269
+
269
270
  function toggleHidePrimitiveRoute(val) {
270
271
  state.hidePrimitiveRoute = val;
271
- onGenerate(false)
272
+ onGenerate(false);
272
273
  }
273
274
 
274
275
  onMounted(async () => {
@@ -308,14 +309,14 @@ const app = createApp({
308
309
  // render graph dialog
309
310
  showRenderGraph,
310
311
  renderCoreData,
311
- toggleShowField
312
+ toggleShowField,
312
313
  };
313
314
  },
314
315
  });
315
316
  app.use(window.Quasar);
316
317
  // Set Quasar primary theme color to green
317
- if (window.Quasar && typeof window.Quasar.setCssVar === 'function') {
318
- window.Quasar.setCssVar('primary', '#009485');
318
+ if (window.Quasar && typeof window.Quasar.setCssVar === "function") {
319
+ window.Quasar.setCssVar("primary", "#009485");
319
320
  }
320
321
  app.component("schema-field-filter", SchemaFieldFilter);
321
322
  app.component("schema-code-display", SchemaCodeDisplay);
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.8.3
3
+ Version: 0.9.1
4
4
  Summary: Visualize FastAPI application's routing tree and dependencies
5
5
  Project-URL: Homepage, https://github.com/allmonday/fastapi-voyager
6
6
  Project-URL: Source, https://github.com/allmonday/fastapi-voyager
@@ -30,6 +30,10 @@ Description-Content-Type: text/markdown
30
30
 
31
31
  <p align="center"><img src="./voyager.jpg" alt="" /></p>
32
32
 
33
+
34
+ [![IMAGE ALT TEXT](http://img.youtube.com/vi/PGlbQq1M-n8/0.jpg)](https://www.youtube.com/watch?v=PGlbQq1M-n8 "FastAPI Voyager")
35
+
36
+
33
37
  > This repo is still in early stage, currently it supports pydantic v2 only, previous name: fastapi-router-viz
34
38
 
35
39
  Inspect your API interactively
@@ -105,10 +109,10 @@ click a node to highlight it's upperstream and downstream nodes. figure out the
105
109
 
106
110
  ```python
107
111
  from fastapi import FastAPI
108
- from fastapi_voyager.server import create_app_with_fastapi
112
+ from fastapi_voyager import create_voyager
109
113
  from tests.demo import app
110
114
 
111
- app.mount('/voyager', create_app_with_fastapi(
115
+ app.mount('/voyager', create_voyager(
112
116
  app,
113
117
  module_color={"tests.service": "red"},
114
118
  module_prefix="tests.service"))
@@ -164,7 +168,7 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
164
168
 
165
169
  ## Plan before v1.0
166
170
 
167
- features:
171
+ ### features:
168
172
  - [x] group schemas by module hierarchy
169
173
  - [x] module-based coloring via Analytics(module_color={...})
170
174
  - [x] view in web browser
@@ -183,38 +187,62 @@ features:
183
187
  - [x] add tooltips
184
188
  - [x] route
185
189
  - [x] group routes by module hierarchy
186
- - [ ] add response_model in route
187
- - [ ] support dataclass (pending)
188
- - [ ] click field to highlight links
190
+ - [x] add response_model in route
191
+ - [x] fixed left bar show tag/ route
192
+ - [x] export voyager core data into json (for better debugging)
193
+ - [x] add api to rebuild core data from json, and render it
194
+ - [x] fix Generic case `test_generic.py`
195
+ - [x] show tips for routes not return pydantic type.
196
+ - [x] fix duplicated link from class and parent class, it also break clicking highlight
197
+ - [x] refactor: abstract render module
198
+
199
+ ### backlog
189
200
  - [ ] user can generate nodes/edges manually and connect to generated ones
190
201
  - [ ] add owner
191
202
  - [ ] add extra info for schema
192
- - [ ] ui optimization
193
- - [ ] fixed left/right bar show field information
194
- - [x] fixed left bar show tag/ route
195
- - [ ] display standard ER diagram `difference`
203
+ - [ ] fixed left/right bar show field information
204
+ - [ ] display standard ER diagram `hard`
196
205
  - [ ] display potential invalid links
197
- - [ ] integration with pydantic-resolve
198
- - [ ] show difference between resolve, post fields
199
- - [x] strikethrough for excluded fields
200
- - [ ] display loader as edges
201
- - [x] export voyager core data into json (for better debugging)
202
- - [x] add api to rebuild core data from json, and render it
203
- - [x] fix Generic case `test_generic.py`
204
- - [ ] show tips for routes not return pydantic type.
205
- - [ ] add http method for route
206
206
 
207
207
  bugs & non feature:
208
- - [x] fix duplicated link from class and parent class, it also break clicking highlight
209
208
  - [ ] add tests
210
- - [x] refactor
211
- - [x] abstract render module
209
+ - [ ] support dataclass (pending)
210
+
211
+ ### in analysis
212
+ - [ ] click field to highlight links
213
+ - [ ] animation effect for edges
214
+ - [ ] customrized right click panel
215
+ - [ ] show own dependencies
216
+
217
+ ### plan:
218
+
219
+ #### 0.9
220
+ - [x] refactor: server.py
221
+ - [x] rename create_app_with_fastapi -> create_voyager
222
+ - [x] add doc for parameters
223
+ - [x] improve initialization time cost
224
+ - [x] query route / schema info through realtime api
225
+ - [x] adjust fe
226
+
227
+ #### 0.10
228
+ - [ ] logging information
229
+ - [ ] open route in swagger
230
+ - config docs path
231
+ - [ ] add http method for route
232
+ - [ ] enable/disable module cluster (may save space)
233
+
234
+ #### 0.11
235
+ - [ ] integration with pydantic-resolve
236
+ - [ ] show hint for resolve, post fields
237
+ - [ ] display loader as edges
238
+
212
239
 
213
240
  ## Using with pydantic-resolve
214
241
 
242
+ WIP: ...
243
+
215
244
  pydantic-resolve's @ensure_subset decorator is helpful to pick fields from `source class` in safe.
216
245
 
217
- TODO: ...
218
246
 
219
247
 
220
248
  ## Credits
@@ -225,7 +253,13 @@ TODO: ...
225
253
 
226
254
  ## Changelog
227
255
 
256
+ - 0.9:
257
+ - 0.9.1:
258
+ - api change: from `create_app_with_fastapi` to `create_voyager`, and expose as `from fastapi_voyager import create_voyager`
259
+ - optimization: lazy load vscode link and source code, speed up the initialization.
228
260
  - 0.8:
261
+ - 0.8.3
262
+ - upgrade theme
229
263
  - 0.8.2
230
264
  - fix silly typo.
231
265
  - 0.8.1
@@ -1,24 +1,24 @@
1
- fastapi_voyager/__init__.py,sha256=E5WTV_sYs2LK8I6jzA7AuvFU5a8_vjnDseC3DMha0iQ,149
2
- fastapi_voyager/cli.py,sha256=2eixX7mtPsZvukc4vrwQOt6XTPJgHUKIGLBy3IIC2jE,11127
1
+ fastapi_voyager/__init__.py,sha256=tZy0Nkj8kTaMgbvHy-mGxVcFGVX0Km-36dnzsAIG2uk,230
2
+ fastapi_voyager/cli.py,sha256=kQb4g6JEGZR99e5r8LyFFEeb_-uT-n_gp_sDoYG3R7k,11118
3
3
  fastapi_voyager/filter.py,sha256=2Yt37o8mhqSqleafO4YRrumh_ExYUqzXFOxQRPuTbAc,8078
4
4
  fastapi_voyager/module.py,sha256=Z2QHNmiLk6ZAJlm2nSmO875Q33TweSg8UxZSzIpU9zY,3499
5
5
  fastapi_voyager/render.py,sha256=qy3g1Rz1s8XkuR_6n1Q1YPwy_oMOjWjNswTHQjdz4N0,7765
6
- fastapi_voyager/server.py,sha256=Q9-f0frKHC4d0vN8ZasZyw6AsWKwLRB0P7z8dZ0lzjQ,4108
7
- fastapi_voyager/type.py,sha256=k62cDofqmy-2q5rInW5LydLaRuDpKLsdbapps8Osqys,1838
6
+ fastapi_voyager/server.py,sha256=pg-LHDj4yU0usDA1b2X2Kt2_OCrexFA2G9ifGxb52Uc,6196
7
+ fastapi_voyager/type.py,sha256=pWYKmgb9e0W_JeD7k54Mr2lxUZV_Ir9TNpewGRwHyHQ,1629
8
8
  fastapi_voyager/type_helper.py,sha256=hjBC4E0tgBpQDlYxGg74uK07SXjsrAgictEETJfIpYM,9231
9
- fastapi_voyager/version.py,sha256=oFsY5N35bJE0AakSBQ3J7JuwVWsgL4KEN-zNDsKGJ-Q,48
10
- fastapi_voyager/voyager.py,sha256=Tz-cJW1Pg9Ei6zX2CPCdAnkTqErp2OIIhhxeeDd6Nss,11221
9
+ fastapi_voyager/version.py,sha256=jjAALUE-D-m7hgihxqGZyrZQPe4_Ch5MQPu89WFuVmw,48
10
+ fastapi_voyager/voyager.py,sha256=pXvA3ye5Jq6aJ6YrZFbTHjJ9MzdydqWllIgY8RFK_4A,10793
11
11
  fastapi_voyager/web/graph-ui.js,sha256=0hqMCzsPJfhgWDuwiXVXaGQL-_7urT_zryo4sXMN8jQ,4209
12
12
  fastapi_voyager/web/graphviz.svg.css,sha256=zDCjjpT0Idufu5YOiZI76PL70-avP3vTyzGPh9M85Do,1563
13
13
  fastapi_voyager/web/graphviz.svg.js,sha256=lvAdbjHc-lMSk4GQp-iqYA2PCFX4RKnW7dFaoe0LUHs,16005
14
14
  fastapi_voyager/web/index.html,sha256=AnsqXfzDrUPO12EWtiT_XJdJ6Kx3qBsRn1IfX393G48,14298
15
15
  fastapi_voyager/web/quasar.min.css,sha256=F5jQe7X2XT54VlvAaa2V3GsBFdVD-vxDZeaPLf6U9CU,203145
16
16
  fastapi_voyager/web/quasar.min.js,sha256=h0ftyPMW_CRiyzeVfQqiup0vrVt4_QWojpqmpnpn07E,502974
17
- fastapi_voyager/web/vue-main.js,sha256=F-JUjwsN0Mdu5KWKNjqIEAzoucVQ9Zu-zE61fMDTdME,9551
17
+ fastapi_voyager/web/vue-main.js,sha256=p8akwqNf9sDM71xio0SN-3JznUOIksoy9SGRiqAeg2s,9459
18
18
  fastapi_voyager/web/component/render-graph.js,sha256=e8Xgh2Kl-nYU0P1gstEmAepCgFnk2J6UdxW8TlMafGs,2322
19
- fastapi_voyager/web/component/route-code-display.js,sha256=NECC1OGcPCdDfbghtRJEnmFM6HmH5J3win2ibapWPeA,2649
20
- fastapi_voyager/web/component/schema-code-display.js,sha256=oOusgTvCaWGnoKb-NBwu0SXqJJf2PTUtp3lUczokTBM,5515
21
- fastapi_voyager/web/component/schema-field-filter.js,sha256=5lK2mdgp7_UWcwURi9djRCkIwih-3f4EmDbhKFuLBDc,6266
19
+ fastapi_voyager/web/component/route-code-display.js,sha256=4VN88n_7FYHxvruP5AbgTdF6Lyj6v_7Ocqy8zOVMjVA,4065
20
+ fastapi_voyager/web/component/schema-code-display.js,sha256=_Okkr8ceJsA_Xngyj_GpxJqdN01GzCjJTeAZamSA9Ks,6514
21
+ fastapi_voyager/web/component/schema-field-filter.js,sha256=1llPkN8w-RrBPHVtjjMcObndaqPnmZ7e4iU6dMJMIaE,6224
22
22
  fastapi_voyager/web/icon/android-chrome-192x192.png,sha256=35sBy6jmUFJCcquStaafHH1qClZIbd-X3PIKSeLkrNo,37285
23
23
  fastapi_voyager/web/icon/android-chrome-512x512.png,sha256=eb2eDjCwIruc05029_0L9hcrkVkv8KceLn1DJMYU0zY,210789
24
24
  fastapi_voyager/web/icon/apple-touch-icon.png,sha256=gnWK46tPnvSw1-oYZjgI02wpoO4OrIzsVzGHC5oKWO0,33187
@@ -26,8 +26,8 @@ fastapi_voyager/web/icon/favicon-16x16.png,sha256=JC07jEzfIYxBIoQn_FHXvyHuxESdhW
26
26
  fastapi_voyager/web/icon/favicon-32x32.png,sha256=C7v1h58cfWOsiLp9yOIZtlx-dLasBcq3NqpHVGRmpt4,1859
27
27
  fastapi_voyager/web/icon/favicon.ico,sha256=tZolYIXkkBcFiYl1A8ksaXN2VjGamzcSdes838dLvNc,15406
28
28
  fastapi_voyager/web/icon/site.webmanifest,sha256=ep4Hzh9zhmiZF2At3Fp1dQrYQuYF_3ZPZxc1KcGBvwQ,263
29
- fastapi_voyager-0.8.3.dist-info/METADATA,sha256=bA7QuzvsXCjTX3vqS31SFzKrquWrad7NKHot6pXFRvw,8082
30
- fastapi_voyager-0.8.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
- fastapi_voyager-0.8.3.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
32
- fastapi_voyager-0.8.3.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
33
- fastapi_voyager-0.8.3.dist-info/RECORD,,
29
+ fastapi_voyager-0.9.1.dist-info/METADATA,sha256=7yR9Wj87QH6f2kxA4QiqDnwWJPQ2-rGOhpC6ZYYfHfE,8914
30
+ fastapi_voyager-0.9.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
+ fastapi_voyager-0.9.1.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
32
+ fastapi_voyager-0.9.1.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
33
+ fastapi_voyager-0.9.1.dist-info/RECORD,,