fastapi-voyager 0.12.3__py3-none-any.whl → 0.12.5__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.
fastapi_voyager/render.py CHANGED
@@ -93,6 +93,8 @@ class Renderer:
93
93
 
94
94
  def render_module_schema(mod: ModuleNode, inherit_color: str | None=None, show_cluster:bool=True) -> str:
95
95
  color: str | None = inherit_color
96
+ cluster_color: str | None = None
97
+
96
98
 
97
99
  # recursively vist module from short to long: 'a', 'a.b', 'a.b.c'
98
100
  # color_flag: {'a', 'a.b.c'}
@@ -103,6 +105,7 @@ class Renderer:
103
105
  if mod.fullname.startswith(k):
104
106
  module_color_flag.remove(k)
105
107
  color = self.module_color[k]
108
+ cluster_color = color if color != inherit_color else None
106
109
  break
107
110
 
108
111
  inner_nodes = [ render_node(node, color) for node in mod.schema_nodes ]
@@ -117,7 +120,7 @@ class Renderer:
117
120
  style="rounded"
118
121
  label = " {mod.name}"
119
122
  labeljust = "l"
120
- {(f'pencolor = "{color}"' if color else 'pencolor="#ccc"')}
123
+ {(f'pencolor = "{cluster_color}"' if cluster_color else 'pencolor="#ccc"')}
121
124
  {('penwidth = 3' if color else 'penwidth=""')}
122
125
  {inner_nodes_str}
123
126
  {child_str}
@@ -260,7 +260,7 @@ def update_forward_refs(kls):
260
260
 
261
261
  local_attrs = getattr(shelled_type, '__dict__', {})
262
262
  if local_attrs.get(const.PYDANTIC_FORWARD_REF_UPDATED, False):
263
- logger.debug(shelled_type.__qualname__, 'visited')
263
+ logger.debug("%s visited", shelled_type.__qualname__)
264
264
  continue
265
265
  if safe_issubclass(shelled_type, BaseModel):
