fastapi-voyager 0.14.1__py3-none-any.whl → 0.15.0__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 +423 -203
- fastapi_voyager/render_style.py +105 -0
- fastapi_voyager/templates/dot/cluster.j2 +10 -0
- fastapi_voyager/templates/dot/cluster_container.j2 +9 -0
- fastapi_voyager/templates/dot/digraph.j2 +25 -0
- fastapi_voyager/templates/dot/link.j2 +1 -0
- fastapi_voyager/templates/dot/route_node.j2 +5 -0
- fastapi_voyager/templates/dot/schema_node.j2 +5 -0
- fastapi_voyager/templates/dot/tag_node.j2 +5 -0
- fastapi_voyager/templates/html/colored_text.j2 +1 -0
- fastapi_voyager/templates/html/pydantic_meta.j2 +1 -0
- fastapi_voyager/templates/html/schema_field_row.j2 +1 -0
- fastapi_voyager/templates/html/schema_header.j2 +2 -0
- fastapi_voyager/templates/html/schema_table.j2 +4 -0
- fastapi_voyager/version.py +1 -1
- {fastapi_voyager-0.14.1.dist-info → fastapi_voyager-0.15.0.dist-info}/METADATA +3 -2
- {fastapi_voyager-0.14.1.dist-info → fastapi_voyager-0.15.0.dist-info}/RECORD +20 -7
- {fastapi_voyager-0.14.1.dist-info → fastapi_voyager-0.15.0.dist-info}/WHEEL +0 -0
- {fastapi_voyager-0.14.1.dist-info → fastapi_voyager-0.15.0.dist-info}/entry_points.txt +0 -0
- {fastapi_voyager-0.14.1.dist-info → fastapi_voyager-0.15.0.dist-info}/licenses/LICENSE +0 -0
fastapi_voyager/render.py
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Render FastAPI application structure to DOT format using Jinja2 templates.
|
|
3
|
+
"""
|
|
1
4
|
from logging import getLogger
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
2
8
|
|
|
3
9
|
from fastapi_voyager.module import build_module_route_tree, build_module_schema_tree
|
|
10
|
+
from fastapi_voyager.render_style import RenderConfig
|
|
4
11
|
from fastapi_voyager.type import (
|
|
5
12
|
PK,
|
|
6
13
|
FieldType,
|
|
7
|
-
Link,
|
|
8
14
|
FieldInfo,
|
|
15
|
+
Link,
|
|
9
16
|
ModuleNode,
|
|
10
17
|
ModuleRoute,
|
|
11
18
|
Route,
|
|
@@ -15,8 +22,38 @@ from fastapi_voyager.type import (
|
|
|
15
22
|
|
|
16
23
|
logger = getLogger(__name__)
|
|
17
24
|
|
|
25
|
+
# Get the template directory relative to this file
|
|
26
|
+
TEMPLATE_DIR = Path(__file__).parent / "templates"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TemplateRenderer:
|
|
30
|
+
"""
|
|
31
|
+
Jinja2-based template renderer for DOT and HTML templates.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, template_dir: Path = TEMPLATE_DIR):
|
|
35
|
+
# Initialize Jinja2 environment
|
|
36
|
+
self.env = Environment(
|
|
37
|
+
loader=FileSystemLoader(template_dir),
|
|
38
|
+
autoescape=select_autoescape(),
|
|
39
|
+
trim_blocks=True,
|
|
40
|
+
lstrip_blocks=True,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def render_template(self, template_name: str, **context) -> str:
|
|
44
|
+
"""Render a template with the given context."""
|
|
45
|
+
template = self.env.get_template(template_name)
|
|
46
|
+
return template.render(**context)
|
|
47
|
+
|
|
18
48
|
|
|
19
49
|
class Renderer:
|
|
50
|
+
"""
|
|
51
|
+
Render FastAPI application structure to DOT format.
|
|
52
|
+
|
|
53
|
+
This class handles the conversion of tags, routes, schemas, and links
|
|
54
|
+
into Graphviz DOT format, with support for custom styling and filtering.
|
|
55
|
+
"""
|
|
56
|
+
|
|
20
57
|
def __init__(
|
|
21
58
|
self,
|
|
22
59
|
*,
|
|
@@ -25,6 +62,7 @@ class Renderer:
|
|
|
25
62
|
schema: str | None = None,
|
|
26
63
|
show_module: bool = True,
|
|
27
64
|
show_pydantic_resolve_meta: bool = False,
|
|
65
|
+
config: RenderConfig | None = None,
|
|
28
66
|
) -> None:
|
|
29
67
|
self.show_fields = show_fields if show_fields in ('single', 'object', 'all') else 'single'
|
|
30
68
|
self.module_color = module_color or {}
|
|
@@ -32,252 +70,434 @@ class Renderer:
|
|
|
32
70
|
self.show_module = show_module
|
|
33
71
|
self.show_pydantic_resolve_meta = show_pydantic_resolve_meta
|
|
34
72
|
|
|
73
|
+
# Use provided config or create default
|
|
74
|
+
self.config = config or RenderConfig()
|
|
75
|
+
self.colors = self.config.colors
|
|
76
|
+
self.style = self.config.style
|
|
77
|
+
|
|
78
|
+
# Initialize template renderer
|
|
79
|
+
self.template_renderer = TemplateRenderer()
|
|
80
|
+
|
|
35
81
|
logger.info(f'show_module: {self.show_module}')
|
|
36
82
|
logger.info(f'module_color: {self.module_color}')
|
|
37
|
-
|
|
38
|
-
def render_pydantic_related_markup(self, field: FieldInfo):
|
|
39
|
-
if self.show_pydantic_resolve_meta is False:
|
|
40
|
-
return ''
|
|
41
83
|
|
|
42
|
-
|
|
84
|
+
def _render_pydantic_meta_parts(self, field: FieldInfo) -> list[str]:
|
|
85
|
+
"""Render pydantic-resolve metadata as HTML parts."""
|
|
86
|
+
if not self.show_pydantic_resolve_meta:
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
parts = []
|
|
43
90
|
if field.is_resolve:
|
|
44
|
-
parts.append(
|
|
91
|
+
parts.append(
|
|
92
|
+
self.template_renderer.render_template(
|
|
93
|
+
'html/colored_text.j2',
|
|
94
|
+
text='● resolve',
|
|
95
|
+
color=self.colors.resolve
|
|
96
|
+
)
|
|
97
|
+
)
|
|
45
98
|
if field.is_post:
|
|
46
|
-
parts.append(
|
|
99
|
+
parts.append(
|
|
100
|
+
self.template_renderer.render_template(
|
|
101
|
+
'html/colored_text.j2',
|
|
102
|
+
text='● post',
|
|
103
|
+
color=self.colors.post
|
|
104
|
+
)
|
|
105
|
+
)
|
|
47
106
|
if field.expose_as_info:
|
|
48
|
-
parts.append(
|
|
107
|
+
parts.append(
|
|
108
|
+
self.template_renderer.render_template(
|
|
109
|
+
'html/colored_text.j2',
|
|
110
|
+
text=f'● expose as: {field.expose_as_info}',
|
|
111
|
+
color=self.colors.expose_as
|
|
112
|
+
)
|
|
113
|
+
)
|
|
49
114
|
if field.send_to_info:
|
|
50
115
|
to_collectors = ', '.join(field.send_to_info)
|
|
51
|
-
parts.append(
|
|
116
|
+
parts.append(
|
|
117
|
+
self.template_renderer.render_template(
|
|
118
|
+
'html/colored_text.j2',
|
|
119
|
+
text=f'● send to: {to_collectors}',
|
|
120
|
+
color=self.colors.send_to
|
|
121
|
+
)
|
|
122
|
+
)
|
|
52
123
|
if field.collect_info:
|
|
53
124
|
defined_collectors = ', '.join(field.collect_info)
|
|
54
|
-
parts.append(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def render_schema_label(self, node: SchemaNode, color: str | None=None) -> str:
|
|
62
|
-
"""
|
|
63
|
-
TODO: should improve the logic with show_pydantic_resolve_meta
|
|
64
|
-
"""
|
|
125
|
+
parts.append(
|
|
126
|
+
self.template_renderer.render_template(
|
|
127
|
+
'html/colored_text.j2',
|
|
128
|
+
text=f'● collectors: {defined_collectors}',
|
|
129
|
+
color=self.colors.collector
|
|
130
|
+
)
|
|
131
|
+
)
|
|
65
132
|
|
|
66
|
-
|
|
133
|
+
return parts
|
|
67
134
|
|
|
68
|
-
|
|
135
|
+
def _render_schema_field(
|
|
136
|
+
self,
|
|
137
|
+
field: FieldInfo,
|
|
138
|
+
max_type_length: int | None = None
|
|
139
|
+
) -> str:
|
|
140
|
+
"""Render a single schema field."""
|
|
141
|
+
max_len = max_type_length or self.config.max_type_length
|
|
142
|
+
|
|
143
|
+
# Truncate type name if too long
|
|
144
|
+
type_name = field.type_name
|
|
145
|
+
if len(type_name) > max_len:
|
|
146
|
+
type_name = type_name[:max_len] + self.config.type_suffix
|
|
147
|
+
|
|
148
|
+
# Format field display
|
|
149
|
+
field_text = f'{field.name}: {type_name}'
|
|
150
|
+
|
|
151
|
+
# Render pydantic metadata
|
|
152
|
+
meta_parts = self._render_pydantic_meta_parts(field)
|
|
153
|
+
meta_html = self.template_renderer.render_template(
|
|
154
|
+
'html/pydantic_meta.j2',
|
|
155
|
+
meta_parts=meta_parts
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Render field text (with strikethrough if excluded)
|
|
159
|
+
text_html = self.template_renderer.render_template(
|
|
160
|
+
'html/colored_text.j2',
|
|
161
|
+
text=field_text,
|
|
162
|
+
color='#000', # Default color
|
|
163
|
+
strikethrough=field.is_exclude
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Combine field text and metadata
|
|
167
|
+
content = f'<font> {text_html} </font> {meta_html}'
|
|
168
|
+
|
|
169
|
+
# Render the table row
|
|
170
|
+
return self.template_renderer.render_template(
|
|
171
|
+
'html/schema_field_row.j2',
|
|
172
|
+
port=field.name,
|
|
173
|
+
align='left',
|
|
174
|
+
content=content
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def _get_filtered_fields(self, node: SchemaNode) -> list[FieldInfo]:
|
|
178
|
+
"""Get fields filtered by show_fields and show_pydantic_resolve_meta settings."""
|
|
179
|
+
|
|
180
|
+
# Filter fields based on pydantic-resolve meta setting
|
|
69
181
|
if self.show_pydantic_resolve_meta:
|
|
70
|
-
fields = [n for n in node.fields if n.has_pydantic_resolve_meta
|
|
182
|
+
fields = [n for n in node.fields if n.has_pydantic_resolve_meta or not n.from_base]
|
|
71
183
|
else:
|
|
72
|
-
fields = [n for n in node.fields if n.from_base
|
|
184
|
+
fields = [n for n in node.fields if not n.from_base]
|
|
73
185
|
|
|
186
|
+
# Further filter by show_fields setting
|
|
74
187
|
if self.show_fields == 'all':
|
|
75
|
-
|
|
188
|
+
return fields
|
|
76
189
|
elif self.show_fields == 'object':
|
|
77
190
|
if self.show_pydantic_resolve_meta:
|
|
78
|
-
#
|
|
79
|
-
|
|
191
|
+
# Show object fields or fields with pydantic-resolve metadata
|
|
192
|
+
return [f for f in fields if f.is_object or f.has_pydantic_resolve_meta]
|
|
80
193
|
else:
|
|
81
|
-
|
|
194
|
+
# Show only object fields
|
|
195
|
+
return [f for f in fields if f.is_object]
|
|
82
196
|
else: # 'single'
|
|
83
|
-
|
|
197
|
+
return []
|
|
84
198
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
199
|
+
def render_schema_label(self, node: SchemaNode, color: str | None = None) -> str:
|
|
200
|
+
"""
|
|
201
|
+
Render a schema node's label as an HTML table.
|
|
88
202
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
field_str = f"""<tr><td align="left" port="f{field.name}" cellpadding="8"><font> {display_xml} </font> {self.render_pydantic_related_markup(field)} </td></tr>"""
|
|
93
|
-
fields_parts.append(field_str)
|
|
203
|
+
TODO: Improve logic with show_pydantic_resolve_meta
|
|
204
|
+
"""
|
|
205
|
+
fields = self._get_filtered_fields(node)
|
|
94
206
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
207
|
+
# Render field rows
|
|
208
|
+
rows = []
|
|
209
|
+
has_base_fields = any(f.from_base for f in node.fields)
|
|
210
|
+
|
|
211
|
+
# Add inherited fields notice if needed
|
|
212
|
+
if self.show_fields == 'all' and has_base_fields:
|
|
213
|
+
notice = self.template_renderer.render_template(
|
|
214
|
+
'html/colored_text.j2',
|
|
215
|
+
text=' Inherited Fields ... ',
|
|
216
|
+
color=self.colors.text_gray
|
|
217
|
+
)
|
|
218
|
+
rows.append(
|
|
219
|
+
self.template_renderer.render_template(
|
|
220
|
+
'html/schema_field_row.j2',
|
|
221
|
+
content=notice,
|
|
222
|
+
align='left'
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Render each field
|
|
227
|
+
for field in fields:
|
|
228
|
+
rows.append(self._render_schema_field(field))
|
|
229
|
+
|
|
230
|
+
# Determine header color
|
|
231
|
+
default_color = self.colors.primary if color is None else color
|
|
232
|
+
header_color = self.colors.highlight if node.id == self.schema else default_color
|
|
233
|
+
|
|
234
|
+
# Render header
|
|
235
|
+
header = self.template_renderer.render_template(
|
|
236
|
+
'html/schema_header.j2',
|
|
237
|
+
text=node.name,
|
|
238
|
+
bg_color=header_color,
|
|
239
|
+
port=PK
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Render complete table
|
|
243
|
+
return self.template_renderer.render_template(
|
|
244
|
+
'html/schema_table.j2',
|
|
245
|
+
header=header,
|
|
246
|
+
rows=''.join(rows)
|
|
247
|
+
)
|
|
100
248
|
|
|
101
249
|
def _handle_schema_anchor(self, source: str) -> str:
|
|
250
|
+
"""Handle schema anchor for DOT links."""
|
|
102
251
|
if '::' in source:
|
|
103
252
|
a, b = source.split('::', 1)
|
|
104
253
|
return f'"{a}":{b}'
|
|
105
254
|
return f'"{source}"'
|
|
106
255
|
|
|
256
|
+
def _format_link_attributes(self, attrs: dict) -> str:
|
|
257
|
+
"""Format link attributes for DOT format."""
|
|
258
|
+
return ', '.join(f'{k}="{v}"' for k, v in attrs.items())
|
|
259
|
+
|
|
107
260
|
def render_link(self, link: Link) -> str:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
261
|
+
"""Render a link in DOT format."""
|
|
262
|
+
source = self._handle_schema_anchor(link.source)
|
|
263
|
+
target = self._handle_schema_anchor(link.target)
|
|
264
|
+
|
|
265
|
+
# Get link style attributes
|
|
266
|
+
attrs = self.style.get_link_attributes(link.type)
|
|
267
|
+
|
|
268
|
+
return self.template_renderer.render_template(
|
|
269
|
+
'dot/link.j2',
|
|
270
|
+
source=source,
|
|
271
|
+
target=target,
|
|
272
|
+
attributes=self._format_link_attributes(attrs)
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def render_schema_node(self, node: SchemaNode, color: str | None = None) -> str:
|
|
276
|
+
"""Render a schema node in DOT format."""
|
|
277
|
+
label = self.render_schema_label(node, color)
|
|
278
|
+
|
|
279
|
+
return self.template_renderer.render_template(
|
|
280
|
+
'dot/schema_node.j2',
|
|
281
|
+
id=node.id,
|
|
282
|
+
label=label,
|
|
283
|
+
margin=self.style.node_margin
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def render_tag_node(self, tag: Tag) -> str:
|
|
287
|
+
"""Render a tag node in DOT format."""
|
|
288
|
+
return self.template_renderer.render_template(
|
|
289
|
+
'dot/tag_node.j2',
|
|
290
|
+
id=tag.id,
|
|
291
|
+
name=tag.name,
|
|
292
|
+
margin=self.style.node_margin
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def render_route_node(self, route: Route) -> str:
|
|
296
|
+
"""Render a route node in DOT format."""
|
|
297
|
+
# Truncate response schema if too long
|
|
298
|
+
response_schema = route.response_schema
|
|
299
|
+
if len(response_schema) > self.config.max_type_length:
|
|
300
|
+
response_schema = response_schema[:self.config.max_type_length] + self.config.type_suffix
|
|
301
|
+
|
|
302
|
+
return self.template_renderer.render_template(
|
|
303
|
+
'dot/route_node.j2',
|
|
304
|
+
id=route.id,
|
|
305
|
+
name=route.name,
|
|
306
|
+
response_schema=response_schema,
|
|
307
|
+
margin=self.style.node_margin
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def _render_module_schema(
|
|
311
|
+
self,
|
|
312
|
+
mod: ModuleNode,
|
|
313
|
+
module_color_flag: set[str],
|
|
314
|
+
inherit_color: str | None = None,
|
|
315
|
+
show_cluster: bool = True
|
|
316
|
+
) -> str:
|
|
317
|
+
"""Render a module schema tree."""
|
|
318
|
+
color = inherit_color
|
|
319
|
+
cluster_color: str | None = None
|
|
320
|
+
|
|
321
|
+
# Check if this module has a custom color
|
|
322
|
+
for k in module_color_flag:
|
|
323
|
+
if mod.fullname.startswith(k):
|
|
324
|
+
module_color_flag.remove(k)
|
|
325
|
+
color = self.module_color[k]
|
|
326
|
+
cluster_color = color if color != inherit_color else None
|
|
327
|
+
break
|
|
328
|
+
|
|
329
|
+
# Render inner schema nodes
|
|
330
|
+
inner_nodes = [
|
|
331
|
+
self.render_schema_node(node, color)
|
|
332
|
+
for node in mod.schema_nodes
|
|
333
|
+
]
|
|
334
|
+
inner_nodes_str = '\n'.join(inner_nodes)
|
|
335
|
+
|
|
336
|
+
# Recursively render child modules
|
|
337
|
+
child_str = '\n'.join(
|
|
338
|
+
self._render_module_schema(
|
|
339
|
+
m,
|
|
340
|
+
module_color_flag=module_color_flag,
|
|
341
|
+
inherit_color=color,
|
|
342
|
+
show_cluster=show_cluster
|
|
343
|
+
)
|
|
344
|
+
for m in mod.modules
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
if show_cluster:
|
|
348
|
+
# Render as a cluster
|
|
349
|
+
cluster_id = f'module_{mod.fullname.replace(".", "_")}'
|
|
350
|
+
pen_style = ''
|
|
351
|
+
|
|
352
|
+
if cluster_color:
|
|
353
|
+
pen_style = f'pencolor = "{cluster_color}"'
|
|
354
|
+
pen_style += '\n' + f'penwidth = 3' if color else ''
|
|
355
|
+
else:
|
|
356
|
+
pen_style = 'pencolor="#ccc"'
|
|
357
|
+
|
|
358
|
+
return self.template_renderer.render_template(
|
|
359
|
+
'dot/cluster.j2',
|
|
360
|
+
cluster_id=cluster_id,
|
|
361
|
+
label=mod.name,
|
|
362
|
+
tooltip=mod.fullname,
|
|
363
|
+
border_color=self.colors.border,
|
|
364
|
+
pen_color=cluster_color,
|
|
365
|
+
pen_width=3 if color and not cluster_color else None,
|
|
366
|
+
content=f'{inner_nodes_str}\n{child_str}'
|
|
367
|
+
)
|
|
121
368
|
else:
|
|
122
|
-
|
|
369
|
+
# Render without cluster
|
|
370
|
+
return f'{inner_nodes_str}\n{child_str}'
|
|
123
371
|
|
|
124
372
|
def render_module_schema_content(self, nodes: list[SchemaNode]) -> str:
|
|
125
|
-
|
|
126
|
-
return f'''
|
|
127
|
-
"{node.id}" [
|
|
128
|
-
label = {self.render_schema_label(node, color)}
|
|
129
|
-
shape = "plain"
|
|
130
|
-
margin="0.5,0.1"
|
|
131
|
-
];'''
|
|
132
|
-
|
|
133
|
-
def render_module_schema(mod: ModuleNode, inherit_color: str | None=None, show_cluster:bool=True) -> str:
|
|
134
|
-
color: str | None = inherit_color
|
|
135
|
-
cluster_color: str | None = None
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
# recursively vist module from short to long: 'a', 'a.b', 'a.b.c'
|
|
139
|
-
# color_flag: {'a', 'a.b.c'}
|
|
140
|
-
# at first 'a', match 'a' -> color, remove 'a' from color_flag
|
|
141
|
-
# at 'a.b', no match
|
|
142
|
-
# at 'a.b.c', match 'a.b.c' -> color, remove 'a.b.c' from color_flag
|
|
143
|
-
for k in module_color_flag:
|
|
144
|
-
if mod.fullname.startswith(k):
|
|
145
|
-
module_color_flag.remove(k)
|
|
146
|
-
color = self.module_color[k]
|
|
147
|
-
cluster_color = color if color != inherit_color else None
|
|
148
|
-
break
|
|
149
|
-
|
|
150
|
-
inner_nodes = [ render_node(node, color) for node in mod.schema_nodes ]
|
|
151
|
-
inner_nodes_str = '\n'.join(inner_nodes)
|
|
152
|
-
child_str = '\n'.join(render_module_schema(mod=m, inherit_color=color, show_cluster=show_cluster) for m in mod.modules)
|
|
153
|
-
|
|
154
|
-
if show_cluster:
|
|
155
|
-
return f'''
|
|
156
|
-
subgraph cluster_module_{mod.fullname.replace('.', '_')} {{
|
|
157
|
-
tooltip="{mod.fullname}"
|
|
158
|
-
color = "#666"
|
|
159
|
-
style="rounded"
|
|
160
|
-
label = " {mod.name}"
|
|
161
|
-
labeljust = "l"
|
|
162
|
-
{(f'pencolor = "{cluster_color}"' if cluster_color else 'pencolor="#ccc"')}
|
|
163
|
-
{('penwidth = 3' if color else 'penwidth=""')}
|
|
164
|
-
{inner_nodes_str}
|
|
165
|
-
{child_str}
|
|
166
|
-
}}'''
|
|
167
|
-
else:
|
|
168
|
-
return f'''
|
|
169
|
-
{inner_nodes_str}
|
|
170
|
-
{child_str}
|
|
171
|
-
'''
|
|
172
|
-
|
|
173
|
-
# if self.show_module:
|
|
373
|
+
"""Render all module schemas."""
|
|
174
374
|
module_schemas = build_module_schema_tree(nodes)
|
|
175
375
|
module_color_flag = set(self.module_color.keys())
|
|
176
|
-
return '\n'.join(render_module_schema(mod=m, show_cluster=self.show_module) for m in module_schemas)
|
|
177
376
|
|
|
178
|
-
|
|
377
|
+
return '\n'.join(
|
|
378
|
+
self._render_module_schema(
|
|
379
|
+
m,
|
|
380
|
+
module_color_flag=module_color_flag,
|
|
381
|
+
show_cluster=self.show_module
|
|
382
|
+
)
|
|
383
|
+
for m in module_schemas
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def _render_module_route(self, mod: ModuleRoute, show_cluster: bool = True) -> str:
|
|
387
|
+
"""Render a module route tree."""
|
|
388
|
+
# Render inner route nodes
|
|
389
|
+
inner_nodes = [self.render_route_node(r) for r in mod.routes]
|
|
390
|
+
inner_nodes_str = '\n'.join(inner_nodes)
|
|
391
|
+
|
|
392
|
+
# Recursively render child modules
|
|
393
|
+
child_str = '\n'.join(
|
|
394
|
+
self._render_module_route(m, show_cluster=show_cluster)
|
|
395
|
+
for m in mod.modules
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
if show_cluster:
|
|
399
|
+
cluster_id = f'route_module_{mod.fullname.replace(".", "_")}'
|
|
400
|
+
|
|
401
|
+
return self.template_renderer.render_template(
|
|
402
|
+
'dot/cluster.j2',
|
|
403
|
+
cluster_id=cluster_id,
|
|
404
|
+
label=mod.name,
|
|
405
|
+
tooltip=mod.fullname,
|
|
406
|
+
border_color=self.colors.border,
|
|
407
|
+
pen_color=None,
|
|
408
|
+
pen_width=None,
|
|
409
|
+
content=f'{inner_nodes_str}\n{child_str}'
|
|
410
|
+
)
|
|
411
|
+
else:
|
|
412
|
+
return f'{inner_nodes_str}\n{child_str}'
|
|
413
|
+
|
|
179
414
|
def render_module_route_content(self, routes: list[Route]) -> str:
|
|
180
|
-
|
|
181
|
-
response_schema = route.response_schema[:25] + '..' if len(route.response_schema) > 25 else route.response_schema
|
|
182
|
-
return f'''
|
|
183
|
-
"{route.id}" [
|
|
184
|
-
label = " {route.name} | {response_schema} "
|
|
185
|
-
margin="0.5,0.1"
|
|
186
|
-
shape = "record"
|
|
187
|
-
];'''
|
|
188
|
-
|
|
189
|
-
def render_module_route(mod: ModuleRoute, show_cluster: bool=True) -> str:
|
|
190
|
-
# Inner route nodes, same style as flat route_str
|
|
191
|
-
inner_nodes = [
|
|
192
|
-
render_route(r) for r in mod.routes
|
|
193
|
-
]
|
|
194
|
-
inner_nodes_str = '\n'.join(inner_nodes)
|
|
195
|
-
child_str = '\n'.join(render_module_route(m, show_cluster=show_cluster) for m in mod.modules)
|
|
196
|
-
if show_cluster:
|
|
197
|
-
return f'''
|
|
198
|
-
subgraph cluster_route_module_{mod.fullname.replace('.', '_')} {{
|
|
199
|
-
tooltip="{mod.fullname}"
|
|
200
|
-
color = "#666"
|
|
201
|
-
style="rounded"
|
|
202
|
-
label = " {mod.name}"
|
|
203
|
-
labeljust = "l"
|
|
204
|
-
{inner_nodes_str}
|
|
205
|
-
{child_str}
|
|
206
|
-
}}'''
|
|
207
|
-
else:
|
|
208
|
-
return f'''
|
|
209
|
-
{inner_nodes_str}
|
|
210
|
-
{child_str}
|
|
211
|
-
'''
|
|
415
|
+
"""Render all module routes."""
|
|
212
416
|
module_routes = build_module_route_tree(routes)
|
|
213
|
-
module_routes_str = '\n'.join(render_module_route(m, show_cluster=self.show_module) for m in module_routes)
|
|
214
|
-
return module_routes_str
|
|
215
417
|
|
|
418
|
+
return '\n'.join(
|
|
419
|
+
self._render_module_route(m, show_cluster=self.show_module)
|
|
420
|
+
for m in module_routes
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
def _render_cluster_container(
|
|
424
|
+
self,
|
|
425
|
+
name: str,
|
|
426
|
+
label: str,
|
|
427
|
+
content: str,
|
|
428
|
+
fontsize: str | None = None
|
|
429
|
+
) -> str:
|
|
430
|
+
"""Render a cluster container (for tags, routes, schemas)."""
|
|
431
|
+
return self.template_renderer.render_template(
|
|
432
|
+
'dot/cluster_container.j2',
|
|
433
|
+
name=name,
|
|
434
|
+
label=label,
|
|
435
|
+
content=content,
|
|
436
|
+
border_color=self.colors.border,
|
|
437
|
+
margin=self.style.cluster_margin,
|
|
438
|
+
fontsize=fontsize or self.style.cluster_fontsize
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def render_dot(
|
|
442
|
+
self,
|
|
443
|
+
tags: list[Tag],
|
|
444
|
+
routes: list[Route],
|
|
445
|
+
nodes: list[SchemaNode],
|
|
446
|
+
links: list[Link],
|
|
447
|
+
spline_line: bool = False
|
|
448
|
+
) -> str:
|
|
449
|
+
"""
|
|
450
|
+
Render the complete DOT graph.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
tags: List of tags
|
|
454
|
+
routes: List of routes
|
|
455
|
+
nodes: List of schema nodes
|
|
456
|
+
links: List of links
|
|
457
|
+
spline_line: Whether to use spline lines
|
|
216
458
|
|
|
217
|
-
|
|
459
|
+
Returns:
|
|
460
|
+
Complete DOT graph as a string
|
|
461
|
+
"""
|
|
462
|
+
# Render tag nodes
|
|
463
|
+
tag_str = '\n'.join(self.render_tag_node(t) for t in tags)
|
|
218
464
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
];''' for t in tags
|
|
226
|
-
])
|
|
465
|
+
# Render tags cluster
|
|
466
|
+
tags_cluster = self._render_cluster_container(
|
|
467
|
+
name='tags',
|
|
468
|
+
label='Tags',
|
|
469
|
+
content=tag_str
|
|
470
|
+
)
|
|
227
471
|
|
|
472
|
+
# Render routes cluster
|
|
228
473
|
module_routes_str = self.render_module_route_content(routes)
|
|
474
|
+
routes_cluster = self._render_cluster_container(
|
|
475
|
+
name='router',
|
|
476
|
+
label='Routes',
|
|
477
|
+
content=module_routes_str
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# Render schemas cluster
|
|
229
481
|
module_schemas_str = self.render_module_schema_content(nodes)
|
|
482
|
+
schemas_cluster = self._render_cluster_container(
|
|
483
|
+
name='schema',
|
|
484
|
+
label='Schema',
|
|
485
|
+
content=module_schemas_str
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
# Render links
|
|
230
489
|
link_str = '\n'.join(self.render_link(link) for link in links)
|
|
231
490
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
];
|
|
246
|
-
node [
|
|
247
|
-
fontsize = "16"
|
|
248
|
-
];
|
|
249
|
-
|
|
250
|
-
subgraph cluster_tags {{
|
|
251
|
-
color = "#aaa"
|
|
252
|
-
margin=18
|
|
253
|
-
style="dashed"
|
|
254
|
-
label = " Tags"
|
|
255
|
-
labeljust = "l"
|
|
256
|
-
fontsize = "20"
|
|
257
|
-
{tag_str}
|
|
258
|
-
}}
|
|
259
|
-
|
|
260
|
-
subgraph cluster_router {{
|
|
261
|
-
color = "#aaa"
|
|
262
|
-
margin=18
|
|
263
|
-
style="dashed"
|
|
264
|
-
label = " Routes"
|
|
265
|
-
labeljust = "l"
|
|
266
|
-
fontsize = "20"
|
|
267
|
-
{module_routes_str}
|
|
268
|
-
}}
|
|
269
|
-
|
|
270
|
-
subgraph cluster_schema {{
|
|
271
|
-
color = "#aaa"
|
|
272
|
-
margin=18
|
|
273
|
-
style="dashed"
|
|
274
|
-
label=" Schema"
|
|
275
|
-
labeljust="l"
|
|
276
|
-
fontsize="20"
|
|
277
|
-
{module_schemas_str}
|
|
278
|
-
}}
|
|
279
|
-
|
|
280
|
-
{link_str}
|
|
281
|
-
}}
|
|
282
|
-
'''
|
|
283
|
-
return dot_str
|
|
491
|
+
# Render complete digraph
|
|
492
|
+
return self.template_renderer.render_template(
|
|
493
|
+
'dot/digraph.j2',
|
|
494
|
+
pad=self.style.pad,
|
|
495
|
+
nodesep=self.style.nodesep,
|
|
496
|
+
spline='line' if spline_line else '',
|
|
497
|
+
font=self.style.font,
|
|
498
|
+
node_fontsize=self.style.node_fontsize,
|
|
499
|
+
tags_cluster=tags_cluster,
|
|
500
|
+
routes_cluster=routes_cluster,
|
|
501
|
+
schemas_cluster=schemas_cluster,
|
|
502
|
+
links=link_str
|
|
503
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Style constants and configuration for rendering DOT graphs and HTML tables.
|
|
3
|
+
"""
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ColorScheme:
|
|
10
|
+
"""Color scheme for graph visualization."""
|
|
11
|
+
|
|
12
|
+
# Node colors
|
|
13
|
+
primary: str = '#009485'
|
|
14
|
+
highlight: str = 'tomato'
|
|
15
|
+
|
|
16
|
+
# Pydantic-resolve metadata colors
|
|
17
|
+
resolve: str = '#47a80f'
|
|
18
|
+
post: str = '#427fa4'
|
|
19
|
+
expose_as: str = '#895cb9'
|
|
20
|
+
send_to: str = '#ca6d6d'
|
|
21
|
+
collector: str = '#777'
|
|
22
|
+
|
|
23
|
+
# Link colors
|
|
24
|
+
inherit: str = 'purple'
|
|
25
|
+
subset: str = 'orange'
|
|
26
|
+
|
|
27
|
+
# Border colors
|
|
28
|
+
border: str = '#666'
|
|
29
|
+
cluster_border: str = '#ccc'
|
|
30
|
+
|
|
31
|
+
# Text colors
|
|
32
|
+
text_gray: str = '#999'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class GraphvizStyle:
|
|
37
|
+
"""Graphviz DOT style configuration."""
|
|
38
|
+
|
|
39
|
+
# Font settings
|
|
40
|
+
font: str = 'Helvetica,Arial,sans-serif'
|
|
41
|
+
node_fontsize: str = '16'
|
|
42
|
+
cluster_fontsize: str = '20'
|
|
43
|
+
|
|
44
|
+
# Layout settings
|
|
45
|
+
nodesep: str = '0.8'
|
|
46
|
+
pad: str = '0.5'
|
|
47
|
+
node_margin: str = '0.5,0.1'
|
|
48
|
+
cluster_margin: str = '18'
|
|
49
|
+
|
|
50
|
+
# Link styles configuration
|
|
51
|
+
LINK_STYLES: dict[str, dict] = field(default_factory=lambda: {
|
|
52
|
+
'tag_route': {
|
|
53
|
+
'style': 'solid',
|
|
54
|
+
'minlen': 3,
|
|
55
|
+
},
|
|
56
|
+
'route_to_schema': {
|
|
57
|
+
'style': 'solid',
|
|
58
|
+
'dir': 'back',
|
|
59
|
+
'arrowtail': 'odot',
|
|
60
|
+
'minlen': 3,
|
|
61
|
+
},
|
|
62
|
+
'schema': {
|
|
63
|
+
'style': 'solid',
|
|
64
|
+
'label': '',
|
|
65
|
+
'dir': 'back',
|
|
66
|
+
'minlen': 3,
|
|
67
|
+
'arrowtail': 'odot',
|
|
68
|
+
},
|
|
69
|
+
'parent': {
|
|
70
|
+
'style': 'solid,dashed',
|
|
71
|
+
'dir': 'back',
|
|
72
|
+
'minlen': 3,
|
|
73
|
+
'taillabel': '< inherit >',
|
|
74
|
+
'color': 'purple',
|
|
75
|
+
'tailport': 'n',
|
|
76
|
+
},
|
|
77
|
+
'subset': {
|
|
78
|
+
'style': 'solid,dashed',
|
|
79
|
+
'dir': 'back',
|
|
80
|
+
'minlen': 3,
|
|
81
|
+
'taillabel': '< subset >',
|
|
82
|
+
'color': 'orange',
|
|
83
|
+
'tailport': 'n',
|
|
84
|
+
},
|
|
85
|
+
'tag_to_schema': {
|
|
86
|
+
'style': 'solid',
|
|
87
|
+
'minlen': 3,
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
def get_link_attributes(self, link_type: str) -> dict:
|
|
92
|
+
"""Get link style attributes for a given link type."""
|
|
93
|
+
return self.LINK_STYLES.get(link_type, {})
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class RenderConfig:
|
|
98
|
+
"""Complete rendering configuration."""
|
|
99
|
+
|
|
100
|
+
colors: ColorScheme = field(default_factory=ColorScheme)
|
|
101
|
+
style: GraphvizStyle = field(default_factory=GraphvizStyle)
|
|
102
|
+
|
|
103
|
+
# Field display settings
|
|
104
|
+
max_type_length: int = 25
|
|
105
|
+
type_suffix: str = '..'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
subgraph cluster_{{ cluster_id }} {
|
|
2
|
+
tooltip="{{ tooltip }}"
|
|
3
|
+
color = "{{ border_color }}"
|
|
4
|
+
style="rounded"
|
|
5
|
+
label = " {{ label }}"
|
|
6
|
+
labeljust = "l"
|
|
7
|
+
{% if pen_color %}pencolor = "{{ pen_color }}"{% endif %}
|
|
8
|
+
{% if pen_width %}penwidth = {{ pen_width }}{% endif %}
|
|
9
|
+
{{ content }}
|
|
10
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
digraph world {
|
|
2
|
+
pad="{{ pad }}"
|
|
3
|
+
nodesep={{ nodesep }}
|
|
4
|
+
{% if spline %}splines={{ spline }}{% endif %}
|
|
5
|
+
fontname="{{ font }}"
|
|
6
|
+
node [fontname="{{ font }}"]
|
|
7
|
+
edge [
|
|
8
|
+
fontname="{{ font }}"
|
|
9
|
+
color="gray"
|
|
10
|
+
]
|
|
11
|
+
graph [
|
|
12
|
+
rankdir = "LR"
|
|
13
|
+
];
|
|
14
|
+
node [
|
|
15
|
+
fontsize = {{ node_fontsize }}
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
{{ tags_cluster }}
|
|
19
|
+
|
|
20
|
+
{{ routes_cluster }}
|
|
21
|
+
|
|
22
|
+
{{ schemas_cluster }}
|
|
23
|
+
|
|
24
|
+
{{ links }}
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{ source }}:e -> {{ target }}:w [{{ attributes }}];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<font color="{{ color }}">{% if strikethrough %}<s>{{ text }}</s>{% else %}{{ text }}{% endif %}</font>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{% if meta_parts %}<br align="left"/><br align="left"/>{{ meta_parts | join('<br align="left"/>') }}<br align="left"/>{% endif %}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<tr><td align="{{ align }}" {% if port %}port="f{{ port }}"{% endif %} cellpadding="8">{{ content }}</td></tr>
|
fastapi_voyager/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.15.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-voyager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
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
|
|
@@ -19,7 +19,8 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
21
|
Requires-Dist: fastapi>=0.110
|
|
22
|
-
Requires-Dist:
|
|
22
|
+
Requires-Dist: jinja2>=3.0.0
|
|
23
|
+
Requires-Dist: pydantic-resolve>=2.4.3
|
|
23
24
|
Provides-Extra: dev
|
|
24
25
|
Requires-Dist: pytest; extra == 'dev'
|
|
25
26
|
Requires-Dist: ruff; extra == 'dev'
|
|
@@ -4,12 +4,25 @@ fastapi_voyager/er_diagram.py,sha256=cMiNKk4ufSM147ldvvfdqfv34Q5mj533VcELsp4Gwwc
|
|
|
4
4
|
fastapi_voyager/filter.py,sha256=AN_HIu8-DtKisIq5mFt7CnqRHtxKewedNGyyaI82hSY,11529
|
|
5
5
|
fastapi_voyager/module.py,sha256=h9YR3BpS-CAcJW9WCdVkF4opqwY32w9T67g9GfdLytk,3425
|
|
6
6
|
fastapi_voyager/pydantic_resolve_util.py,sha256=r4Rq7BtBcFOMV7O2Ab9TwLyRNL1yNDiQlGUVybf-sXs,3524
|
|
7
|
-
fastapi_voyager/render.py,sha256=
|
|
7
|
+
fastapi_voyager/render.py,sha256=pk8cxPJXgKQo_ZIjZnUEYUhA3bg0bO2D1VMupkRhyFg,16837
|
|
8
|
+
fastapi_voyager/render_style.py,sha256=mPOuChEl71-3agCbPwkMt2sFmax2AEKDI6dK90eFPRc,2552
|
|
8
9
|
fastapi_voyager/server.py,sha256=DN4KP37lD_lt4ISg7yoJwzVvw3nKn6qr9C6aH75mR-g,8928
|
|
9
10
|
fastapi_voyager/type.py,sha256=zluWvh5vpnjXJ9aAmyNJTSmXZPjAHCvgRT5oQRAjHrg,2104
|
|
10
11
|
fastapi_voyager/type_helper.py,sha256=FmfrZAI3Z4uDdh3sH_kH7UGoY6yNVPapneSN86qY_wo,10209
|
|
11
|
-
fastapi_voyager/version.py,sha256=
|
|
12
|
+
fastapi_voyager/version.py,sha256=v16QMf21an7W4R5BTOYtQb6D8lpmZbl1S2DlRw2aFVs,49
|
|
12
13
|
fastapi_voyager/voyager.py,sha256=4vonmL-xt54C5San-DRBq4mjoV8Q96eoWRy68MJ1IJw,14169
|
|
14
|
+
fastapi_voyager/templates/dot/cluster.j2,sha256=I2z9KkfCzmAtqXe0gXBnxnOfBXUSpdlATs3uf-O8_B8,307
|
|
15
|
+
fastapi_voyager/templates/dot/cluster_container.j2,sha256=2tH1mOJvPoVKE_aHVMR3t06TfH_dYa9OeH6DBqSHt_A,204
|
|
16
|
+
fastapi_voyager/templates/dot/digraph.j2,sha256=wZuiO-vvZ-AJ1FcMQG4BLevUyxk6yA-yEpUa3Us05mE,435
|
|
17
|
+
fastapi_voyager/templates/dot/link.j2,sha256=9XSbqlJXEcqY8r_gXJLqnVl5N_VxOXUfLGaYs3X3qw0,53
|
|
18
|
+
fastapi_voyager/templates/dot/route_node.j2,sha256=HBI-4HE7tZTCt5ST2aEHp9ntOebFniw5xqxXMRFtDNI,120
|
|
19
|
+
fastapi_voyager/templates/dot/schema_node.j2,sha256=xusd_TgO-eeBBd7d0vFMhCq3HmqRx2mOQNHBYA8ARmY,86
|
|
20
|
+
fastapi_voyager/templates/dot/tag_node.j2,sha256=bzqRXpYYAv_PEE0nDfZgczPJEAl-fGflFGkRQ7ncvzc,96
|
|
21
|
+
fastapi_voyager/templates/html/colored_text.j2,sha256=XgRFWKwY0AI6okEFPyOKGEWYz7gSaRRZ8It3qmmRq84,104
|
|
22
|
+
fastapi_voyager/templates/html/pydantic_meta.j2,sha256=_tsSqjucs_QrAlPIVRy9u6I2-lMVdIXb0AkMM6TEJkE,130
|
|
23
|
+
fastapi_voyager/templates/html/schema_field_row.j2,sha256=KfKexHO_QJV-OIJS0eiY_7fqA8031fWpD2g2wTv4BuE,111
|
|
24
|
+
fastapi_voyager/templates/html/schema_header.j2,sha256=9WpuHLy3Zbv5GHG08qqaj5Xf-gaR-79ErBYuANZp7iA,179
|
|
25
|
+
fastapi_voyager/templates/html/schema_table.j2,sha256=rzphiGk1il7uv4Gr2p_HLPHqyLZk63vLrGAmIduTdSE,117
|
|
13
26
|
fastapi_voyager/web/graph-ui.js,sha256=DlSRHoTCpWMS6EQsW8naLr8yRn6ofF6wIUL-OsvEjvs,6480
|
|
14
27
|
fastapi_voyager/web/graphviz.svg.css,sha256=zDCjjpT0Idufu5YOiZI76PL70-avP3vTyzGPh9M85Do,1563
|
|
15
28
|
fastapi_voyager/web/graphviz.svg.js,sha256=wZwz_lBztoXmujEN21P0w-HMpdmbqPwTQQ6Ebxd9rGo,18569
|
|
@@ -29,8 +42,8 @@ fastapi_voyager/web/icon/favicon-16x16.png,sha256=JC07jEzfIYxBIoQn_FHXvyHuxESdhW
|
|
|
29
42
|
fastapi_voyager/web/icon/favicon-32x32.png,sha256=C7v1h58cfWOsiLp9yOIZtlx-dLasBcq3NqpHVGRmpt4,1859
|
|
30
43
|
fastapi_voyager/web/icon/favicon.ico,sha256=tZolYIXkkBcFiYl1A8ksaXN2VjGamzcSdes838dLvNc,15406
|
|
31
44
|
fastapi_voyager/web/icon/site.webmanifest,sha256=ep4Hzh9zhmiZF2At3Fp1dQrYQuYF_3ZPZxc1KcGBvwQ,263
|
|
32
|
-
fastapi_voyager-0.
|
|
33
|
-
fastapi_voyager-0.
|
|
34
|
-
fastapi_voyager-0.
|
|
35
|
-
fastapi_voyager-0.
|
|
36
|
-
fastapi_voyager-0.
|
|
45
|
+
fastapi_voyager-0.15.0.dist-info/METADATA,sha256=tHdt2irJCbL27F6cA7P4O2U8XKA-zxOT9jaf-gwl7Mk,8213
|
|
46
|
+
fastapi_voyager-0.15.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
47
|
+
fastapi_voyager-0.15.0.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
|
|
48
|
+
fastapi_voyager-0.15.0.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
|
|
49
|
+
fastapi_voyager-0.15.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|