fastapi-voyager 0.15.2__py3-none-any.whl → 0.15.4__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/server.py CHANGED
@@ -19,6 +19,7 @@ WEB_DIR = Path(__file__).parent / "web"
19
19
  WEB_DIR.mkdir(exist_ok=True)
20
20
 
21
21
  GA_PLACEHOLDER = "<!-- GA_SNIPPET -->"
22
+ VERSION_PLACEHOLDER = "<!-- VERSION_PLACEHOLDER -->"
22
23
 
23
24
  def _build_ga_snippet(ga_id: str | None) -> str:
24
25
  if not ga_id:
@@ -197,7 +198,9 @@ def create_voyager(
197
198
  index_file = WEB_DIR / "index.html"
198
199
  if index_file.exists():
199
200
  content = index_file.read_text(encoding="utf-8")
200
- return content.replace(GA_PLACEHOLDER, _build_ga_snippet(ga_id))
201
+ content = content.replace(GA_PLACEHOLDER, _build_ga_snippet(ga_id))
202
+ content = content.replace(VERSION_PLACEHOLDER, f"?v={__version__}")
203
+ return content
201
204
  # fallback simple page if index.html missing
202
205
  return """
203
206
  <!doctype html>
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.15.2"
2
+ __version__ = "0.15.4"
@@ -69,10 +69,45 @@ export class GraphUI {
69
69
  }
70
70
 
71
71
  highlightSchemaBanner(node) {
72
+ // Get all polygons in the node
72
73
  const polygons = node.querySelectorAll("polygon")
73
- const ele = polygons[2] // select the second polygon
74
- if (ele) {
75
- ele.setAttribute("stroke-width", "3.5")
74
+
75
+ // The first polygon is typically the outer frame of the entire node
76
+ const outerFrame = polygons[0]
77
+ // The second polygon is typically the title background
78
+ const titleBg = polygons[1]
79
+
80
+ if (outerFrame) {
81
+ // Save original attributes for potential restoration
82
+ if (!outerFrame.hasAttribute("data-original-stroke")) {
83
+ outerFrame.setAttribute("data-original-stroke", outerFrame.getAttribute("stroke") || "")
84
+ outerFrame.setAttribute(
85
+ "data-original-stroke-width",
86
+ outerFrame.getAttribute("stroke-width") || "1"
87
+ )
88
+ outerFrame.setAttribute("data-original-fill", outerFrame.getAttribute("fill") || "")
89
+ }
90
+
91
+ // Apply bold purple border to the outer frame
92
+ outerFrame.setAttribute("stroke", "#822dba")
93
+ outerFrame.setAttribute("stroke-width", "3.0")
94
+ }
95
+
96
+ if (titleBg) {
97
+ // Save original attributes
98
+ if (!titleBg.hasAttribute("data-original-stroke")) {
99
+ titleBg.setAttribute("data-original-stroke", titleBg.getAttribute("stroke") || "")
100
+ titleBg.setAttribute(
101
+ "data-original-stroke-width",
102
+ titleBg.getAttribute("stroke-width") || "1"
103
+ )
104
+ titleBg.setAttribute("data-original-fill", titleBg.getAttribute("fill") || "")
105
+ }
106
+
107
+ // Apply purple background to title
108
+ titleBg.setAttribute("fill", "#822dba")
109
+ // Also update the stroke to match
110
+ titleBg.setAttribute("stroke", "#822dba")
76
111
  }
77
112
  }
78
113
 
@@ -2,8 +2,14 @@
2
2
  <head>
3
3
  <title>FastAPI Voyager</title>
4
4
  <meta name="theme-color" content="#ffffff" />
5
- <link rel="stylesheet" href="fastapi-voyager-static/graphviz.svg.css" />
6
- <link rel="stylesheet" href="fastapi-voyager-static/quasar.min.css" />
5
+ <link
6
+ rel="stylesheet"
7
+ href="fastapi-voyager-static/graphviz.svg.css<!-- VERSION_PLACEHOLDER -->"
8
+ />
9
+ <link
10
+ rel="stylesheet"
11
+ href="fastapi-voyager-static/quasar.min.css<!-- VERSION_PLACEHOLDER -->"
12
+ />
7
13
  <!-- App Icons / Favicons -->
8
14
  <link
9
15
  rel="apple-touch-icon"
@@ -111,8 +117,6 @@
111
117
  border-radius: 50%;
112
118
  background: #009485;
113
119
  color: white;
114
- border: 2px solid white;
115
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
116
120
  cursor: pointer;
117
121
  display: flex;
118
122
  align-items: center;
@@ -284,10 +288,15 @@
284
288
  aria-label="Help"
285
289
  style="margin-right: 50px; margin-left: 20px"
286
290
  >
287
- <q-tooltip anchor="bottom middle" self="top middle" :offset="[0,8]">
291
+ <q-tooltip
292
+ anchor="bottom middle"
293
+ self="top middle"
294
+ :offset="[0,8]"
295
+ style="background-color: #f5f5f5; color: black; border: 1px solid #666"
296
+ >
288
297
  <div
289
298
  class="column items-start text-left"
290
- style="line-height: 1.4; font-size: 12px"
299
+ style="line-height: 1.4; font-size: 14px"
291
300
  >
292
301
  <ul>
293
302
  <li>scroll to zoom in/out</li>
@@ -571,7 +580,7 @@
571
580
  </q-dialog>
572
581
  </div>
573
582
  <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
574
- <script src="fastapi-voyager-static/quasar.min.js"></script>
583
+ <script src="fastapi-voyager-static/quasar.min.js<!-- VERSION_PLACEHOLDER -->"></script>
575
584
  <script
576
585
  src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"
577
586
  integrity="sha512-egJ/Y+22P9NQ9aIyVCh0VCOsfydyn8eNmqBy+y2CnJG+fpRIxXMS6jbWP8tVKp0jp+NO5n8WtMUAnNnGoJKi4w=="
@@ -593,7 +602,7 @@
593
602
  ></script>
594
603
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-mousewheel/3.1.13/jquery.mousewheel.min.js"></script>
595
604
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-color/2.1.2/jquery.color.min.js"></script>
596
- <script src="fastapi-voyager-static/graphviz.svg.js"></script>
605
+ <script src="fastapi-voyager-static/graphviz.svg.js<!-- VERSION_PLACEHOLDER -->"></script>
597
606
  <!-- highlight.js minimal ES module load (python only) -->
598
607
  <link
599
608
  rel="stylesheet"
@@ -615,7 +624,10 @@
615
624
  }
616
625
  })
617
626
  </script>
618
- <script type="module" src="fastapi-voyager-static/vue-main.js"></script>
627
+ <script
628
+ type="module"
629
+ src="fastapi-voyager-static/vue-main.js<!-- VERSION_PLACEHOLDER -->"
630
+ ></script>
619
631
 
620
632
  <!-- GA_SNIPPET -->
621
633
  </body>
@@ -1,10 +1,6 @@
1
1
  const { reactive } = window.Vue
2
2
 
3
3
  const state = reactive({
4
- item: {
5
- count: 0,
6
- },
7
-
8
4
  version: "",
9
5
  config: {
10
6
  initial_page_policy: "first",
@@ -15,7 +11,8 @@ const state = reactive({
15
11
  mode: "voyager", // voyager / er-diagram
16
12
 
17
13
  previousTagRoute: {
18
- // for shift + click, store previous tag/route, and populate back when needed
14
+ // Store the last non-search tag/route selection for restoration when clearing search
15
+ // Used by resetSearch to return to the state before entering search mode
19
16
  hasValue: false,
20
17
  tag: null,
21
18
  routeId: null,
@@ -65,6 +62,9 @@ const state = reactive({
65
62
  fieldOptions: [],
66
63
  },
67
64
 
65
+ // cache all schema options for filtering
66
+ allSchemaOptions: [],
67
+
68
68
  // route information
69
69
  routeDetail: {
70
70
  show: false,
@@ -105,13 +105,446 @@ const state = reactive({
105
105
  },
106
106
  })
107
107
 
108
- const mutations = {
109
- increment() {
110
- state.item.count += 1
108
+ const getters = {
109
+ /**
110
+ * Find tag name by route ID
111
+ * Used to determine which tag a route belongs to
112
+ */
113
+ findTagByRoute(routeId) {
114
+ return (
115
+ state.leftPanel.tags.find((tag) => (tag.routes || []).some((route) => route.id === routeId))
116
+ ?.name || null
117
+ )
118
+ },
119
+ }
120
+
121
+ const actions = {
122
+ /**
123
+ * Read tag, route and mode from URL query parameters
124
+ * @returns {{ tag: string|null, route: string|null, mode: string|null }}
125
+ */
126
+ readQuerySelection() {
127
+ if (typeof window === "undefined") {
128
+ return { tag: null, route: null, mode: null }
129
+ }
130
+ const params = new URLSearchParams(window.location.search)
131
+ return {
132
+ tag: params.get("tag") || null,
133
+ route: params.get("route") || null,
134
+ mode: params.get("mode") || null,
135
+ }
136
+ },
137
+
138
+ /**
139
+ * Sync current tag, route and mode selection to URL
140
+ * Updates browser URL without reloading the page
141
+ */
142
+ syncSelectionToUrl() {
143
+ if (typeof window === "undefined") {
144
+ return
145
+ }
146
+ const params = new URLSearchParams(window.location.search)
147
+ if (state.leftPanel.tag) {
148
+ params.set("tag", state.leftPanel.tag)
149
+ } else {
150
+ params.delete("tag")
151
+ }
152
+ if (state.leftPanel.routeId) {
153
+ params.set("route", state.leftPanel.routeId)
154
+ } else {
155
+ params.delete("route")
156
+ }
157
+ // Always sync mode to URL for consistency
158
+ if (state.mode) {
159
+ params.set("mode", state.mode)
160
+ } else {
161
+ params.delete("mode")
162
+ }
163
+ const hash = window.location.hash || ""
164
+ const search = params.toString()
165
+ const base = window.location.pathname
166
+ const newUrl = search ? `${base}?${search}${hash}` : `${base}${hash}`
167
+ window.history.replaceState({}, "", newUrl)
168
+ },
169
+
170
+ /**
171
+ * Apply selection from URL query parameters to state
172
+ * @param {{ tag: string|null, route: string|null, mode: string|null }} selection
173
+ * @returns {boolean} - true if any selection was applied
174
+ */
175
+ applySelectionFromQuery(selection) {
176
+ let applied = false
177
+ if (selection.tag && state.leftPanel.tags.some((tag) => tag.name === selection.tag)) {
178
+ state.leftPanel.tag = selection.tag
179
+ state.leftPanel._tag = selection.tag
180
+ applied = true
181
+ }
182
+ if (selection.route && state.graph.routeItems?.[selection.route]) {
183
+ state.leftPanel.routeId = selection.route
184
+ applied = true
185
+ const inferredTag = getters.findTagByRoute(selection.route)
186
+ if (inferredTag) {
187
+ state.leftPanel.tag = inferredTag
188
+ state.leftPanel._tag = inferredTag
189
+ }
190
+ }
191
+ // Apply mode from URL if it's valid
192
+ if (selection.mode === "voyager" || selection.mode === "er-diagram") {
193
+ state.mode = selection.mode
194
+ applied = true
195
+ }
196
+ return applied
197
+ },
198
+
199
+ /**
200
+ * Restore full tags from cache
201
+ * Used when resetting search mode
202
+ */
203
+ loadFullTags() {
204
+ state.leftPanel.tags = state.leftPanel.fullTagsCache
205
+ },
206
+
207
+ /**
208
+ * Populate field options based on selected schema
209
+ * @param {string} schemaId - Schema ID
210
+ */
211
+ populateFieldOptions(schemaId) {
212
+ if (!schemaId) {
213
+ state.search.fieldOptions = []
214
+ state.search.fieldName = null
215
+ return
216
+ }
217
+ const schema = state.graph.schemaMap?.[schemaId]
218
+ if (!schema) {
219
+ state.search.fieldOptions = []
220
+ state.search.fieldName = null
221
+ return
222
+ }
223
+ const fields = Array.isArray(schema.fields) ? schema.fields.map((f) => f.name) : []
224
+ state.search.fieldOptions = fields
225
+ if (!fields.includes(state.search.fieldName)) {
226
+ state.search.fieldName = null
227
+ }
228
+ },
229
+
230
+ /**
231
+ * Rebuild schema options from schema map
232
+ * Should be called when schema map changes
233
+ */
234
+ rebuildSchemaOptions() {
235
+ const dict = state.graph.schemaMap || {}
236
+ const opts = Object.values(dict).map((s) => ({
237
+ label: s.name,
238
+ desc: s.id,
239
+ value: s.id,
240
+ }))
241
+ state.allSchemaOptions = opts
242
+ state.search.schemaOptions = opts.slice()
243
+ this.populateFieldOptions(state.search.schemaName)
244
+ },
245
+
246
+ /**
247
+ * Load tags based on search criteria
248
+ * @returns {Promise<void>}
249
+ */
250
+ async loadSearchedTags() {
251
+ try {
252
+ const payload = {
253
+ schema_name: state.search.schemaName,
254
+ schema_field: state.search.fieldName || null,
255
+ show_fields: state.filter.showFields,
256
+ brief: state.filter.brief,
257
+ hide_primitive_route: state.filter.hidePrimitiveRoute,
258
+ show_module: state.filter.showModule,
259
+ }
260
+ const res = await fetch("dot-search", {
261
+ method: "POST",
262
+ headers: { "Content-Type": "application/json" },
263
+ body: JSON.stringify(payload),
264
+ })
265
+ if (res.ok) {
266
+ const data = await res.json()
267
+ const tags = Array.isArray(data.tags) ? data.tags : []
268
+ state.leftPanel.tags = tags
269
+ }
270
+ } catch (err) {
271
+ console.error("dot-search failed", err)
272
+ }
273
+ },
274
+
275
+ /**
276
+ * Load initial data from API
277
+ * @param {Function} onGenerate - Callback to generate graph after load
278
+ * @param {Function} renderBasedOnInitialPolicy - Callback to render based on policy
279
+ * @returns {Promise<void>}
280
+ */
281
+ async loadInitial(onGenerate, renderBasedOnInitialPolicy) {
282
+ state.initializing = true
283
+ try {
284
+ const res = await fetch("dot")
285
+ const data = await res.json()
286
+ const tags = Array.isArray(data.tags) ? data.tags : []
287
+ state.leftPanel.tags = tags
288
+ // Cache the full tags for later use (e.g., resetSearch)
289
+ state.leftPanel.fullTagsCache = tags
290
+
291
+ const schemasArr = Array.isArray(data.schemas) ? data.schemas : []
292
+ // Build dict keyed by id for faster lookups and simpler prop passing
293
+ const schemaMap = Object.fromEntries(schemasArr.map((s) => [s.id, s]))
294
+ state.graph.schemaMap = schemaMap
295
+ state.graph.schemaKeys = new Set(Object.keys(schemaMap))
296
+ state.graph.routeItems = data.tags
297
+ .map((t) => t.routes)
298
+ .flat()
299
+ .reduce((acc, r) => {
300
+ acc[r.id] = r
301
+ return acc
302
+ }, {})
303
+ state.modeControl.briefModeEnabled = data.enable_brief_mode || false
304
+ state.version = data.version || ""
305
+ state.swagger.url = data.swagger_url || null
306
+ state.config.has_er_diagram = data.has_er_diagram || false
307
+ state.config.enable_pydantic_resolve_meta = data.enable_pydantic_resolve_meta || false
308
+
309
+ this.rebuildSchemaOptions()
310
+
311
+ const querySelection = this.readQuerySelection()
312
+ const restoredFromQuery = this.applySelectionFromQuery(querySelection)
313
+ if (restoredFromQuery) {
314
+ this.syncSelectionToUrl()
315
+ onGenerate()
316
+ return
317
+ } else {
318
+ state.config.initial_page_policy = data.initial_page_policy
319
+ // Check if mode was applied from URL even if tag/route wasn't
320
+ if (
321
+ querySelection.mode &&
322
+ (querySelection.mode === "voyager" || querySelection.mode === "er-diagram")
323
+ ) {
324
+ this.syncSelectionToUrl()
325
+ onGenerate()
326
+ return
327
+ }
328
+ renderBasedOnInitialPolicy()
329
+ }
330
+
331
+ // default route options placeholder
332
+ } catch (e) {
333
+ console.error("Initial load failed", e)
334
+ } finally {
335
+ state.initializing = false
336
+ }
337
+ },
338
+
339
+ /**
340
+ * Filter schema options based on search text
341
+ * Used by Quasar select component's filter function
342
+ * @param {string} val - Search text
343
+ * @param {Function} update - Quasar update callback
344
+ */
345
+ filterSearchSchemas(val, update) {
346
+ const needle = (val || "").toLowerCase()
347
+ update(() => {
348
+ if (!needle) {
349
+ state.search.schemaOptions = state.allSchemaOptions.slice()
350
+ return
351
+ }
352
+ state.search.schemaOptions = state.allSchemaOptions.filter((option) =>
353
+ option.label.toLowerCase().includes(needle)
354
+ )
355
+ })
356
+ },
357
+
358
+ /**
359
+ * Handle schema selection change
360
+ * Updates state and triggers search if a schema is selected
361
+ * @param {string} val - Selected schema ID
362
+ * @param {Function} onSearch - Callback to trigger search
363
+ */
364
+ onSearchSchemaChange(val, onSearch) {
365
+ state.search.schemaName = val
366
+ state.search.mode = false
367
+ if (!val) {
368
+ // Clearing the select should only run resetSearch via @clear
369
+ return
370
+ }
371
+ onSearch()
372
+ },
373
+
374
+ /**
375
+ * Reset detail panels (right drawer and route detail)
376
+ */
377
+ resetDetailPanels() {
378
+ state.rightDrawer.drawer = false
379
+ state.routeDetail.show = false
380
+ state.schemaDetail.schemaCodeName = ""
381
+ },
382
+
383
+ /**
384
+ * Reset left panel selection and regenerate
385
+ * @param {Function} onGenerate - Callback to regenerate graph
386
+ */
387
+ onReset(onGenerate) {
388
+ state.leftPanel.tag = null
389
+ state.leftPanel._tag = null
390
+ state.leftPanel.routeId = null
391
+ this.syncSelectionToUrl()
392
+ onGenerate()
393
+ },
394
+
395
+ /**
396
+ * Toggle pydantic resolve meta visibility
397
+ * @param {boolean} val - New value
398
+ * @param {Function} onGenerate - Callback to regenerate graph
399
+ */
400
+ togglePydanticResolveMeta(val, onGenerate) {
401
+ state.modeControl.pydanticResolveMetaEnabled = val
402
+ try {
403
+ localStorage.setItem("pydantic_resolve_meta", JSON.stringify(val))
404
+ } catch (e) {
405
+ console.warn("Failed to save pydantic_resolve_meta to localStorage", e)
406
+ }
407
+ onGenerate()
408
+ },
409
+
410
+ /**
411
+ * Toggle show module clustering
412
+ * @param {boolean} val - New value
413
+ * @param {Function} onGenerate - Callback to regenerate graph
414
+ */
415
+ toggleShowModule(val, onGenerate) {
416
+ state.filter.showModule = val
417
+ try {
418
+ localStorage.setItem("show_module_cluster", JSON.stringify(val))
419
+ } catch (e) {
420
+ console.warn("Failed to save show_module_cluster to localStorage", e)
421
+ }
422
+ onGenerate()
423
+ },
424
+
425
+ /**
426
+ * Toggle show fields option
427
+ * @param {string} field - Field display option ("single", "object", "all")
428
+ * @param {Function} onGenerate - Callback to regenerate graph
429
+ */
430
+ toggleShowField(field, onGenerate) {
431
+ state.filter.showFields = field
432
+ onGenerate(false)
433
+ },
434
+
435
+ /**
436
+ * Toggle brief mode
437
+ * @param {boolean} val - New value
438
+ * @param {Function} onGenerate - Callback to regenerate graph
439
+ */
440
+ toggleBrief(val, onGenerate) {
441
+ state.filter.brief = val
442
+ try {
443
+ localStorage.setItem("brief_mode", JSON.stringify(val))
444
+ } catch (e) {
445
+ console.warn("Failed to save brief_mode to localStorage", e)
446
+ }
447
+ onGenerate()
448
+ },
449
+
450
+ /**
451
+ * Toggle hide primitive route
452
+ * @param {boolean} val - New value
453
+ * @param {Function} onGenerate - Callback to regenerate graph
454
+ */
455
+ toggleHidePrimitiveRoute(val, onGenerate) {
456
+ state.filter.hidePrimitiveRoute = val
457
+ try {
458
+ localStorage.setItem("hide_primitive", JSON.stringify(val))
459
+ } catch (e) {
460
+ console.warn("Failed to save hide_primitive to localStorage", e)
461
+ }
462
+ onGenerate(false)
463
+ },
464
+
465
+ /**
466
+ * Render based on initial page policy
467
+ * @param {Function} onGenerate - Callback to regenerate graph
468
+ */
469
+ renderBasedOnInitialPolicy(onGenerate) {
470
+ switch (state.config.initial_page_policy) {
471
+ case "full":
472
+ onGenerate()
473
+ return
474
+ case "empty":
475
+ return
476
+ case "first":
477
+ state.leftPanel.tag = state.leftPanel.tags.length > 0 ? state.leftPanel.tags[0].name : null
478
+ state.leftPanel._tag = state.leftPanel.tag
479
+ this.syncSelectionToUrl()
480
+ onGenerate()
481
+ return
482
+ }
483
+ },
484
+
485
+ /**
486
+ * Build payload for Voyager rendering
487
+ * @returns {Object} Payload for dot API
488
+ */
489
+ buildVoyagerPayload() {
490
+ const activeSchema = state.search.mode ? state.search.schemaName : null
491
+ const activeField = state.search.mode ? state.search.fieldName : null
492
+ return {
493
+ tags: state.leftPanel.tag ? [state.leftPanel.tag] : null,
494
+ schema_name: activeSchema || null,
495
+ schema_field: activeField || null,
496
+ route_name: state.leftPanel.routeId || null,
497
+ show_fields: state.filter.showFields,
498
+ brief: state.filter.brief,
499
+ hide_primitive_route: state.filter.hidePrimitiveRoute,
500
+ show_module: state.filter.showModule,
501
+ show_pydantic_resolve_meta: state.modeControl.pydanticResolveMetaEnabled,
502
+ }
503
+ },
504
+
505
+ /**
506
+ * Build payload for ER Diagram rendering
507
+ * @returns {Object} Payload for er-diagram API
508
+ */
509
+ buildErDiagramPayload() {
510
+ return {
511
+ show_fields: state.filter.showFields,
512
+ show_module: state.filter.showModule,
513
+ }
514
+ },
515
+
516
+ /**
517
+ * Restore search state and return whether to regenerate
518
+ * @returns {boolean} - true if should regenerate with previous selection
519
+ */
520
+ resetSearchState() {
521
+ state.search.mode = false
522
+ const hadPreviousValue = state.previousTagRoute.hasValue
523
+
524
+ if (hadPreviousValue) {
525
+ state.leftPanel.tag = state.previousTagRoute.tag
526
+ state.leftPanel._tag = state.previousTagRoute.tag
527
+ state.leftPanel.routeId = state.previousTagRoute.routeId
528
+ // Clear the saved state
529
+ state.previousTagRoute.hasValue = false
530
+ } else {
531
+ state.leftPanel.tag = null
532
+ state.leftPanel._tag = null
533
+ state.leftPanel.routeId = null
534
+ }
535
+
536
+ this.syncSelectionToUrl()
537
+ this.loadFullTags()
538
+
539
+ return hadPreviousValue
111
540
  },
112
541
  }
113
542
 
543
+ const mutations = {}
544
+
114
545
  export const store = {
115
546
  state,
547
+ getters,
548
+ actions,
116
549
  mutations,
117
550
  }
@@ -22,7 +22,6 @@ function loadToggleState(key, defaultValue = false) {
22
22
  const app = createApp({
23
23
  setup() {
24
24
  let graphUI = null
25
- const allSchemaOptions = ref([])
26
25
  const erDiagramLoading = ref(false)
27
26
  const erDiagramCache = ref("")
28
27
 
@@ -42,20 +41,13 @@ const app = createApp({
42
41
  graphUI = new GraphUI("#graph", {
43
42
  onSchemaShiftClick: (id) => {
44
43
  if (store.state.graph.schemaKeys.has(id)) {
45
- // Only save current tag/route if we're not already in search mode
46
- // This prevents overwriting the saved state with null values
47
- if (!store.state.previousTagRoute.hasValue && !store.state.search.mode) {
48
- store.state.previousTagRoute.tag = store.state.leftPanel.tag
49
- store.state.previousTagRoute.routeId = store.state.leftPanel.routeId
50
- store.state.previousTagRoute.hasValue = true
51
- }
52
44
  store.state.search.mode = true
53
45
  store.state.search.schemaName = id
54
46
  onSearch()
55
47
  }
56
48
  },
57
49
  onSchemaClick: (id) => {
58
- resetDetailPanels()
50
+ store.actions.resetDetailPanels()
59
51
  if (store.state.graph.schemaKeys.has(id)) {
60
52
  store.state.schemaDetail.schemaCodeName = id
61
53
  store.state.rightDrawer.drawer = true
@@ -66,257 +58,44 @@ const app = createApp({
66
58
  }
67
59
  },
68
60
  resetCb: () => {
69
- resetDetailPanels()
61
+ store.actions.resetDetailPanels()
70
62
  },
71
63
  })
72
64
  }
73
65
 
74
- function rebuildSchemaOptions() {
75
- const dict = store.state.graph.schemaMap || {}
76
- const opts = Object.values(dict).map((s) => ({
77
- label: s.name,
78
- desc: s.id,
79
- value: s.id,
80
- }))
81
- allSchemaOptions.value = opts
82
- store.state.search.schemaOptions = opts.slice()
83
- populateFieldOptions(store.state.search.schemaName)
84
- }
85
-
86
- function populateFieldOptions(schemaId) {
87
- if (!schemaId) {
88
- store.state.search.fieldOptions = []
89
- store.state.search.fieldName = null
90
- return
91
- }
92
- const schema = store.state.graph.schemaMap?.[schemaId]
93
- if (!schema) {
94
- store.state.search.fieldOptions = []
95
- store.state.search.fieldName = null
96
- return
97
- }
98
- const fields = Array.isArray(schema.fields) ? schema.fields.map((f) => f.name) : []
99
- store.state.search.fieldOptions = fields
100
- if (!fields.includes(store.state.search.fieldName)) {
101
- store.state.search.fieldName = null
102
- }
103
- }
104
-
105
- function filterSearchSchemas(val, update) {
106
- const needle = (val || "").toLowerCase()
107
- update(() => {
108
- if (!needle) {
109
- store.state.search.schemaOptions = allSchemaOptions.value.slice()
110
- return
111
- }
112
- store.state.search.schemaOptions = allSchemaOptions.value.filter((option) =>
113
- option.label.toLowerCase().includes(needle)
114
- )
115
- })
116
- }
117
-
118
- function onSearchSchemaChange(val) {
119
- store.state.search.schemaName = val
120
- store.state.search.mode = false
121
- if (!val) {
122
- // Clearing the select should only run resetSearch via @clear
123
- return
124
- }
125
- onSearch()
126
- }
127
-
128
- function readQuerySelection() {
129
- if (typeof window === "undefined") {
130
- return { tag: null, route: null }
131
- }
132
- const params = new URLSearchParams(window.location.search)
133
- return {
134
- tag: params.get("tag") || null,
135
- route: params.get("route") || null,
136
- }
137
- }
138
-
139
- function findTagByRoute(routeId) {
140
- return (
141
- store.state.leftPanel.tags.find((tag) =>
142
- (tag.routes || []).some((route) => route.id === routeId)
143
- )?.name || null
144
- )
145
- }
146
-
147
- function syncSelectionToUrl() {
148
- if (typeof window === "undefined") {
149
- return
150
- }
151
- const params = new URLSearchParams(window.location.search)
152
- if (store.state.leftPanel.tag) {
153
- params.set("tag", store.state.leftPanel.tag)
154
- } else {
155
- params.delete("tag")
156
- }
157
- if (store.state.leftPanel.routeId) {
158
- params.set("route", store.state.leftPanel.routeId)
159
- } else {
160
- params.delete("route")
161
- }
162
- const hash = window.location.hash || ""
163
- const search = params.toString()
164
- const base = window.location.pathname
165
- const newUrl = search ? `${base}?${search}${hash}` : `${base}${hash}`
166
- window.history.replaceState({}, "", newUrl)
167
- }
168
-
169
- function applySelectionFromQuery(selection) {
170
- let applied = false
171
- if (selection.tag && store.state.leftPanel.tags.some((tag) => tag.name === selection.tag)) {
172
- store.state.leftPanel.tag = selection.tag
173
- store.state.leftPanel._tag = selection.tag
174
- applied = true
175
- }
176
- if (selection.route && store.state.graph.routeItems?.[selection.route]) {
177
- store.state.leftPanel.routeId = selection.route
178
- applied = true
179
- const inferredTag = findTagByRoute(selection.route)
180
- if (inferredTag) {
181
- store.state.leftPanel.tag = inferredTag
182
- store.state.leftPanel._tag = inferredTag
183
- }
184
- }
185
- return applied
186
- }
187
-
188
66
  async function resetSearch() {
189
- store.state.search.mode = false
190
- const hadPreviousValue = store.state.previousTagRoute.hasValue
191
-
192
- if (hadPreviousValue) {
193
- store.state.leftPanel.tag = store.state.previousTagRoute.tag
194
- store.state.leftPanel._tag = store.state.previousTagRoute.tag
195
- store.state.leftPanel.routeId = store.state.previousTagRoute.routeId
196
- store.state.previousTagRoute.hasValue = false
197
- } else {
198
- store.state.leftPanel.tag = null
199
- store.state.leftPanel._tag = null
200
- store.state.leftPanel.routeId = null
201
- }
202
-
203
- syncSelectionToUrl()
204
-
205
- // Load the full tags from cache (not search results) since we're resetting search
206
- loadFullTags()
67
+ const hadPreviousValue = store.actions.resetSearchState()
207
68
 
208
69
  // If we restored a previous tag/route, generate with it
209
70
  // Otherwise, fall back to initial policy
210
71
  if (hadPreviousValue) {
211
72
  onGenerate()
212
73
  } else {
213
- renderBasedOnInitialPolicy()
74
+ store.actions.renderBasedOnInitialPolicy(onGenerate)
214
75
  }
215
76
  }
216
77
 
217
- function loadFullTags() {
218
- // Restore from cache (set by loadInitial)
219
- store.state.leftPanel.tags = store.state.leftPanel.fullTagsCache
220
- }
221
-
222
78
  async function onSearch() {
79
+ // Save current state before entering search mode (only if not already saved)
80
+ if (!store.state.previousTagRoute.hasValue) {
81
+ store.state.previousTagRoute.tag = store.state.leftPanel.tag
82
+ store.state.previousTagRoute.routeId = store.state.leftPanel.routeId
83
+ store.state.previousTagRoute.hasValue = true
84
+ }
85
+
223
86
  store.state.search.mode = true
224
87
  store.state.leftPanel.tag = null
225
88
  store.state.leftPanel._tag = null
226
89
  store.state.leftPanel.routeId = null
227
- syncSelectionToUrl()
228
- await loadSearchedTags()
90
+ store.actions.syncSelectionToUrl()
91
+ await store.actions.loadSearchedTags()
229
92
  await onGenerate()
230
93
  }
231
- async function loadSearchedTags() {
232
- try {
233
- const payload = {
234
- schema_name: store.state.search.schemaName,
235
- schema_field: store.state.search.fieldName || null,
236
- show_fields: store.state.filter.showFields,
237
- brief: store.state.filter.brief,
238
- hide_primitive_route: store.state.filter.hidePrimitiveRoute,
239
- show_module: store.state.filter.showModule,
240
- }
241
- const res = await fetch("dot-search", {
242
- method: "POST",
243
- headers: { "Content-Type": "application/json" },
244
- body: JSON.stringify(payload),
245
- })
246
- if (res.ok) {
247
- const data = await res.json()
248
- const tags = Array.isArray(data.tags) ? data.tags : []
249
- store.state.leftPanel.tags = tags
250
- }
251
- } catch (err) {
252
- console.error("dot-search failed", err)
253
- }
254
- }
255
94
 
256
95
  async function loadInitial() {
257
- store.state.initializing = true
258
- try {
259
- const res = await fetch("dot")
260
- const data = await res.json()
261
- const tags = Array.isArray(data.tags) ? data.tags : []
262
- store.state.leftPanel.tags = tags
263
- // Cache the full tags for later use (e.g., resetSearch)
264
- store.state.leftPanel.fullTagsCache = tags
265
-
266
- const schemasArr = Array.isArray(data.schemas) ? data.schemas : []
267
- // Build dict keyed by id for faster lookups and simpler prop passing
268
- const schemaMap = Object.fromEntries(schemasArr.map((s) => [s.id, s]))
269
- store.state.graph.schemaMap = schemaMap
270
- store.state.graph.schemaKeys = new Set(Object.keys(schemaMap))
271
- store.state.graph.routeItems = data.tags
272
- .map((t) => t.routes)
273
- .flat()
274
- .reduce((acc, r) => {
275
- acc[r.id] = r
276
- return acc
277
- }, {})
278
- store.state.modeControl.briefModeEnabled = data.enable_brief_mode || false
279
- store.state.version = data.version || ""
280
- store.state.swagger.url = data.swagger_url || null
281
- store.state.config.has_er_diagram = data.has_er_diagram || false
282
- store.state.config.enable_pydantic_resolve_meta = data.enable_pydantic_resolve_meta || false
283
-
284
- rebuildSchemaOptions()
285
-
286
- const querySelection = readQuerySelection()
287
- const restoredFromQuery = applySelectionFromQuery(querySelection)
288
- if (restoredFromQuery) {
289
- syncSelectionToUrl()
290
- onGenerate()
291
- return
292
- } else {
293
- store.state.config.initial_page_policy = data.initial_page_policy
294
- renderBasedOnInitialPolicy()
295
- }
296
-
297
- // default route options placeholder
298
- } catch (e) {
299
- console.error("Initial load failed", e)
300
- } finally {
301
- store.state.initializing = false
302
- }
303
- }
304
-
305
- async function renderBasedOnInitialPolicy() {
306
- switch (store.state.config.initial_page_policy) {
307
- case "full":
308
- onGenerate()
309
- return
310
- case "empty":
311
- return
312
- case "first":
313
- store.state.leftPanel.tag =
314
- store.state.leftPanel.tags.length > 0 ? store.state.leftPanel.tags[0].name : null
315
- store.state.leftPanel._tag = store.state.leftPanel.tag
316
- syncSelectionToUrl()
317
- onGenerate()
318
- return
319
- }
96
+ await store.actions.loadInitial(onGenerate, (cb) =>
97
+ store.actions.renderBasedOnInitialPolicy(cb)
98
+ )
320
99
  }
321
100
 
322
101
  async function onGenerate(resetZoom = true) {
@@ -331,21 +110,9 @@ const app = createApp({
331
110
  }
332
111
 
333
112
  async function renderVoyager(resetZoom = true) {
334
- const activeSchema = store.state.search.mode ? store.state.search.schemaName : null
335
- const activeField = store.state.search.mode ? store.state.search.fieldName : null
336
113
  store.state.generating = true
337
114
  try {
338
- const payload = {
339
- tags: store.state.leftPanel.tag ? [store.state.leftPanel.tag] : null,
340
- schema_name: activeSchema || null,
341
- schema_field: activeField || null,
342
- route_name: store.state.leftPanel.routeId || null,
343
- show_fields: store.state.filter.showFields,
344
- brief: store.state.filter.brief,
345
- hide_primitive_route: store.state.filter.hidePrimitiveRoute,
346
- show_module: store.state.filter.showModule,
347
- show_pydantic_resolve_meta: store.state.modeControl.pydanticResolveMetaEnabled,
348
- }
115
+ const payload = store.actions.buildVoyagerPayload()
349
116
  initGraphUI()
350
117
  const res = await fetch("dot", {
351
118
  method: "POST",
@@ -362,37 +129,10 @@ const app = createApp({
362
129
  }
363
130
  }
364
131
 
365
- function resetDetailPanels() {
366
- store.state.rightDrawer.drawer = false
367
- store.state.routeDetail.show = false
368
- store.state.schemaDetail.schemaCodeName = ""
369
- }
370
-
371
- async function onReset() {
372
- store.state.leftPanel.tag = null
373
- store.state.leftPanel._tag = null
374
- store.state.leftPanel.routeId = null
375
- syncSelectionToUrl()
376
- onGenerate()
377
- }
378
-
379
- async function togglePydanticResolveMeta(val) {
380
- store.state.modeControl.pydanticResolveMetaEnabled = val
381
- try {
382
- localStorage.setItem("pydantic_resolve_meta", JSON.stringify(val))
383
- } catch (e) {
384
- console.warn("Failed to save pydantic_resolve_meta to localStorage", e)
385
- }
386
- onGenerate()
387
- }
388
-
389
132
  async function renderErDiagram(resetZoom = true) {
390
133
  initGraphUI()
391
134
  erDiagramLoading.value = true
392
- const payload = {
393
- show_fields: store.state.filter.showFields,
394
- show_module: store.state.filter.showModule,
395
- }
135
+ const payload = store.actions.buildErDiagramPayload()
396
136
  try {
397
137
  const res = await fetch("er-diagram", {
398
138
  method: "POST",
@@ -423,12 +163,14 @@ const app = createApp({
423
163
  store.state.leftPanel.previousWidth = store.state.leftPanel.width
424
164
  }
425
165
  store.state.leftPanel.width = 0
166
+ store.actions.syncSelectionToUrl()
426
167
  await renderErDiagram()
427
168
  } else {
428
169
  store.state.search.invisible = false
429
170
 
430
171
  const fallbackWidth = store.state.leftPanel.previousWidth || 300
431
172
  store.state.leftPanel.width = fallbackWidth
173
+ store.actions.syncSelectionToUrl()
432
174
  await onGenerate()
433
175
  }
434
176
  }
@@ -447,7 +189,7 @@ const app = createApp({
447
189
 
448
190
  store.state.rightDrawer.drawer = false
449
191
  store.state.routeDetail.show = false
450
- syncSelectionToUrl()
192
+ store.actions.syncSelectionToUrl()
451
193
  }
452
194
 
453
195
  function toggleTagNavigatorCollapse() {
@@ -468,7 +210,7 @@ const app = createApp({
468
210
 
469
211
  function selectRoute(routeId) {
470
212
  // find belonging tag
471
- const belongingTag = findTagByRoute(routeId)
213
+ const belongingTag = store.getters.findTagByRoute(routeId)
472
214
  if (belongingTag) {
473
215
  store.state.leftPanel.tag = belongingTag
474
216
  store.state.leftPanel._tag = belongingTag
@@ -483,45 +225,10 @@ const app = createApp({
483
225
  store.state.rightDrawer.drawer = false
484
226
  store.state.routeDetail.show = false
485
227
  store.state.schemaDetail.schemaCodeName = ""
486
- syncSelectionToUrl()
487
- onGenerate()
488
- }
489
-
490
- function toggleShowModule(val) {
491
- store.state.filter.showModule = val
492
- try {
493
- localStorage.setItem("show_module_cluster", JSON.stringify(val))
494
- } catch (e) {
495
- console.warn("Failed to save show_module_cluster to localStorage", e)
496
- }
228
+ store.actions.syncSelectionToUrl()
497
229
  onGenerate()
498
230
  }
499
231
 
500
- function toggleShowField(field) {
501
- store.state.filter.showFields = field
502
- onGenerate(false)
503
- }
504
-
505
- function toggleBrief(val) {
506
- store.state.filter.brief = val
507
- try {
508
- localStorage.setItem("brief_mode", JSON.stringify(val))
509
- } catch (e) {
510
- console.warn("Failed to save brief_mode to localStorage", e)
511
- }
512
- onGenerate()
513
- }
514
-
515
- function toggleHidePrimitiveRoute(val) {
516
- store.state.filter.hidePrimitiveRoute = val
517
- try {
518
- localStorage.setItem("hide_primitive", JSON.stringify(val))
519
- } catch (e) {
520
- console.warn("Failed to save hide_primitive to localStorage", e)
521
- }
522
- onGenerate(false)
523
- }
524
-
525
232
  function startDragDrawer(e) {
526
233
  const startX = e.clientX
527
234
  const startWidth = store.state.rightDrawer.width
@@ -549,7 +256,7 @@ const app = createApp({
549
256
  watch(
550
257
  () => store.state.graph.schemaMap,
551
258
  () => {
552
- rebuildSchemaOptions()
259
+ store.actions.rebuildSchemaOptions()
553
260
  },
554
261
  { deep: false }
555
262
  )
@@ -573,8 +280,8 @@ const app = createApp({
573
280
  watch(
574
281
  () => store.state.search.schemaName,
575
282
  (schemaId) => {
576
- store.state.search.schemaOptions = allSchemaOptions.value.slice()
577
- populateFieldOptions(schemaId)
283
+ store.state.search.schemaOptions = store.state.allSchemaOptions.slice()
284
+ store.actions.populateFieldOptions(schemaId)
578
285
  if (!schemaId) {
579
286
  store.state.search.mode = false
580
287
  }
@@ -591,21 +298,22 @@ const app = createApp({
591
298
  store,
592
299
  onSearch,
593
300
  resetSearch,
594
- filterSearchSchemas,
595
- onSearchSchemaChange,
301
+ filterSearchSchemas: (val, update) => store.actions.filterSearchSchemas(val, update),
302
+ onSearchSchemaChange: (val) => store.actions.onSearchSchemaChange(val, onSearch),
596
303
  toggleTag,
597
304
  toggleTagNavigatorCollapse,
598
- toggleBrief,
599
- toggleHidePrimitiveRoute,
305
+ toggleBrief: (val) => store.actions.toggleBrief(val, onGenerate),
306
+ toggleHidePrimitiveRoute: (val) => store.actions.toggleHidePrimitiveRoute(val, onGenerate),
600
307
  selectRoute,
601
308
  onGenerate,
602
- onReset,
603
- toggleShowField,
309
+ onReset: () => store.actions.onReset(onGenerate),
310
+ toggleShowField: (field) => store.actions.toggleShowField(field, onGenerate),
604
311
  startDragDrawer,
605
- toggleShowModule,
312
+ toggleShowModule: (val) => store.actions.toggleShowModule(val, onGenerate),
606
313
  onModeChange,
607
314
  renderErDiagram,
608
- togglePydanticResolveMeta,
315
+ togglePydanticResolveMeta: (val) => store.actions.togglePydanticResolveMeta(val, onGenerate),
316
+ resetDetailPanels: () => store.actions.resetDetailPanels(),
609
317
  }
610
318
  },
611
319
  })
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.15.2
3
+ Version: 0.15.4
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
@@ -6,10 +6,10 @@ fastapi_voyager/module.py,sha256=h9YR3BpS-CAcJW9WCdVkF4opqwY32w9T67g9GfdLytk,342
6
6
  fastapi_voyager/pydantic_resolve_util.py,sha256=r4Rq7BtBcFOMV7O2Ab9TwLyRNL1yNDiQlGUVybf-sXs,3524
7
7
  fastapi_voyager/render.py,sha256=5tTuvvCCUwFCq3WJGT1rfTSW41mSDoVyJwMyQGrmhiQ,17271
8
8
  fastapi_voyager/render_style.py,sha256=mPOuChEl71-3agCbPwkMt2sFmax2AEKDI6dK90eFPRc,2552
9
- fastapi_voyager/server.py,sha256=MUc_ia3_QIbYQ8VenOxv3CEYVTiPDs7e_H40YoalxZs,8970
9
+ fastapi_voyager/server.py,sha256=E0gGU7D1pBvnV7gFLBUvnkwtiBFbU1PbxEXHHSIA1oA,9115
10
10
  fastapi_voyager/type.py,sha256=zluWvh5vpnjXJ9aAmyNJTSmXZPjAHCvgRT5oQRAjHrg,2104
11
11
  fastapi_voyager/type_helper.py,sha256=FmfrZAI3Z4uDdh3sH_kH7UGoY6yNVPapneSN86qY_wo,10209
12
- fastapi_voyager/version.py,sha256=MNw2WhSygJJXBlu1ZTOV_kDUGMOPYB98_73ZGP4k0S4,49
12
+ fastapi_voyager/version.py,sha256=AgJGrnhFIPGdn_tW4I4oEPuyTOgMXEkK0I5qVj2kag0,49
13
13
  fastapi_voyager/voyager.py,sha256=4vonmL-xt54C5San-DRBq4mjoV8Q96eoWRy68MJ1IJw,14169
14
14
  fastapi_voyager/templates/dot/cluster.j2,sha256=I2z9KkfCzmAtqXe0gXBnxnOfBXUSpdlATs3uf-O8_B8,307
15
15
  fastapi_voyager/templates/dot/cluster_container.j2,sha256=2tH1mOJvPoVKE_aHVMR3t06TfH_dYa9OeH6DBqSHt_A,204
@@ -24,14 +24,14 @@ fastapi_voyager/templates/html/pydantic_meta.j2,sha256=_tsSqjucs_QrAlPIVRy9u6I2-
24
24
  fastapi_voyager/templates/html/schema_field_row.j2,sha256=KfKexHO_QJV-OIJS0eiY_7fqA8031fWpD2g2wTv4BuE,111
25
25
  fastapi_voyager/templates/html/schema_header.j2,sha256=9WpuHLy3Zbv5GHG08qqaj5Xf-gaR-79ErBYuANZp7iA,179
26
26
  fastapi_voyager/templates/html/schema_table.j2,sha256=rzphiGk1il7uv4Gr2p_HLPHqyLZk63vLrGAmIduTdSE,117
27
- fastapi_voyager/web/graph-ui.js,sha256=cmEcM35rQsR6eQA2Sc8zJ_MOA0UfEQ9WvGA1zNInUUc,6285
27
+ fastapi_voyager/web/graph-ui.js,sha256=4gEkXTgbA6CouD4IDMW5yKYfJTxHN2vL9G0CAr6w4qA,7662
28
28
  fastapi_voyager/web/graphviz.svg.css,sha256=K218ov_mdSe3ga4KwhiBB92ynVvm5zaAk9_D9a3d8hE,1546
29
29
  fastapi_voyager/web/graphviz.svg.js,sha256=deI815RgxpZ3_MpELeV-TBYy2MVuUvZtQOHfS3aeXHY,18203
30
- fastapi_voyager/web/index.html,sha256=lA59Op4u1bb-B105Iadn2KN_n11-AtXFdJglUezZIDg,23225
30
+ fastapi_voyager/web/index.html,sha256=wM9vJ_UfHR8p98F6SEMCKKjJcBEl0EyosWuPqVZYXvA,23496
31
31
  fastapi_voyager/web/quasar.min.css,sha256=F5jQe7X2XT54VlvAaa2V3GsBFdVD-vxDZeaPLf6U9CU,203145
32
32
  fastapi_voyager/web/quasar.min.js,sha256=h0ftyPMW_CRiyzeVfQqiup0vrVt4_QWojpqmpnpn07E,502974
33
- fastapi_voyager/web/store.js,sha256=h9WLKgFdmHrF9riirX-3FnCODZzeNZcvnxqZvfFnXLw,2020
34
- fastapi_voyager/web/vue-main.js,sha256=49iJ_uoXde-jcYJpS-_Gd10zGuxo_nhp1-LAAsFzyVc,20034
33
+ fastapi_voyager/web/store.js,sha256=7anY7HGF7aEx1xpXWz58Ca5WbnYCRnzdj7kqNjcaT_M,15103
34
+ fastapi_voyager/web/vue-main.js,sha256=4lJi6ADrcaOzjXcQePkp7CyiCkAnvhnO-nkF3E6G3s4,10650
35
35
  fastapi_voyager/web/component/demo.js,sha256=sAklFGhKGmMy9-ofgOw2oPIidAoIOgHu6yvV51L_MAA,350
36
36
  fastapi_voyager/web/component/render-graph.js,sha256=9wnO70n3eyPKTpa744idgs5PSwgvzbfv4InZ68eEOKs,2454
37
37
  fastapi_voyager/web/component/route-code-display.js,sha256=a823nBz3EEjutW2pfi73rcF3hodCBmgYNmuZi94sXE4,3615
@@ -43,8 +43,8 @@ fastapi_voyager/web/icon/favicon-16x16.png,sha256=JC07jEzfIYxBIoQn_FHXvyHuxESdhW
43
43
  fastapi_voyager/web/icon/favicon-32x32.png,sha256=C7v1h58cfWOsiLp9yOIZtlx-dLasBcq3NqpHVGRmpt4,1859
44
44
  fastapi_voyager/web/icon/favicon.ico,sha256=tZolYIXkkBcFiYl1A8ksaXN2VjGamzcSdes838dLvNc,15406
45
45
  fastapi_voyager/web/icon/site.webmanifest,sha256=GRozZ5suTykYcPMap1QhjrAB8PLW0mbT_phhzw_utvQ,316
46
- fastapi_voyager-0.15.2.dist-info/METADATA,sha256=O7X7HJ1JMuXk_bJJ93gre1ESQOaeyBxWj4UtYtku1jo,8513
47
- fastapi_voyager-0.15.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
- fastapi_voyager-0.15.2.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
49
- fastapi_voyager-0.15.2.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
50
- fastapi_voyager-0.15.2.dist-info/RECORD,,
46
+ fastapi_voyager-0.15.4.dist-info/METADATA,sha256=6BftnkJpr0w8cKiAubLyJ6ZiOeGmvlkOwE9jZLZ5UMM,8513
47
+ fastapi_voyager-0.15.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
+ fastapi_voyager-0.15.4.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
49
+ fastapi_voyager-0.15.4.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
50
+ fastapi_voyager-0.15.4.dist-info/RECORD,,