266
266
  update_pydantic_forward_refs(shelled_type)
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.12.3"
2
+ __version__ = "0.12.5"
@@ -0,0 +1,17 @@
1
+ const { defineComponent, computed } = window.Vue;
2
+
3
+ import { store } from '../store.js'
4
+
5
+ export default defineComponent({
6
+ name: "Demo",
7
+ emits: ["close"],
8
+ setup() {
9
+ return { store };
10
+ },
11
+ template: `
12
+ <div>
13
+ <p>Count: {{ store.state.item.count }}</p>
14
+ <button @click="store.mutations.increment()">Add</button>
15
+ </div>
16
+ `
17
+ });
@@ -14,7 +14,6 @@ export default defineComponent({
14
14
  name: "SchemaFieldFilter",
15
15
  props: {
16
16
  schemaName: { type: String, default: null }, // external injection triggers auto-query
17
- // externally provided schemas dict (state.rawSchemasFull): { [id]: schema }
18
17
  schemas: { type: Object, default: () => ({}) },
19
18
  },
20
19
  emits: ["queried", "close"],
@@ -82,9 +82,7 @@ export class GraphUI {
82
82
  }
83
83
  const set = $();
84
84
  set.push(this);
85
- // const obj = { set, direction: "bidirectional" };
86
85
  const schemaName = event.currentTarget.dataset.name;
87
- // self.currentSelection = [obj];
88
86
  if (schemaName) {
89
87
  try {
90
88
  self.options.onSchemaClick(schemaName);
@@ -93,6 +91,17 @@ export class GraphUI {
93
91
  }
94
92
  }
95
93
  });
94
+
95
+ self.gv.edges().click(function (event) {
96
+ // const set = $();
97
+ // const downStreamNode = event.currentTarget.dataset.name.split("->")[1];
98
+ // const nodes = self.gv.nodesByName();
99
+ // set.push(nodes[downStreamNode]);
100
+ // const obj = { set, direction: "single" };
101
+ // self.currentSelection = [obj];
102
+ // todo highlight edge and downstream node
103
+ })
104
+
96
105
  self.gv.nodes().click(function (event) {
97
106
  const set = $();
98
107
  set.push(this);
@@ -100,7 +109,6 @@ export class GraphUI {
100
109
 
101
110
  const schemaName = event.currentTarget.dataset.name;
102
111
  if (event.shiftKey && self.options.onSchemaClick) {
103
- // try data-name or title text
104
112
  if (schemaName) {
105
113
  try {
106
114
  self.options.onSchemaShiftClick(schemaName);
@@ -123,20 +131,15 @@ export class GraphUI {
123
131
  const set = $();
124
132
  set.push(this);
125
133
  const obj = { set, direction: "single" };
126
- if (event.ctrlKey || event.metaKey || event.shiftKey) {
127
- self.currentSelection.push(obj);
128
- } else {
129
- self.currentSelection = [obj];
130
- }
134
+ self.currentSelection = [obj];
131
135
  self._highlight();
132
136
  });
133
137
 
134
- // svg 背景点击高亮清空
135
-
138
+ // click background to reset highlight
136
139
  $(document)
137
140
  .off("click.graphui")
138
141
  .on("click.graphui", function (evt) {
139
- // 如果点击目标不在 graph 容器内,直接退出
142
+ // if outside container, do nothing
140
143
  const graphContainer = $(self.selector)[0];
141
144
  if (
142
145
  !graphContainer ||
@@ -105,13 +105,13 @@
105
105
  >
106
106
  <q-icon class="q-mr-sm" name="satellite_alt"></q-icon>
107
107
  <span> FastAPI Voyager </span>
108
- <span v-if="state.version" style="font-size: 12px; margin-left: 8px; font-weight: normal;">{{ state.version }}</span>
108
+ <span v-if="store.state.version" style="font-size: 12px; margin-left: 8px; font-weight: normal;">{{ store.state.version }}</span>
109
109
  </div>
110
110
  <div class="col-auto" style="font-size: 16px">
111
111
  <q-option-group
112
112
  style="margin-left: 80px"
113
- v-model="state.showFields"
114
- :options="state.fieldOptions"
113
+ v-model="store.state.filter.showFields"
114
+ :options="store.state.fieldOptions"
115
115
  @update:model-value="(val) => toggleShowField(val)"
116
116
  color="primary"
117
117
  type="radio"
@@ -137,7 +137,7 @@
137
137
  flat
138
138
  label="Search"
139
139
  class="q-mr-md"
140
- @click="showDialog()"
140
+ @click="showSearchDialog()"
141
141
  />
142
142
  </div>
143
143
  <div class="col-auto">
@@ -176,8 +176,8 @@
176
176
  </q-header>
177
177
 
178
178
  <q-drawer
179
- v-model="state.detailDrawer"
180
- :width="state.drawerWidth"
179
+ v-model="store.state.rightDrawer.drawer"
180
+ :width="store.state.rightDrawer.width"
181
181
  side="right"
182
182
  style="border-left: 1px solid #888;"
183
183
  bordered
@@ -201,7 +201,7 @@
201
201
 
202
202
  <div style="z-index: 11; position: absolute; left: -17px; top: 9px">
203
203
  <q-btn
204
- @click="state.detailDrawer = !state.detailDrawer"
204
+ @click="store.state.rightDrawer.drawer = !store.state.rightDrawer.drawer"
205
205
  round
206
206
  unelevated
207
207
  color="primary"
@@ -210,14 +210,14 @@
210
210
  />
211
211
  </div>
212
212
  <schema-code-display
213
- :schema-name="schemaCodeName"
214
- :schemas="state.rawSchemasFull"
213
+ :schema-name="store.state.schemaDetail.schemaCodeName"
214
+ :schemas="store.state.graph.schemaMap"
215
215
  />
216
216
  </q-drawer>
217
217
 
218
218
  <q-page-container>
219
219
  <q-splitter
220
- v-model="state.splitter"
220
+ v-model="store.state.leftPanel.width"
221
221
  unit="px"
222
222
  :limits="[200, 800]"
223
223
  class="adjust-fit"
@@ -236,12 +236,12 @@
236
236
  <q-scroll-area class="fit">
237
237
  <q-list dense separator>
238
238
  <q-expansion-item
239
- v-for="tag in state.rawTags"
239
+ v-for="tag in store.state.leftPanel.tags"
240
240
  :key="tag.name"
241
241
  expand-separator
242
- :model-value="state._tag === tag.name"
242
+ :model-value="store.state.leftPanel._tag === tag.name"
243
243
  @update:model-value="(val) => toggleTag(tag.name, val)"
244
- :header-class="state.tag === tag.name ? 'text-primary text-bold' : ''"
244
+ :header-class="store.state.leftPanel.tag === tag.name ? 'text-primary text-bold' : ''"
245
245
  content-class="q-pa-none"
246
246
  >
247
247
  <template #header>
@@ -249,21 +249,21 @@
249
249
  <q-icon
250
250
  style="vertical-align: top;"
251
251
  class="q-mr-sm"
252
- :name="state.tag == tag.name ? 'folder' : 'folder_open'"
252
+ :name="store.state.leftPanel.tag == tag.name ? 'folder' : 'folder_open'"
253
253
  ></q-icon>
254
254
  <span>{{ tag.name }} <q-chip style="position:relative; top: -1px;" class="q-ml-md" dense>{{ tag.routes.length }}</q-chip></span>
255
- <a v-if="state._tag == tag.name" target="_blank" class="q-ml-sm" v-if="state.swaggerUrl" :href="state.swaggerUrl + '#/' + tag.name">
255
+ <a v-if="store.state.leftPanel._tag == tag.name" target="_blank" class="q-ml-sm" v-if="store.state.swagger.url" :href="store.state.swagger.url + '#/' + tag.name">
256
256
  <q-icon color="primary" size="" name="link" title="open in swagger"></q-icon>
257
257
  </a>
258
258
  </div>
259
259
  </template>
260
260
  <q-list separator style="overflow: auto; max-height: 60vh;">
261
261
  <q-item
262
- v-for="route in (state.hidePrimitiveRoute ? tag.routes.filter(r => !r.is_primitive) :tag.routes || [])"
262
+ v-for="route in (store.state.filter.hidePrimitiveRoute ? tag.routes.filter(r => !r.is_primitive) :tag.routes || [])"
263
263
  :key="route.id"
264
264
  clickable
265
265
  v-ripple
266
- :active="state.routeId === route.id"
266
+ :active="store.state.leftPanel.routeId === route.id"
267
267
  active-class=""
268
268
  @click="selectRoute(route.id)"
269
269
  >
@@ -274,7 +274,7 @@
274
274
  name="data_object"
275
275
  ></q-icon>
276
276
  {{ route.name }}
277
- <a v-if="state.routeId == route.id" target="_blank" class="q-ml-md" v-if="state.swaggerUrl" :href="state.swaggerUrl + '#/' + tag.name + '/' + route.unique_id">
277
+ <a v-if="store.state.leftPanel.routeId == route.id" target="_blank" class="q-ml-md" v-if="store.state.swagger.url" :href="store.state.swagger.url + '#/' + tag.name + '/' + route.unique_id">
278
278
  <q-icon color="primary" size="" name="link" title="open in swagger"></q-icon>
279
279
  </a>
280
280
  </span>
@@ -298,22 +298,12 @@
298
298
  <template #after>
299
299
  <div style="position: relative; width: 100%; height: 100%;">
300
300
  <div id="graph" class="adjust-fit"></div>
301
- <div style="position: absolute; left: 8px; bottom: 8px; z-index: 10; background: rgba(255,255,255,0.85); border-radius: 4px; padding: 2px 8px;">
301
+ <div style="position: absolute; left: 8px; top: 8px; z-index: 10; background: rgba(255,255,255,0.85); border-radius: 4px; padding: 2px 8px;">
302
302
  <div class="q-mt-sm">
303
303
  <q-toggle
304
- v-model="state.focus"
305
- v-show="schemaCodeName"
306
- @update:model-value="val => onFocusChange(val)"
307
- label="Focus"
308
- dense
309
- title="pick a schema and toggle focus on to display related nodes only"
310
- />
311
- </div>
312
- <div class="q-mt-sm">
313
- <q-toggle
314
- v-if="state.enableBriefMode"
304
+ v-if="store.state.modeControl.briefModeEnabled"
315
305
  dense
316
- v-model="state.brief"
306
+ v-model="store.state.filter.brief"
317
307
  label="Brief Mode"
318
308
  @update:model-value="(val) => toggleBrief(val)"
319
309
  title="skip middle classes, config module_prefix to enable it"
@@ -321,7 +311,7 @@
321
311
  </div>
322
312
  <div class="q-mt-sm">
323
313
  <q-toggle
324
- v-model="state.hidePrimitiveRoute"
314
+ v-model="store.state.filter.hidePrimitiveRoute"
325
315
  @update:model-value="(val) => toggleHidePrimitiveRoute(val)"
326
316
  label="Hide Primitive"
327
317
  dense
@@ -330,120 +320,53 @@
330
320
  </div>
331
321
  <div class="q-mt-sm">
332
322
  <q-toggle
333
- v-model="state.showModule"
323
+ v-model="store.state.filter.showModule"
334
324
  @update:model-value="(val) => toggleShowModule(val)"
335
325
  label="Show Module Cluster"
336
326
  dense
337
327
  title="show module cluster"
338
328
  />
339
329
  </div>
330
+ <div class="q-mt-sm">
331
+ <q-toggle
332
+ v-model="store.state.modeControl.focus"
333
+ v-show="store.state.schemaDetail.schemaCodeName"
334
+ @update:model-value="val => onFocusChange(val)"
335
+ label="Focus"
336
+ dense
337
+ title="pick a schema and toggle focus on to display related nodes only"
338
+ />
340
339
  </div>
341
340
  </div>
342
341
  </template>
343
342
  </q-splitter>
344
343
  </q-page-container>
345
344
  </q-layout>
346
- <!-- Detail Dialog -->
347
- <q-dialog v-model="showDetail" :persistent="true" :maximized="true">
348
- <detail-dialog
349
- :schema-name="schemaName"
350
- :show-fields="state.showFields"
351
- :model-value="showDetail"
352
- @close="closeDetail"
353
- />
354
- </q-dialog>
355
345
 
356
346
  <!-- Schema Field Filter Dialog -->
357
347
  <q-dialog
358
- v-model="showSchemaFieldFilter"
348
+ v-model="store.state.searchDialog.show"
359
349
  :persistent="true"
360
350
  :maximized="true"
361
351
  >
362
352
  <schema-field-filter
363
- :schemas="state.rawSchemasFull"
364
- :schema-name="schemaFieldFilterSchema"
365
- @close="showSchemaFieldFilter = false"
353
+ :schemas="store.state.graph.schemaMap"
354
+ :schema-name="store.state.searchDialog.schema"
355
+ @close="store.state.searchDialog.show = false"
366
356
  />
367
357
  </q-dialog>
368
358
 
369
- <q-dialog v-model="showRouteDetail" seamless position="bottom">
359
+ <q-dialog v-model="store.state.routeDetail.show" seamless position="bottom">
370
360
  <q-card style="width: 1100px; max-width: 1100px; max-height: 40vh">
371
361
  <route-code-display
372
- :route-id="routeCodeId"
373
- @close="showRouteDetail=false"
362
+ :route-id="store.state.routeDetail.routeCodeId"
363
+ @close="store.state.routeDetail.show = false"
374
364
  />
375
365
  </q-card>
376
366
  </q-dialog>
377
367
 
378
- <!-- Dump Core Data Dialog -->
379
- <q-dialog v-model="showDumpDialog" :maximized="true" :persistent="false">
380
- <div style="height: 100%; position: relative; background: #fff">
381
- <q-btn
382
- flat
383
- dense
384
- round
385
- icon="content_copy"
386
- aria-label="Copy"
387
- @click="copyDumpJson"
388
- style="
389
- position: absolute;
390
- top: 6px;
391
- right: 62px;
392
- z-index: 11;
393
- background: rgba(255, 255, 255, 0.85);
394
- "
395
- ></q-btn>
396
- <q-btn
397
- flat
398
- dense
399
- round
400
- icon="close"
401
- aria-label="Close"
402
- @click="showDumpDialog = false"
403
- style="
404
- position: absolute;
405
- top: 6px;
406
- right: 6px;
407
- z-index: 11;
408
- background: rgba(255, 255, 255, 0.85);
409
- "
410
- ></q-btn>
411
- <div>
412
- <pre
413
- style="padding: 20px; overflow: auto"
414
- ><code>{{ dumpJson }}</code></pre>
415
- </div>
416
- </div>
417
- </q-dialog>
418
-
419
- <!-- Import Core Data Dialog -->
420
- <q-dialog v-model="showImportDialog" :persistent="true">
421
- <q-card style="min-width: 70vw; max-width: 90vw">
422
- <q-card-section class="text-h6">Import core data JSON</q-card-section>
423
- <q-card-section>
424
- <q-btn color="primary" label="Render" @click="onImportConfirm" />
425
- </q-card-section>
426
- <q-card-section>
427
- <q-input
428
- v-model="importJsonText"
429
- type="textarea"
430
- autogrow
431
- filled
432
- label="Paste JSON here"
433
- />
434
- </q-card-section>
435
- </q-card>
436
- </q-dialog>
437
-
438
- <!-- Render Graph Dialog (from imported core data) -->
439
- <q-dialog v-model="showRenderGraph" :maximized="true" :persistent="false">
440
- <render-graph
441
- :core-data="renderCoreData"
442
- @close="showRenderGraph = false"
443
- />
444
- </q-dialog>
445
368
  </div>
446
- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
369
+ <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
447
370
  <script src="fastapi-voyager-static/quasar.min.js"></script>
448
371
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js" integrity="sha512-egJ/Y+22P9NQ9aIyVCh0VCOsfydyn8eNmqBy+y2CnJG+fpRIxXMS6jbWP8tVKp0jp+NO5n8WtMUAnNnGoJKi4w==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
449
372
  <script
@@ -0,0 +1,101 @@
1
+ const { reactive } = window.Vue;
2
+
3
+ const state = reactive({
4
+ item: {
5
+ count: 0
6
+ },
7
+
8
+ version: '',
9
+
10
+ swagger: {
11
+ url: ''
12
+ },
13
+
14
+ rightDrawer: {
15
+ drawer: false,
16
+ width: 300
17
+ },
18
+
19
+ fieldOptions: [
20
+ { label: "No field", value: "single" },
21
+ { label: "Object fields", value: "object" },
22
+ { label: "All fields", value: "all" },
23
+ ],
24
+
25
+ // tags and routes
26
+ leftPanel: {
27
+ width: 300,
28
+ tags: null,
29
+ tag: null,
30
+ _tag: null,
31
+ routeId: null,
32
+ },
33
+
34
+ graph: {
35
+ schemaId: null,
36
+ schemaKeys: new Set(),
37
+ schemaMap: {},
38
+ routeItems: []
39
+ },
40
+
41
+ leftPanelFiltered: {
42
+
43
+ },
44
+
45
+ // schema options, schema, fields
46
+ search: {
47
+
48
+ },
49
+
50
+
51
+ // route information
52
+ routeDetail: {
53
+ show: false,
54
+ routeCodeId: ''
55
+ },
56
+
57
+ // schema information
58
+ schemaDetail: {
59
+ show: false,
60
+ schemaCodeName: '',
61
+ },
62
+
63
+ searchDialog: {
64
+ show: false,
65
+ schema: null
66
+ },
67
+
68
+ // global status
69
+ status: {
70
+ generating: false,
71
+ loading: false,
72
+ initializing: true,
73
+ },
74
+
75
+ // brief, hide primitive ...
76
+ modeControl: {
77
+ focus: false, // control the schema param
78
+ briefModeEnabled: false, // show brief mode toggle
79
+ },
80
+
81
+ // api filters
82
+ filter: {
83
+ hidePrimitiveRoute: false,
84
+ showFields: 'object',
85
+ brief: false,
86
+ showModule: true,
87
+ }
88
+
89
+ })
90
+
91
+ const mutations = {
92
+ increment() {
93
+ state.item.count += 1
94
+ }
95
+ }
96
+
97
+
98
+ export const store = {
99
+ state,
100
+ mutations
101
+ }
@@ -1,64 +1,17 @@
1
1
  import SchemaFieldFilter from "./component/schema-field-filter.js";
2
2
  import SchemaCodeDisplay from "./component/schema-code-display.js";
3
3
  import RouteCodeDisplay from "./component/route-code-display.js";
4
+ import Demo from './component/demo.js'
4
5
  import RenderGraph from "./component/render-graph.js";
5
6
  import { GraphUI } from "./graph-ui.js";
6
- const { createApp, reactive, onMounted, watch, ref } = window.Vue;
7
+ import { store } from './store.js'
8
+
9
+ const { createApp, onMounted, ref } = window.Vue;
7
10
 
8
11
  const app = createApp({
9
12
  setup() {
10
- const state = reactive({
11
- // options and selections
12
- tag: null, // picked tag
13
- _tag: null, // display tag
14
- routeId: null, // picked route
15
- schemaId: null, // picked schema
16
- showFields: "object",
17
- fieldOptions: [
18
- { label: "No field", value: "single" },
19
- { label: "Object fields", value: "object" },
20
- { label: "All fields", value: "all" },
21
- ],
22
- enableBriefMode: false,
23
- brief: false,
24
- focus: false,
25
- hidePrimitiveRoute: false,
26
- generating: false,
27
- swaggerUrl: null,
28
- rawTags: [], // [{ name, routes: [{ id, name }] }]
29
- rawSchemas: new Set(), // [{ name, id }]
30
- rawSchemasFull: {}, // full schemas dict: { [schema.id]: schema }
31
- initializing: true,
32
- // Splitter size (left panel width in px)
33
- splitter: 300,
34
- detailDrawer: false,
35
- drawerWidth: 300, // drawer 宽度
36
- version: "", // version from backend
37
- showModule: true,
38
- });
39
-
40
- const showDetail = ref(false);
41
- const showSchemaFieldFilter = ref(false);
42
- const showDumpDialog = ref(false);
43
- const dumpJson = ref("");
44
- const showImportDialog = ref(false);
45
- const importJsonText = ref("");
46
- const showRenderGraph = ref(false);
47
- const renderCoreData = ref(null);
48
- const schemaName = ref(""); // used by detail dialog
49
- const schemaFieldFilterSchema = ref(null); // external schemaName for schema-field-filter
50
- const schemaCodeName = ref("");
51
- const routeCodeId = ref("");
52
- const showRouteDetail = ref(false);
53
13
  let graphUI = null;
54
14
 
55
- function openDetail() {
56
- showDetail.value = true;
57
- }
58
- function closeDetail() {
59
- showDetail.value = false;
60
- }
61
-
62
15
  function readQuerySelection() {
63
16
  if (typeof window === "undefined") {
64
17
  return { tag: null, route: null };
@@ -72,7 +25,7 @@ const app = createApp({
72
25
 
73
26
  function findTagByRoute(routeId) {
74
27
  return (
75
- state.rawTags.find((tag) =>
28
+ store.state.leftPanel.tags.find((tag) =>
76
29
  (tag.routes || []).some((route) => route.id === routeId)
77
30
  )?.name || null
78
31
  );
@@ -83,13 +36,13 @@ const app = createApp({
83
36
  return;
84
37
  }
85
38
  const params = new URLSearchParams(window.location.search);
86
- if (state.tag) {
87
- params.set("tag", state.tag);
39
+ if (store.state.leftPanel.tag) {
40
+ params.set("tag", store.state.leftPanel.tag);
88
41
  } else {
89
42
  params.delete("tag");
90
43
  }
91
- if (state.routeId) {
92
- params.set("route", state.routeId);
44
+ if (store.state.leftPanel.routeId) {
45
+ params.set("route", store.state.leftPanel.routeId);
93
46
  } else {
94
47
  params.delete("route");
95
48
  }
@@ -102,45 +55,47 @@ const app = createApp({
102
55
 
103
56
  function applySelectionFromQuery(selection) {
104
57
  let applied = false;
105
- if (selection.tag && state.rawTags.some((tag) => tag.name === selection.tag)) {
106
- state.tag = selection.tag;
107
- state._tag = selection.tag;
58
+ if (selection.tag && store.state.leftPanel.tags.some((tag) => tag.name === selection.tag)) {
59
+ store.state.leftPanel.tag = selection.tag;
60
+ store.state.leftPanel._tag = selection.tag;
108
61
  applied = true;
109
62
  }
110
- if (selection.route && state.routeItems?.[selection.route]) {
111
- state.routeId = selection.route;
63
+ if (selection.route && store.state.graph.routeItems?.[selection.route]) {
64
+ store.state.leftPanel.routeId = selection.route;
112
65
  applied = true;
113
66
  const inferredTag = findTagByRoute(selection.route);
114
67
  if (inferredTag) {
115
- state.tag = inferredTag;
116
- state._tag = inferredTag;
68
+ store.state.leftPanel.tag = inferredTag;
69
+ store.state.leftPanel._tag = inferredTag;
117
70
  }
118
71
  }
119
72
  return applied;
120
73
  }
121
74
 
122
75
  async function loadInitial() {
123
- state.initializing = true;
76
+ store.state.initializing = true;
124
77
  try {
125
78
  const res = await fetch("dot");
126
79
  const data = await res.json();
127
- state.rawTags = Array.isArray(data.tags) ? data.tags : [];
80
+ store.state.leftPanel.tags = Array.isArray(data.tags) ? data.tags : [];
81
+
128
82
  const schemasArr = Array.isArray(data.schemas) ? data.schemas : [];
129
83
  // Build dict keyed by id for faster lookups and simpler prop passing
130
- state.rawSchemasFull = Object.fromEntries(
84
+ const schemaMap = Object.fromEntries(
131
85
  schemasArr.map((s) => [s.id, s])
132
86
  );
133
- state.rawSchemas = new Set(Object.keys(state.rawSchemasFull));
134
- state.routeItems = data.tags
87
+ store.state.graph.schemaMap = schemaMap
88
+ store.state.graph.schemaKeys = new Set(Object.keys(schemaMap));
89
+ store.state.graph.routeItems = data.tags
135
90
  .map((t) => t.routes)
136
91
  .flat()
137
92
  .reduce((acc, r) => {
138
93
  acc[r.id] = r;
139
94
  return acc;
140
95
  }, {});
141
- state.enableBriefMode = data.enable_brief_mode || false;
142
- state.version = data.version || "";
143
- state.swaggerUrl = data.swagger_url || null
96
+ store.state.modeControl.briefModeEnabled = data.enable_brief_mode || false;
97
+ store.state.version = data.version || "";
98
+ store.state.swagger.url = data.swagger_url || null
144
99
 
145
100
  const querySelection = readQuerySelection();
146
101
  const restoredFromQuery = applySelectionFromQuery(querySelection);
@@ -157,8 +112,8 @@ const app = createApp({
157
112
  case "empty":
158
113
  return
159
114
  case "first":
160
- state.tag = state.rawTags.length > 0 ? state.rawTags[0].name : null;
161
- state._tag = state.tag;
115
+ store.state.leftPanel.tag = store.state.leftPanel.tags.length > 0 ? store.state.leftPanel.tags[0].name : null;
116
+ store.state.leftPanel._tag = store.state.leftPanel.tag;
162
117
  onGenerate();
163
118
  return
164
119
  }
@@ -167,7 +122,7 @@ const app = createApp({
167
122
  } catch (e) {
168
123
  console.error("Initial load failed", e);
169
124
  } finally {
170
- state.initializing = false;
125
+ store.state.initializing = false;
171
126
  }
172
127
  }
173
128
 
@@ -177,24 +132,24 @@ const app = createApp({
177
132
  } else {
178
133
  await onGenerate(false);
179
134
  setTimeout(() => {
180
- const ele = $(`[data-name='${schemaCodeName.value}'] polygon`);
135
+ const ele = $(`[data-name='${store.state.schemaDetail.schemaCodeName}'] polygon`);
181
136
  ele.dblclick();
182
137
  }, 1);
183
138
  }
184
139
  }
185
140
 
186
141
  async function onGenerate(resetZoom = true) {
187
- const schema_name = state.focus ? schemaCodeName.value : null;
188
- state.generating = true;
142
+ const schema_name = store.state.modeControl.focus ? store.state.schemaDetail.schemaCodeName : null;
143
+ store.state.generating = true;
189
144
  try {
190
145
  const payload = {
191
- tags: state.tag ? [state.tag] : null,
146
+ tags: store.state.leftPanel.tag ? [store.state.leftPanel.tag] : null,
192
147
  schema_name: schema_name || null,
193
- route_name: state.routeId || null,
194
- show_fields: state.showFields,
195
- brief: state.brief,
196
- hide_primitive_route: state.hidePrimitiveRoute,
197
- show_module: state.showModule,
148
+ route_name: store.state.leftPanel.routeId || null,
149
+ show_fields: store.state.filter.showFields,
150
+ brief: store.state.modeControl.brief,
151
+ hide_primitive_route: store.state.filter.hidePrimitiveRoute,
152
+ show_module: store.state.filter.showModule,
198
153
  };
199
154
  const res = await fetch("dot", {
200
155
  method: "POST",
@@ -207,21 +162,21 @@ const app = createApp({
207
162
  if (!graphUI) {
208
163
  graphUI = new GraphUI("#graph", {
209
164
  onSchemaShiftClick: (id) => {
210
- if (state.rawSchemas.has(id)) {
165
+ if (store.state.graph.schemaKeys.has(id)) {
211
166
  resetDetailPanels();
212
- schemaFieldFilterSchema.value = id;
213
- showSchemaFieldFilter.value = true;
167
+ store.state.searchDialog.show = true;
168
+ store.state.searchDialog.schema = id;
214
169
  }
215
170
  },
216
171
  onSchemaClick: (id) => {
217
172
  resetDetailPanels();
218
- if (state.rawSchemas.has(id)) {
219
- schemaCodeName.value = id;
220
- state.detailDrawer = true;
173
+ if (store.state.graph.schemaKeys.has(id)) {
174
+ store.state.schemaDetail.schemaCodeName = id;
175
+ store.state.rightDrawer.drawer = true;
221
176
  }
222
- if (id in state.routeItems) {
223
- routeCodeId.value = id;
224
- showRouteDetail.value = true;
177
+ if (id in store.state.graph.routeItems) {
178
+ store.state.routeDetail.routeCodeId = id;
179
+ store.state.routeDetail.show = true;
225
180
  }
226
181
  },
227
182
  resetCb: () => {
@@ -233,149 +188,95 @@ const app = createApp({
233
188
  } catch (e) {
234
189
  console.error("Generate failed", e);
235
190
  } finally {
236
- state.generating = false;
191
+ store.state.generating = false;
237
192
  }
238
193
  }
239
194
 
240
- async function onDumpData() {
241
- try {
242
- const payload = {
243
- tags: state.tag ? [state.tag] : null,
244
- schema_name: state.schemaId || null,
245
- route_name: state.routeId || null,
246
- show_fields: state.showFields,
247
- brief: state.brief,
248
- };
249
- const res = await fetch("dot-core-data", {
250
- method: "POST",
251
- headers: { "Content-Type": "application/json" },
252
- body: JSON.stringify(payload),
253
- });
254
- const json = await res.json();
255
- dumpJson.value = JSON.stringify(json, null, 2);
256
- showDumpDialog.value = true;
257
- } catch (e) {
258
- console.error("Dump data failed", e);
259
- }
260
- }
261
-
262
- async function copyDumpJson() {
263
- try {
264
- await navigator.clipboard.writeText(dumpJson.value || "");
265
- if (window.Quasar?.Notify) {
266
- window.Quasar.Notify.create({ type: "positive", message: "Copied" });
267
- }
268
- } catch (e) {
269
- console.error("Copy failed", e);
270
- }
271
- }
272
-
273
- function openImportDialog() {
274
- importJsonText.value = "";
275
- showImportDialog.value = true;
276
- }
277
-
278
- async function onImportConfirm() {
279
- let payloadObj = null;
280
- try {
281
- payloadObj = JSON.parse(importJsonText.value || "{}");
282
- } catch (e) {
283
- if (window.Quasar?.Notify) {
284
- window.Quasar.Notify.create({
285
- type: "negative",
286
- message: "Invalid JSON",
287
- });
288
- }
289
- return;
290
- }
291
- // Move the request into RenderGraph component: pass the parsed object and let the component call /dot-render-core-data
292
- renderCoreData.value = payloadObj;
293
- showRenderGraph.value = true;
294
- showImportDialog.value = false;
295
- }
296
-
297
- function showDialog() {
298
- schemaFieldFilterSchema.value = null;
299
- showSchemaFieldFilter.value = true;
195
+ function showSearchDialog() {
196
+ store.state.searchDialog.show = true;
197
+ store.state.searchDialog.schema = null;
300
198
  }
301
199
 
302
200
  function resetDetailPanels() {
303
- state.detailDrawer = false;
304
- showRouteDetail.value = false;
305
- schemaCodeName.value = "";
201
+ store.state.rightDrawer.drawer = false;
202
+ store.state.routeDetail.show = false
203
+ store.state.schemaDetail.schemaCodeName = "";
306
204
  }
307
205
 
308
206
  async function onReset() {
309
- state.tag = null;
310
- state._tag = null;
311
- state.routeId = "";
312
- state.schemaId = null;
207
+ store.state.leftPanel.tag = null;
208
+ store.state.leftPanel._tag = null;
209
+ store.state.leftPanel.routeId = null;
210
+
211
+ store.state.graph.schemaId = null;
212
+
313
213
  // state.showFields = "object";
314
- state.focus = false;
315
- schemaCodeName.value = "";
214
+ store.state.modeControl.focus = false;
215
+ store.state.schemaDetail.schemaCodeName = "";
316
216
  onGenerate();
317
217
  syncSelectionToUrl();
318
218
  }
319
219
 
320
220
  function toggleTag(tagName, expanded = null) {
321
221
  if (expanded === true) {
322
- state._tag = tagName;
323
- state.tag = tagName;
324
- state.routeId = "";
325
- state.focus = false;
326
- schemaCodeName.value = "";
222
+ store.state.leftPanel._tag = tagName;
223
+ store.state.leftPanel.tag = tagName;
224
+ store.state.leftPanel.routeId = "";
225
+
226
+ store.state.modeControl.focus = false;
227
+ store.state.schemaDetail.schemaCodeName = "";
327
228
  onGenerate();
328
229
  } else {
329
- state._tag = null;
230
+ store.state.leftPanel._tag = null;
330
231
  }
331
232
 
332
- state.detailDrawer = false;
333
- showRouteDetail.value = false;
233
+ store.state.rightDrawer.drawer = false;
234
+ store.state.routeDetail.show = false
334
235
  syncSelectionToUrl();
335
236
  }
336
237
 
337
238
  function selectRoute(routeId) {
338
- if (state.routeId === routeId) {
339
- state.routeId = "";
239
+ if (store.state.leftPanel.routeId === routeId) {
240
+ store.state.leftPanel.routeId = "";
340
241
  } else {
341
- state.routeId = routeId;
242
+ store.state.leftPanel.routeId = routeId;
342
243
  }
343
- state.detailDrawer = false;
344
- showRouteDetail.value = false;
345
- state.focus = false;
346
- schemaCodeName.value = "";
244
+ store.state.rightDrawer.drawer = false;
245
+ store.state.routeDetail.show = false
246
+ store.state.modeControl.focus = false;
247
+ store.state.schemaDetail.schemaCodeName = "";
347
248
  onGenerate();
348
249
  syncSelectionToUrl();
349
250
  }
350
251
 
351
252
  function toggleShowModule(val) {
352
- state.showModule = val;
253
+ store.state.filter.showModule = val;
353
254
  onGenerate()
354
255
  }
355
256
 
356
257
  function toggleShowField(field) {
357
- state.showFields = field;
258
+ store.state.filter.showFields = field;
358
259
  onGenerate(false);
359
260
  }
360
261
 
361
262
  function toggleBrief(val) {
362
- state.brief = val;
263
+ store.state.modeControl.brief = val;
363
264
  onGenerate();
364
265
  }
365
266
 
366
267
  function toggleHidePrimitiveRoute(val) {
367
- state.hidePrimitiveRoute = val;
268
+ store.state.filter.hidePrimitiveRoute = val;
368
269
  onGenerate(false);
369
270
  }
370
271
 
371
272
  function startDragDrawer(e) {
372
273
  const startX = e.clientX;
373
- const startWidth = state.drawerWidth;
274
+ const startWidth = store.state.rightDrawer.width;
374
275
 
375
276
  function onMouseMove(moveEvent) {
376
277
  const deltaX = startX - moveEvent.clientX;
377
278
  const newWidth = Math.max(300, Math.min(800, startWidth + deltaX));
378
- state.drawerWidth = newWidth;
279
+ store.state.rightDrawer.width = newWidth;
379
280
  }
380
281
 
381
282
  function onMouseUp() {
@@ -399,49 +300,33 @@ const app = createApp({
399
300
  });
400
301
 
401
302
  return {
402
- state,
303
+ store,
403
304
  toggleTag,
404
305
  toggleBrief,
405
306
  toggleHidePrimitiveRoute,
406
307
  selectRoute,
407
308
  onGenerate,
408
309
  onReset,
409
- showDetail,
410
- showRouteDetail,
411
- openDetail,
412
- closeDetail,
413
- schemaName,
414
- showSchemaFieldFilter,
415
- schemaFieldFilterSchema,
416
- showDialog,
417
- schemaCodeName,
418
- routeCodeId,
419
- // dump/import
420
- showDumpDialog,
421
- dumpJson,
422
- copyDumpJson,
423
- onDumpData,
424
- showImportDialog,
425
- importJsonText,
426
- openImportDialog,
427
- onImportConfirm,
428
- // render graph dialog
429
- showRenderGraph,
430
- renderCoreData,
310
+ showSearchDialog,
431
311
  toggleShowField,
432
312
  startDragDrawer,
433
313
  onFocusChange,
434
- toggleShowModule
314
+ toggleShowModule,
435
315
  };
436
316
  },
437
317
  });
318
+
438
319
  app.use(window.Quasar);
320
+
439
321
  // Set Quasar primary theme color to green
440
322
  if (window.Quasar && typeof window.Quasar.setCssVar === "function") {
441
323
  window.Quasar.setCssVar("primary", "#009485");
442
324
  }
443
- app.component("schema-field-filter", SchemaFieldFilter);
444
- app.component("schema-code-display", SchemaCodeDisplay);
445
- app.component("route-code-display", RouteCodeDisplay);
446
- app.component("render-graph", RenderGraph);
325
+
326
+ app.component("schema-field-filter", SchemaFieldFilter); // shift click and see relationships
327
+ app.component("schema-code-display", SchemaCodeDisplay); // double click to see node details
328
+ app.component("route-code-display", RouteCodeDisplay); // double click to see route details
329
+ app.component("render-graph", RenderGraph); // for debug, render pasted dot content
330
+ app.component('demo-component', Demo)
331
+
447
332
  app.mount("#q-app");
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.12.3
3
+ Version: 0.12.5
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
@@ -31,14 +31,31 @@ Description-Content-Type: text/markdown
31
31
  [![PyPI Downloads](https://static.pepy.tech/badge/fastapi-voyager/month)](https://pepy.tech/projects/fastapi-voyager)
32
32
 
33
33
 
34
- > This repo is still in early stage, it supports pydantic v2 only
35
34
 
36
35
  Visualize your FastAPI endpoints, and explore them interactively.
37
36
 
38
- [visit online demo](https://www.newsyeah.fun/voyager/) of project: [composition oriented development pattern](https://github.com/allmonday/composition-oriented-development-pattern)
37
+ > This repo is still in early stage, it supports pydantic v2 only
38
+
39
+ [live demo](https://www.newsyeah.fun/voyager/) of project: [composition oriented development pattern](https://github.com/allmonday/composition-oriented-development-pattern)
39
40
 
40
41
  <img width="1600" height="986" alt="image" src="https://github.com/user-attachments/assets/8829cda0-f42d-4c84-be2f-b019bb5fe7e1" />
41
42
 
43
+ with configuration:
44
+
45
+ ```python
46
+ app.mount('/voyager',
47
+ create_voyager(
48
+ app,
49
+ module_color={'src.services': 'tomato'},
50
+ module_prefix='src.services',
51
+ swagger_url="/docs",
52
+ ga_id="G-R64S7Q49VL",
53
+ initial_page_policy='first',
54
+ online_repo_url='https://github.com/allmonday/composition-oriented-development-pattern/blob/master'))
55
+ ```
56
+
57
+ https://github.com/allmonday/composition-oriented-development-pattern/blob/master/src/main.py#L48
58
+
42
59
  ## Plan & Raodmap
43
60
  - [ideas](./docs/idea.md)
44
61
  - [changelog & roadmap](./docs/changelog.md)
@@ -2,23 +2,25 @@ fastapi_voyager/__init__.py,sha256=kqwzThE1YhmQ_7jPKGlnWvqRGC-hFrRqq_lKhKaleYU,2
2
2
  fastapi_voyager/cli.py,sha256=td3yIIigEomhSdDO-Xkh-CgpEwCafwlwnpvxnT9QsBo,10488
3
3
  fastapi_voyager/filter.py,sha256=AN_HIu8-DtKisIq5mFt7CnqRHtxKewedNGyyaI82hSY,11529
4
4
  fastapi_voyager/module.py,sha256=h9YR3BpS-CAcJW9WCdVkF4opqwY32w9T67g9GfdLytk,3425
5
- fastapi_voyager/render.py,sha256=1S9GFQ4LnNC_Qd-yiM8Jw8FkTxt2huREppc2sO0dFxA,9820
5
+ fastapi_voyager/render.py,sha256=7jSChISqTV2agaO55thhwyrOhqVOMux4x7k8rSQROnU,9960
6
6
  fastapi_voyager/server.py,sha256=MZNRpcXor2q8Rj3OSf6EH8NkgDChxfzUtnIY8ilRkaY,7053
7
7
  fastapi_voyager/type.py,sha256=7EL1zaIwKVRGpLig7fqaOrZGN5k0Rm31C9COfck3CSs,1750
8
- fastapi_voyager/type_helper.py,sha256=quQPV0dbb5JwpfpC5tL2Zad73f_fJXF2-k46ZMuXrZs,9716
9
- fastapi_voyager/version.py,sha256=TeMwxEaDOniKVMHjL1Q1lrmHL9cdAEzO838ewxqUUAU,49
8
+ fastapi_voyager/type_helper.py,sha256=JXD_OE_xTARkGjWDsnO_xfvyZ0vcwViYyqCp6oEHBTM,9719
9
+ fastapi_voyager/version.py,sha256=0gsRRMzt46OBC-7IuF0VTu6UPwKs_X7qmw7KMjDhIeo,49
10
10
  fastapi_voyager/voyager.py,sha256=LiRUb0ZG2cfnyY_pwRqoeZjxb6Pu6xy_lqPiMupxoKM,13510
11
- fastapi_voyager/web/graph-ui.js,sha256=9ONPxQHvk4HxYq6KtKc_2VbJmUgd-gh7i3Biv1rkqC4,5734
11
+ fastapi_voyager/web/graph-ui.js,sha256=NiwUFHCZPE4C-Hx4qwFvHwPyXw_lu2ar3WnPkmVGQN0,5850
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=8cmlwQzE5tonHj_QozdKcr3z-7JFsvE7cjf70dye0y8,19377
14
+ fastapi_voyager/web/index.html,sha256=3HixCgVF9VtTx89G3usDXpEh7NZnLk_p9siz4OUVUHE,17444
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=m6U24ythzjQuAYXUm9BsTdFrApFNqW26B0Bf7TsybqQ,13275
17
+ fastapi_voyager/web/store.js,sha256=fW-3uUwWNUsW8vXbTqltHvvlIroBeZTJvFkMSjuWdVg,1630
18
+ fastapi_voyager/web/vue-main.js,sha256=H50LbB-g3jWaz3Xv2O8RgajlibjtoiHu1HDTumjf7sY,10766
19
+ fastapi_voyager/web/component/demo.js,sha256=bQb16Un4XZ3Mf8qL6gvyrXe_mmA3V3mSIRMQAWg2MNk,352
18
20
  fastapi_voyager/web/component/render-graph.js,sha256=e8Xgh2Kl-nYU0P1gstEmAepCgFnk2J6UdxW8TlMafGs,2322
19
21
  fastapi_voyager/web/component/route-code-display.js,sha256=8NJPPjNRUC21gjpY8XYEQs4RBbhX1pCiqEhJp39ku6k,3678
20
22
  fastapi_voyager/web/component/schema-code-display.js,sha256=qKUMV2RFQzR8deof2iC4vyp65UaWadtVsDAXjY-i3vE,7042
21
- fastapi_voyager/web/component/schema-field-filter.js,sha256=c--XiXJrhIS7sYo1x8ZwMoqak0k9xLkNYTWoli-zd38,6253
23
+ fastapi_voyager/web/component/schema-field-filter.js,sha256=i6Zp02wfSBWQb4AJ7H_Q9vgCVzLaVnH1I2JIot0sV-0,6172
22
24
  fastapi_voyager/web/icon/android-chrome-192x192.png,sha256=35sBy6jmUFJCcquStaafHH1qClZIbd-X3PIKSeLkrNo,37285
23
25
  fastapi_voyager/web/icon/android-chrome-512x512.png,sha256=eb2eDjCwIruc05029_0L9hcrkVkv8KceLn1DJMYU0zY,210789
24
26
  fastapi_voyager/web/icon/apple-touch-icon.png,sha256=gnWK46tPnvSw1-oYZjgI02wpoO4OrIzsVzGHC5oKWO0,33187
@@ -26,8 +28,8 @@ fastapi_voyager/web/icon/favicon-16x16.png,sha256=JC07jEzfIYxBIoQn_FHXvyHuxESdhW
26
28
  fastapi_voyager/web/icon/favicon-32x32.png,sha256=C7v1h58cfWOsiLp9yOIZtlx-dLasBcq3NqpHVGRmpt4,1859
27
29
  fastapi_voyager/web/icon/favicon.ico,sha256=tZolYIXkkBcFiYl1A8ksaXN2VjGamzcSdes838dLvNc,15406
28
30
  fastapi_voyager/web/icon/site.webmanifest,sha256=ep4Hzh9zhmiZF2At3Fp1dQrYQuYF_3ZPZxc1KcGBvwQ,263
29
- fastapi_voyager-0.12.3.dist-info/METADATA,sha256=zCZqZOPMmQMh0guXA7dmVHP0QD1M8ZJKFT6dyR_hWRo,6009
30
- fastapi_voyager-0.12.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
- fastapi_voyager-0.12.3.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
32
- fastapi_voyager-0.12.3.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
33
- fastapi_voyager-0.12.3.dist-info/RECORD,,
31
+ fastapi_voyager-0.12.5.dist-info/METADATA,sha256=QO3zauma0JaAR_DyhX5yQqNkDf868_2cAw1mHHKY3o0,6523
32
+ fastapi_voyager-0.12.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
33
+ fastapi_voyager-0.12.5.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
34
+ fastapi_voyager-0.12.5.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
35
+ fastapi_voyager-0.12.5.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any