fastapi-voyager 0.8.3__py3-none-any.whl → 0.9.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.
@@ -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.2"
@@ -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
@@ -4,60 +4,119 @@ const { defineComponent, ref, watch, onMounted } = window.Vue;
4
4
  // Props:
5
5
  // routeId: route id key in routeItems
6
6
  // modelValue: dialog visibility
7
- // routes: object map { id: { id, name, source_code } }
8
7
  export default defineComponent({
9
- name: 'RouteCodeDisplay',
8
+ name: "RouteCodeDisplay",
10
9
  props: {
11
10
  routeId: { type: String, required: true },
12
11
  modelValue: { type: Boolean, default: false },
13
- routes: { type: Object, default: () => ({}) },
14
12
  },
15
- emits: ['close'],
13
+ emits: ["close"],
16
14
  setup(props, { emit }) {
17
- const code = ref('');
18
- const error = ref(null);
19
- const link = ref('');
15
+ const loading = ref(false);
16
+ const code = ref("");
17
+ const error = ref("");
18
+ const link = ref("");
20
19
 
21
- function close() { emit('close'); }
20
+ function close() {
21
+ emit("close");
22
+ }
22
23
 
23
24
  function highlightLater() {
24
25
  requestAnimationFrame(() => {
25
26
  try {
26
27
  if (window.hljs) {
27
- const block = document.querySelector('.frv-route-code-display pre code.language-python');
28
+ const block = document.querySelector(
29
+ ".frv-route-code-display pre code.language-python"
30
+ );
28
31
  if (block) {
29
32
  window.hljs.highlightElement(block);
30
33
  }
31
34
  }
32
35
  } catch (e) {
33
- console.warn('highlight failed', e);
36
+ console.warn("highlight failed", e);
34
37
  }
35
38
  });
36
39
  }
37
40
 
38
- function load() {
41
+ async function load() {
42
+ if (!props.routeId) {
43
+ code.value = "";
44
+ return;
45
+ }
46
+
47
+ loading.value = true;
39
48
  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 || '';
49
+ code.value = "";
50
+ link.value = "";
51
+
52
+ // try to fetch from server: POST /source with { schema_name: routeId }
53
+ const payload = { schema_name: props.routeId };
54
+ try {
55
+ const resp = await fetch(`source`, {
56
+ method: "POST",
57
+ headers: {
58
+ Accept: "application/json",
59
+ "Content-Type": "application/json",
60
+ },
61
+ body: JSON.stringify(payload),
62
+ });
63
+
64
+ const data = await resp.json().catch(() => ({}));
65
+ if (resp.ok) {
66
+ code.value = data.source_code || "// no source code available";
67
+ } else {
68
+ error.value = (data && data.error) || "Failed to load source";
69
+ }
70
+ } catch (e) {
71
+ error.value = e && e.message ? e.message : "Failed to load source";
72
+ } finally {
73
+ loading.value = false;
74
+ }
75
+
76
+ try {
77
+ const resp = await fetch(`vscode-link`, {
78
+ method: "POST",
79
+ headers: {
80
+ Accept: "application/json",
81
+ "Content-Type": "application/json",
82
+ },
83
+ body: JSON.stringify(payload),
84
+ });
85
+
86
+ const data = await resp.json().catch(() => ({}));
87
+ if (resp.ok) {
88
+ link.value = data.link || "// no source code available";
89
+ } else {
90
+ error.value += (data && data.error) || "Failed to load vscode link";
91
+ }
92
+ } catch (e) {
93
+ } finally {
94
+ loading.value = false;
95
+ }
96
+
97
+ if (!error.value) {
45
98
  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
99
  }
53
100
  }
54
101
 
55
- watch(() => props.modelValue, (v) => { if (v) load(); });
56
- watch(() => props.routeId, () => { if (props.modelValue) load(); });
102
+ watch(
103
+ () => props.modelValue,
104
+ (v) => {
105
+ if (v) load();
106
+ }
107
+ );
108
+ watch(
109
+ () => props.routeId,
110
+ () => {
111
+ if (props.modelValue) load();
112
+ }
113
+ );
57
114
 
58
- onMounted(() => { if (props.modelValue) load(); });
115
+ onMounted(() => {
116
+ if (props.modelValue) load();
117
+ });
59
118
 
60
- return { code, error, close, link };
119
+ return { loading, code, error, close, link };
61
120
  },
62
121
  template: `
63
122
  <div class="frv-route-code-display" style="border:1px solid #ccc; position:relative; width:50vw; max-width:50vw; height:100%; background:#fff;">
@@ -66,8 +125,9 @@ export default defineComponent({
66
125
  <a :href="link" target="_blank" rel="noopener" style="font-size:12px; color:#3b82f6;">Open in VSCode</a>
67
126
  </div>
68
127
  <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>
128
+ <div v-if="loading" style="font-family:Menlo, monospace; font-size:12px;">Loading source...</div>
129
+ <div v-else-if="error" style="color:#c10015; font-family:Menlo, monospace; font-size:12px;">{{ error }}</div>
70
130
  <pre v-else style="margin:0;"><code class="language-python">{{ code }}</code></pre>
71
131
  </div>
72
- </div>`
132
+ </div>`,
73
133
  });
@@ -14,17 +14,16 @@ 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
- schemas: { type: Array, default: () => [] },
17
+ schemas: { type: Object, default: () => ({}) },
19
18
  },
20
19
  emits: ["close"],
21
20
  setup(props, { emit }) {
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,73 @@ 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";
74
+ } else {
75
+ error.value = (data && data.error) || "Failed to load source";
76
+ }
77
+ } catch (e) {
78
+ error.value = "Failed to load source";
79
+ } finally {
80
+ loading.value = false;
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";
68
96
  } else {
69
- error.value = "Schema not found";
97
+ error.value += (data && data.error) || "Failed to load source";
70
98
  }
71
99
  } catch (e) {
72
100
  error.value = "Failed to load source";
73
101
  } finally {
74
102
  loading.value = false;
75
103
  }
104
+
105
+ fields.value = props.schemas[props.schemaName].fields || []
106
+
107
+ if (!error.value && tab.value === "source") {
108
+ highlightLater();
109
+ }
76
110
  }
77
111
 
78
112
  // re-highlight when switching back to source tab
79
113
  watch(
80
114
  () => tab.value,
81
115
  (val) => {
82
- if (val === 'source') {
116
+ if (val === "source") {
83
117
  highlightLater();
84
118
  }
85
119
  }
@@ -14,7 +14,8 @@ export default defineComponent({
14
14
  name: "SchemaFieldFilter",
15
15
  props: {
16
16
  schemaName: { type: String, default: null }, // external injection triggers auto-query
17
- schemas: { type: Array, default: () => [] }, // externally provided schemas (state.rawSchemasFull or similar)
17
+ // externally provided schemas dict (state.rawSchemasFull): { [id]: schema }
18
+ schemas: { type: Object, default: () => ({}) },
18
19
  },
19
20
  emits: ["queried", "close"],
20
21
  setup(props, { emit }) {
@@ -39,12 +40,14 @@ export default defineComponent({
39
40
  let lastAppliedExternal = null;
40
41
 
41
42
  async function loadSchemas() {
42
- // Refactored: use externally provided props.schemas directly; no network call.
43
+ // Use externally provided props.schemas dict directly; no network call.
43
44
  state.error = null;
44
- state.schemas = Array.isArray(props.schemas) ? props.schemas : [];
45
+ const dict = props.schemas && typeof props.schemas === "object" ? props.schemas : {};
46
+ // Flatten to array for local operations
47
+ state.schemas = Object.values(dict);
45
48
  state.schemaOptions = state.schemas.map((s) => ({
46
- label: `${s.name} (${s.fullname})`,
47
- value: s.fullname,
49
+ label: `${s.name} (${s.id})`,
50
+ value: s.id,
48
51
  }));
49
52
  // Maintain compatibility: loadingSchemas flag toggled quickly (no async work)
50
53
  state.loadingSchemas = false;
@@ -54,8 +57,8 @@ export default defineComponent({
54
57
  const needle = (val || "").toLowerCase();
55
58
  update(() => {
56
59
  let opts = state.schemas.map((s) => ({
57
- label: `${s.name} (${s.fullname})`,
58
- value: s.fullname,
60
+ label: `${s.name} (${s.id})`,
61
+ value: s.id,
59
62
  }));
60
63
  if (needle) {
61
64
  opts = opts.filter((o) => o.label.toLowerCase().includes(needle));
@@ -67,7 +70,7 @@ export default defineComponent({
67
70
  function onSchemaChange(val) {
68
71
  state.schemaFullname = val;
69
72
  state.fieldName = null;
70
- const schema = state.schemas.find((s) => s.fullname === val);
73
+ const schema = state.schemas.find((s) => s.id === val);
71
74
  state.fieldOptions = schema ? schema.fields.map((f) => f.name) : [];
72
75
  }
73
76
 
@@ -106,9 +109,9 @@ export default defineComponent({
106
109
  function applyExternalSchema(name) {
107
110
  if (!name || !state.schemas.length) return;
108
111
  if (lastAppliedExternal === name) return; // avoid duplicate
109
- const schema = state.schemas.find((s) => s.fullname === name);
112
+ const schema = state.schemas.find((s) => s.id === name);
110
113
  if (!schema) return;
111
- state.schemaFullname = schema.fullname;
114
+ state.schemaFullname = schema.id;
112
115
  state.fieldOptions = schema.fields.map((f) => f.name);
113
116
  state.fieldName = null; // reset field for external injection
114
117
  lastAppliedExternal = name;
@@ -197,7 +197,7 @@
197
197
  </div>
198
198
  </div>
199
199
  </div>
200
- <div style="flex: 1 1 auto; min-height: 0;">
200
+ <div style="flex: 1 1 auto; min-height: 10px;">
201
201
  <q-splitter
202
202
  v-model="state.splitter"
203
203
  unit="px"
@@ -323,7 +323,6 @@
323
323
  <route-code-display
324
324
  :route-id="routeCodeId"
325
325
  :model-value="showRouteCode"
326
- :routes="state.routeItems"
327
326
  @close="showRouteCode = false"
328
327
  />
329
328
  </q-dialog>
@@ -9,13 +9,9 @@ const app = createApp({
9
9
  setup() {
10
10
  const state = reactive({
11
11
  // options and selections
12
- tag: null,
13
- tagOptions: [], // array of strings
14
- routeId: null,
15
- routeOptions: [], // [{ label, value }]
16
- schemaFullname: null,
17
- schemaOptions: [], // [{ label, value }]
18
- routeItems: {}, // { id: { label, value } }
12
+ tag: null, // picked tag
13
+ routeId: null, // picked route
14
+ schemaId: null, // picked schema
19
15
  showFields: "object",
20
16
  fieldOptions: [
21
17
  { label: "No fields", value: "single" },
@@ -26,17 +22,17 @@ const app = createApp({
26
22
  hidePrimitiveRoute: false,
27
23
  generating: false,
28
24
  rawTags: [], // [{ name, routes: [{ id, name }] }]
29
- rawSchemas: [], // [{ name, fullname }]
30
- rawSchemasFull: [], // full objects with source_code & fields
25
+ rawSchemas: new Set(), // [{ name, id }]
26
+ rawSchemasFull: {}, // full schemas dict: { [schema.id]: schema }
31
27
  initializing: true,
32
28
  // Splitter size (left panel width in px)
33
29
  splitter: 300,
34
30
  });
31
+
35
32
  const showDetail = ref(false);
36
33
  const showSchemaFieldFilter = ref(false);
37
34
  const showSchemaCode = ref(false);
38
35
  const showRouteCode = ref(false);
39
- // Dump/Import dialogs and rendered graph dialog
40
36
  const showDumpDialog = ref(false);
41
37
  const dumpJson = ref("");
42
38
  const showImportDialog = ref(false);
@@ -47,6 +43,7 @@ const app = createApp({
47
43
  const schemaFieldFilterSchema = ref(null); // external schemaName for schema-field-filter
48
44
  const schemaCodeName = ref("");
49
45
  const routeCodeId = ref("");
46
+
50
47
  function openDetail() {
51
48
  showDetail.value = true;
52
49
  }
@@ -54,47 +51,18 @@ const app = createApp({
54
51
  showDetail.value = false;
55
52
  }
56
53
 
57
- function onFilterTags(val, update) {
58
- const normalized = (val || "").toLowerCase();
59
- update(() => {
60
- if (!normalized) {
61
- state.tagOptions = state.rawTags.map((t) => t.name);
62
- return;
63
- }
64
- state.tagOptions = state.rawTags
65
- .map((t) => t.name)
66
- .filter((n) => n.toLowerCase().includes(normalized));
67
- });
68
- }
69
-
70
- function onFilterSchemas(val, update) {
71
- const normalized = (val || "").toLowerCase();
72
- update(() => {
73
- const makeLabel = (s) => `${s.name} (${s.fullname})`;
74
- let list = state.rawSchemas.map((s) => ({
75
- label: makeLabel(s),
76
- value: s.fullname,
77
- }));
78
- if (normalized) {
79
- list = list.filter((opt) =>
80
- opt.label.toLowerCase().includes(normalized)
81
- );
82
- }
83
- state.schemaOptions = list;
84
- });
85
- }
86
-
87
54
  async function loadInitial() {
88
55
  state.initializing = true;
89
56
  try {
90
57
  const res = await fetch("dot");
91
58
  const data = await res.json();
92
59
  state.rawTags = Array.isArray(data.tags) ? data.tags : [];
93
- state.rawSchemasFull = Array.isArray(data.schemas) ? data.schemas : [];
94
- state.rawSchemas = state.rawSchemasFull.map((s) => ({
95
- name: s.name,
96
- fullname: s.fullname,
97
- }));
60
+ const schemasArr = Array.isArray(data.schemas) ? data.schemas : [];
61
+ // Build dict keyed by id for faster lookups and simpler prop passing
62
+ state.rawSchemasFull = Object.fromEntries(
63
+ schemasArr.map((s) => [s.id, s])
64
+ );
65
+ state.rawSchemas = new Set(Object.keys(state.rawSchemasFull));
98
66
  state.routeItems = data.tags
99
67
  .map((t) => t.routes)
100
68
  .flat()
@@ -103,13 +71,7 @@ const app = createApp({
103
71
  return acc;
104
72
  }, {});
105
73
 
106
- state.tagOptions = state.rawTags.map((t) => t.name);
107
- state.schemaOptions = state.rawSchemas.map((s) => ({
108
- label: `${s.name} (${s.fullname})`,
109
- value: s.fullname,
110
- }));
111
74
  // default route options placeholder
112
- state.routeOptions = [];
113
75
  } catch (e) {
114
76
  console.error("Initial load failed", e);
115
77
  } finally {
@@ -117,16 +79,16 @@ const app = createApp({
117
79
  }
118
80
  }
119
81
 
120
- async function onGenerate(resetZoom=true) {
82
+ async function onGenerate(resetZoom = true) {
121
83
  state.generating = true;
122
84
  try {
123
85
  const payload = {
124
86
  tags: state.tag ? [state.tag] : null,
125
- schema_name: state.schemaFullname || null,
87
+ schema_name: state.schemaId || null,
126
88
  route_name: state.routeId || null,
127
89
  show_fields: state.showFields,
128
90
  brief: state.brief,
129
- hide_primitive_route: state.hidePrimitiveRoute
91
+ hide_primitive_route: state.hidePrimitiveRoute,
130
92
  };
131
93
 
132
94
  const res = await fetch("dot", {
@@ -138,21 +100,20 @@ const app = createApp({
138
100
 
139
101
  // create graph instance once
140
102
  const graphUI = new GraphUI("#graph", {
141
- onSchemaClick: (name) => {
142
- if (state.rawSchemas.find((s) => s.fullname === name)) {
143
- schemaFieldFilterSchema.value = name;
103
+ onSchemaClick: (id) => {
104
+ if (state.rawSchemas.has(id)) {
105
+ schemaFieldFilterSchema.value = id;
144
106
  showSchemaFieldFilter.value = true;
145
107
  }
146
108
  },
147
- onSchemaAltClick: (name) => {
148
- // priority: schema full name; else route id
149
- if (state.rawSchemas.find((s) => s.fullname === name)) {
150
- schemaCodeName.value = name;
109
+ onSchemaAltClick: (id) => {
110
+ if (state.rawSchemas.has(id)) {
111
+ schemaCodeName.value = id;
151
112
  showSchemaCode.value = true;
152
113
  return;
153
114
  }
154
- if (name in state.routeItems) {
155
- routeCodeId.value = name;
115
+ if (id in state.routeItems) {
116
+ routeCodeId.value = id;
156
117
  showRouteCode.value = true;
157
118
  return;
158
119
  }
@@ -171,7 +132,7 @@ const app = createApp({
171
132
  try {
172
133
  const payload = {
173
134
  tags: state.tag ? [state.tag] : null,
174
- schema_name: state.schemaFullname || null,
135
+ schema_name: state.schemaId || null,
175
136
  route_name: state.routeId || null,
176
137
  show_fields: state.showFields,
177
138
  brief: state.brief,
@@ -232,16 +193,16 @@ const app = createApp({
232
193
  async function onReset() {
233
194
  state.tag = null;
234
195
  state.routeId = "";
235
- state.schemaFullname = null;
196
+ state.schemaId = null;
236
197
  // state.showFields = "object";
237
198
  state.brief = false;
238
- onGenerate()
199
+ onGenerate();
239
200
  }
240
201
 
241
202
  function toggleTag(tagName, expanded = null) {
242
203
  if (expanded === true) {
243
204
  state.tag = tagName;
244
- state.routeId = ''
205
+ state.routeId = "";
245
206
  onGenerate();
246
207
  return;
247
208
  }
@@ -249,26 +210,26 @@ const app = createApp({
249
210
 
250
211
  function selectRoute(routeId) {
251
212
  if (state.routeId === routeId) {
252
- state.routeId = ''
213
+ state.routeId = "";
253
214
  } else {
254
- state.routeId = routeId
215
+ state.routeId = routeId;
255
216
  }
256
- onGenerate()
217
+ onGenerate();
257
218
  }
258
219
 
259
220
  function toggleShowField(field) {
260
221
  state.showFields = field;
261
- onGenerate(false)
222
+ onGenerate(false);
262
223
  }
263
224
 
264
225
  function toggleBrief(val) {
265
226
  state.brief = val;
266
- onGenerate()
227
+ onGenerate();
267
228
  }
268
-
229
+
269
230
  function toggleHidePrimitiveRoute(val) {
270
231
  state.hidePrimitiveRoute = val;
271
- onGenerate(false)
232
+ onGenerate(false);
272
233
  }
273
234
 
274
235
  onMounted(async () => {
@@ -281,8 +242,6 @@ const app = createApp({
281
242
  toggleBrief,
282
243
  toggleHidePrimitiveRoute,
283
244
  selectRoute,
284
- onFilterTags,
285
- onFilterSchemas,
286
245
  onGenerate,
287
246
  onReset,
288
247
  showDetail,
@@ -308,14 +267,14 @@ const app = createApp({
308
267
  // render graph dialog
309
268
  showRenderGraph,
310
269
  renderCoreData,
311
- toggleShowField
270
+ toggleShowField,
312
271
  };
313
272
  },
314
273
  });
315
274
  app.use(window.Quasar);
316
275
  // Set Quasar primary theme color to green
317
- if (window.Quasar && typeof window.Quasar.setCssVar === 'function') {
318
- window.Quasar.setCssVar('primary', '#009485');
276
+ if (window.Quasar && typeof window.Quasar.setCssVar === "function") {
277
+ window.Quasar.setCssVar("primary", "#009485");
319
278
  }
320
279
  app.component("schema-field-filter", SchemaFieldFilter);
321
280
  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.2
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
@@ -28,13 +28,12 @@ Description-Content-Type: text/markdown
28
28
  [![pypi](https://img.shields.io/pypi/v/fastapi-voyager.svg)](https://pypi.python.org/pypi/fastapi-voyager)
29
29
  ![Python Versions](https://img.shields.io/pypi/pyversions/fastapi-voyager)
30
30
 
31
- <p align="center"><img src="./voyager.jpg" alt="" /></p>
32
-
33
- > This repo is still in early stage, currently it supports pydantic v2 only, previous name: fastapi-router-viz
31
+ > This repo is still in early stage, it supports pydantic v2 only
34
32
 
35
- Inspect your API interactively
33
+ Inspect your API interactively!
36
34
 
37
- <img width="1480" height="648" alt="image" src="https://github.com/user-attachments/assets/a6ccc9f1-cf06-493a-b99b-eb07767564bd" />
35
+ <p align="center"><img src="./voyager.jpg" alt="" /></p>
36
+ <p align="center"><a target="_blank" rel="" href="https://www.youtube.com/watch?v=PGlbQq1M-n8"><img src="http://img.youtube.com/vi/PGlbQq1M-n8/0.jpg" alt="" style="max-width: 100%;"></a></p>
38
37
 
39
38
  ## Installation
40
39
 
@@ -48,11 +47,6 @@ uv add fastapi-voyager
48
47
  voyager -m path.to.your.app.module --server
49
48
  ```
50
49
 
51
- ## Dependencies
52
-
53
- - FastAPI
54
- - [pydantic-resolve](https://github.com/allmonday/pydantic-resolve)
55
-
56
50
 
57
51
  ## Feature
58
52
 
@@ -75,9 +69,9 @@ voyager -m tests.demo
75
69
  ```
76
70
 
77
71
  ### generate the graph
78
- after initialization, pick tag, rotue (optional) and click `generate`.
72
+ after initialization, pick tag, rotue to render graph
79
73
 
80
- <img width="1919" height="898" alt="image" style="border: 1px solid #aaa" src="https://github.com/user-attachments/assets/05e321d0-49f3-4af6-a7c7-f4c9c6b1dbfd" />
74
+ <img width="1628" height="765" alt="image" src="https://github.com/user-attachments/assets/b4712f82-e754-453b-aa69-24c932b8f48f" />
81
75
 
82
76
  ### highlight
83
77
  click a node to highlight it's upperstream and downstream nodes. figure out the related models of one page, or homw many pages are related with one model.
@@ -85,9 +79,9 @@ click a node to highlight it's upperstream and downstream nodes. figure out the
85
79
  <img width="1485" height="616" alt="image" style="border: 1px solid #aaa" src="https://github.com/user-attachments/assets/70c4095f-86c7-45da-a6f0-fd41ac645813" />
86
80
 
87
81
  ### filter related nodes
88
- `shift` click a node to check related node, pick a field to narrow the result.
82
+ `shift` click a node to check related node, pick a field to narrow the result, picked node is marked as red.
89
83
 
90
- <img width="1917" height="800" alt="image" style="border: 1px solid #aaa" src="https://github.com/user-attachments/assets/e770dc70-f293-49e1-bcd7-d8dffa15d9ea" />
84
+ <img width="1423" height="552" alt="image" src="https://github.com/user-attachments/assets/468a058d-afa1-4601-a7c5-c6aad6a8a557" />
91
85
 
92
86
  ### view source code
93
87
  `alt` click a node to show source code or open file in vscode.
@@ -105,10 +99,10 @@ click a node to highlight it's upperstream and downstream nodes. figure out the
105
99
 
106
100
  ```python
107
101
  from fastapi import FastAPI
108
- from fastapi_voyager.server import create_app_with_fastapi
102
+ from fastapi_voyager import create_voyager
109
103
  from tests.demo import app
110
104
 
111
- app.mount('/voyager', create_app_with_fastapi(
105
+ app.mount('/voyager', create_voyager(
112
106
  app,
113
107
  module_color={"tests.service": "red"},
114
108
  module_prefix="tests.service"))
@@ -164,7 +158,25 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
164
158
 
165
159
  ## Plan before v1.0
166
160
 
167
- features:
161
+
162
+ ### backlog
163
+ - [ ] user can generate nodes/edges manually and connect to generated ones
164
+ - [ ] add owner
165
+ - [ ] add extra info for schema
166
+ - [ ] fixed left/right bar show field information
167
+ - [ ] display standard ER diagram `hard`
168
+ - [ ] display potential invalid links
169
+ - [ ] support dataclass (pending)
170
+
171
+ ### in analysis
172
+ - [ ] click field to highlight links
173
+ - [ ] animation effect for edges
174
+ - [ ] customrized right click panel
175
+ - [ ] show own dependencies
176
+ - [ ] clean up fe code
177
+
178
+ ### plan:
179
+ #### <0.9:
168
180
  - [x] group schemas by module hierarchy
169
181
  - [x] module-based coloring via Analytics(module_color={...})
170
182
  - [x] view in web browser
@@ -183,49 +195,80 @@ features:
183
195
  - [x] add tooltips
184
196
  - [x] route
185
197
  - [x] group routes by module hierarchy
186
- - [ ] add response_model in route
187
- - [ ] support dataclass (pending)
188
- - [ ] click field to highlight links
189
- - [ ] user can generate nodes/edges manually and connect to generated ones
190
- - [ ] add owner
191
- - [ ] 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`
196
- - [ ] 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
198
+ - [x] add response_model in route
199
+ - [x] fixed left bar show tag/ route
201
200
  - [x] export voyager core data into json (for better debugging)
202
201
  - [x] add api to rebuild core data from json, and render it
203
202
  - [x] fix Generic case `test_generic.py`
204
- - [ ] show tips for routes not return pydantic type.
205
- - [ ] add http method for route
206
-
207
- bugs & non feature:
203
+ - [x] show tips for routes not return pydantic type.
208
204
  - [x] fix duplicated link from class and parent class, it also break clicking highlight
205
+ - [x] refactor: abstract render module
206
+
207
+ #### 0.9
208
+ - [x] refactor: server.py
209
+ - [x] rename create_app_with_fastapi -> create_voyager
210
+ - [x] add doc for parameters
211
+ - [x] improve initialization time cost
212
+ - [x] query route / schema info through realtime api
213
+ - [x] adjust fe
214
+
215
+ #### 0.10
216
+ - [ ] support opening route in swagger
217
+ - config docs path
218
+ - [ ] add http method for route
219
+ - [ ] enable/disable module cluster (may save space)
220
+ - [ ] logging information
209
221
  - [ ] add tests
210
- - [x] refactor
211
- - [x] abstract render module
222
+ - [ ] hide brief mode if not configured
223
+ - [ ] optimize static resource
224
+ - [ ] show route count in tag expansion item
225
+ - [ ] route list show have a max height to trigger scrollable
226
+ - [ ] fix layout issue when rendering huge graph
227
+
228
+ #### 0.11
229
+ - [ ] improve user experience
230
+ - double click to show detail
231
+ - improve search dialog
232
+
233
+ #### 0.12
234
+ - [ ] integration with pydantic-resolve
235
+ - [ ] show hint for resolve, post fields
236
+ - [ ] display loader as edges
237
+
212
238
 
213
239
  ## Using with pydantic-resolve
214
240
 
241
+ WIP: ...
242
+
215
243
  pydantic-resolve's @ensure_subset decorator is helpful to pick fields from `source class` in safe.
216
244
 
217
- TODO: ...
218
245
 
219
246
 
220
247
  ## Credits
221
248
 
222
- - https://apis.guru/graphql-voyager/, for inspiration.
223
- - https://github.com/tintinweb/vscode-interactive-graphviz, for web visualization.
249
+ - https://apis.guru/graphql-voyager/, thanks for inspiration.
250
+ - https://github.com/tintinweb/vscode-interactive-graphviz, thanks for web visualization.
251
+
252
+
253
+ ## Dependencies
254
+
255
+ - FastAPI
256
+ - [pydantic-resolve](https://github.com/allmonday/pydantic-resolve)
257
+ - Quasar
224
258
 
225
259
 
226
260
  ## Changelog
227
261
 
262
+ - 0.9:
263
+ - 0.9.2:
264
+ - fix: missing fields in schema detail panel
265
+ - optimization: clean up fe codes.
266
+ - 0.9.1:
267
+ - api change: from `create_app_with_fastapi` to `create_voyager`, and expose as `from fastapi_voyager import create_voyager`
268
+ - optimization: lazy load vscode link and source code, speed up the initialization.
228
269
  - 0.8:
270
+ - 0.8.3
271
+ - upgrade theme
229
272
  - 0.8.2
230
273
  - fix silly typo.
231
274
  - 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=Su4VudgwOkDWEBzNahJ3LX1SbIA7uezku9g1zsUddHU,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
- fastapi_voyager/web/index.html,sha256=AnsqXfzDrUPO12EWtiT_XJdJ6Kx3qBsRn1IfX393G48,14298
14
+ fastapi_voyager/web/index.html,sha256=pmctp4OizRd8ETKEI0vl0Seu9nIQklfnbFDDyztFoy4,14264
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=ZRJQBV54YqoKkLI2a8WHF6uYeFUpqwBl-lykkaYxCXI,8182
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=Pdg8JX_yHhoUUGSqgRs_zTciIfw5wYzG6-kQfHtdWFU,3956
20
+ fastapi_voyager/web/component/schema-code-display.js,sha256=Aa_mXxmNgIbImoT1_zuzsPgbPq8QaakUAzNWTxHxM58,6584
21
+ fastapi_voyager/web/component/schema-field-filter.js,sha256=PR8d2_u1UiSLrS5zqAvY2UR8LbvjBqiUDt71tm1DJTs,6345
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.2.dist-info/METADATA,sha256=k_l0KTvYeGwMhRPdulVlob3_q-uZ9UTEHo_KLeXTHsQ,9221
30
+ fastapi_voyager-0.9.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
+ fastapi_voyager-0.9.2.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
32
+ fastapi_voyager-0.9.2.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
33
+ fastapi_voyager-0.9.2.dist-info/RECORD,,