fastapi-voyager 0.4.3__py3-none-any.whl → 0.4.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/graph.py +18 -14
- fastapi_voyager/module.py +20 -0
- fastapi_voyager/type.py +20 -11
- fastapi_voyager/type_helper.py +46 -55
- fastapi_voyager/version.py +1 -1
- {fastapi_voyager-0.4.3.dist-info → fastapi_voyager-0.4.5.dist-info}/METADATA +18 -6
- {fastapi_voyager-0.4.3.dist-info → fastapi_voyager-0.4.5.dist-info}/RECORD +10 -10
- {fastapi_voyager-0.4.3.dist-info → fastapi_voyager-0.4.5.dist-info}/WHEEL +0 -0
- {fastapi_voyager-0.4.3.dist-info → fastapi_voyager-0.4.5.dist-info}/entry_points.txt +0 -0
- {fastapi_voyager-0.4.3.dist-info → fastapi_voyager-0.4.5.dist-info}/licenses/LICENSE +0 -0
fastapi_voyager/graph.py
CHANGED
|
@@ -12,7 +12,7 @@ from fastapi_voyager.type_helper import (
|
|
|
12
12
|
update_forward_refs
|
|
13
13
|
)
|
|
14
14
|
from pydantic import BaseModel
|
|
15
|
-
from fastapi_voyager.type import Route, SchemaNode, Link, Tag, ModuleNode
|
|
15
|
+
from fastapi_voyager.type import Route, SchemaNode, Link, Tag, ModuleNode, LinkType
|
|
16
16
|
from fastapi_voyager.module import build_module_tree
|
|
17
17
|
from fastapi_voyager.filter import filter_graph
|
|
18
18
|
|
|
@@ -156,7 +156,8 @@ class Analytics:
|
|
|
156
156
|
source_origin: str,
|
|
157
157
|
target: str,
|
|
158
158
|
target_origin: str,
|
|
159
|
-
type:
|
|
159
|
+
type: LinkType
|
|
160
|
+
) -> bool:
|
|
160
161
|
"""
|
|
161
162
|
1. add link to link_set
|
|
162
163
|
2. if duplicated, do nothing, else insert
|
|
@@ -262,17 +263,22 @@ class Analytics:
|
|
|
262
263
|
|
|
263
264
|
def generate_dot(self):
|
|
264
265
|
|
|
266
|
+
def generate_link(link: Link):
|
|
267
|
+
if link.type == 'internal':
|
|
268
|
+
return f'''{handle_entry(link.source)}:e -> {handle_entry(link.target)}:w [ {get_link_attributes(link)} ];'''
|
|
269
|
+
else:
|
|
270
|
+
return f'''{handle_entry(link.source)} -> {handle_entry(link.target)} [ {get_link_attributes(link)} ];'''
|
|
271
|
+
|
|
272
|
+
|
|
265
273
|
def get_link_attributes(link: Link):
|
|
266
|
-
if link.type == '
|
|
267
|
-
return 'style = "dashed",
|
|
268
|
-
elif link.type == 'parent':
|
|
269
|
-
return 'style = "solid", dir="back", minlen=3, taillabel = "< inherit >", color = "purple", tailport="n"'
|
|
274
|
+
if link.type == 'parent':
|
|
275
|
+
return 'style = "solid, dashed", dir="back", minlen=3, taillabel = "< inherit >", color = "purple", tailport="n"'
|
|
270
276
|
elif link.type == 'entry':
|
|
271
|
-
return 'style = "solid", label = "", minlen=3'
|
|
277
|
+
return 'style = "solid", label = "", minlen=3, tailport="e", headport="w"'
|
|
272
278
|
elif link.type == 'subset':
|
|
273
|
-
return 'style = "solid", dir="back", minlen=3, taillabel = "< subset >", color = "orange", tailport="n"'
|
|
279
|
+
return 'style = "solid, dashed", dir="back", minlen=3, taillabel = "< subset >", color = "orange", tailport="n"'
|
|
274
280
|
|
|
275
|
-
return 'style = "solid", arrowtail="
|
|
281
|
+
return 'style = "solid", arrowtail="odot", dir="back", minlen=3'
|
|
276
282
|
|
|
277
283
|
def render_module(mod: ModuleNode):
|
|
278
284
|
color = self.module_color.get(mod.fullname)
|
|
@@ -296,7 +302,7 @@ class Analytics:
|
|
|
296
302
|
style="rounded"
|
|
297
303
|
label = " {mod.name}"
|
|
298
304
|
labeljust = "l"
|
|
299
|
-
{(f'pencolor = "{color}"' if color else 'pencolor=""')}
|
|
305
|
+
{(f'pencolor = "{color}"' if color else 'pencolor="#ccc"')}
|
|
300
306
|
{(f'penwidth = 3' if color else 'penwidth=""')}
|
|
301
307
|
{inner_nodes_str}
|
|
302
308
|
{child_str}
|
|
@@ -340,9 +346,7 @@ class Analytics:
|
|
|
340
346
|
|
|
341
347
|
modules_str = '\n'.join(render_module(m) for m in _modules)
|
|
342
348
|
|
|
343
|
-
links = [
|
|
344
|
-
f'''{handle_entry(link.source)} -> {handle_entry(link.target)} [ {get_link_attributes(link)} ];''' for link in _links
|
|
345
|
-
]
|
|
349
|
+
links = [ generate_link(link) for link in _links ]
|
|
346
350
|
link_str = '\n'.join(links)
|
|
347
351
|
|
|
348
352
|
template = f'''
|
|
@@ -376,7 +380,7 @@ class Analytics:
|
|
|
376
380
|
color = "#aaa"
|
|
377
381
|
margin=18
|
|
378
382
|
style="dashed"
|
|
379
|
-
label = "
|
|
383
|
+
label = " Routes"
|
|
380
384
|
labeljust = "l"
|
|
381
385
|
fontsize = "20"
|
|
382
386
|
{route_str}
|
fastapi_voyager/module.py
CHANGED
|
@@ -41,4 +41,24 @@ def build_module_tree(schema_nodes: list[SchemaNode]) -> list[ModuleNode]:
|
|
|
41
41
|
result = list(top_modules.values())
|
|
42
42
|
if root_level_nodes:
|
|
43
43
|
result.append(ModuleNode(name="__root__", fullname="__root__", schema_nodes=root_level_nodes, modules=[]))
|
|
44
|
+
|
|
45
|
+
# Collapse pass: if a module has exactly one child module and no schema_nodes,
|
|
46
|
+
# merge it upward (A + B -> A.B). Repeat until fixed point.
|
|
47
|
+
def collapse(node: ModuleNode):
|
|
48
|
+
# Collapse chains at current node
|
|
49
|
+
while len(node.modules) == 1 and len(node.schema_nodes) == 0:
|
|
50
|
+
child = node.modules[0]
|
|
51
|
+
# Merge child's identity into current node
|
|
52
|
+
node.name = f"{node.name}.{child.name}"
|
|
53
|
+
# Prefer child's fullname which already reflects full path
|
|
54
|
+
node.fullname = child.fullname
|
|
55
|
+
node.schema_nodes = child.schema_nodes
|
|
56
|
+
node.modules = child.modules
|
|
57
|
+
# Recurse into children
|
|
58
|
+
for m in node.modules:
|
|
59
|
+
collapse(m)
|
|
60
|
+
|
|
61
|
+
for top in result:
|
|
62
|
+
collapse(top)
|
|
63
|
+
|
|
44
64
|
return result
|
fastapi_voyager/type.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
2
|
from typing import Literal
|
|
3
3
|
|
|
4
|
+
@dataclass
|
|
5
|
+
class NodeBase:
|
|
6
|
+
id: str
|
|
7
|
+
name: str
|
|
4
8
|
|
|
5
9
|
@dataclass
|
|
6
10
|
class FieldInfo:
|
|
@@ -11,23 +15,17 @@ class FieldInfo:
|
|
|
11
15
|
is_exclude: bool = False
|
|
12
16
|
|
|
13
17
|
@dataclass
|
|
14
|
-
class Tag:
|
|
15
|
-
id: str
|
|
16
|
-
name: str
|
|
18
|
+
class Tag(NodeBase):
|
|
17
19
|
routes: list['Route'] # route.id
|
|
18
20
|
|
|
19
21
|
@dataclass
|
|
20
|
-
class Route:
|
|
21
|
-
id: str
|
|
22
|
-
name: str
|
|
22
|
+
class Route(NodeBase):
|
|
23
23
|
source_code: str = ''
|
|
24
24
|
vscode_link: str = '' # optional vscode deep link
|
|
25
25
|
|
|
26
26
|
@dataclass
|
|
27
|
-
class SchemaNode:
|
|
28
|
-
id: str
|
|
27
|
+
class SchemaNode(NodeBase):
|
|
29
28
|
module: str
|
|
30
|
-
name: str
|
|
31
29
|
source_code: str = '' # optional for tests / backward compatibility
|
|
32
30
|
vscode_link: str = '' # optional vscode deep link
|
|
33
31
|
fields: list[FieldInfo] = field(default_factory=list)
|
|
@@ -39,10 +37,21 @@ class ModuleNode:
|
|
|
39
37
|
schema_nodes: list[SchemaNode]
|
|
40
38
|
modules: list['ModuleNode']
|
|
41
39
|
|
|
40
|
+
|
|
41
|
+
# type:
|
|
42
|
+
# - entry: tag -> route, route -> response model
|
|
43
|
+
# - subset: schema -> schema (subset)
|
|
44
|
+
# - parent: schema -> schema (inheritance)
|
|
45
|
+
# - internal: schema -> schema (field reference)
|
|
46
|
+
LinkType = Literal['internal', 'parent', 'entry', 'subset']
|
|
47
|
+
|
|
42
48
|
@dataclass
|
|
43
49
|
class Link:
|
|
50
|
+
# node + field level links
|
|
44
51
|
source: str
|
|
45
|
-
source_origin: str # internal relationship
|
|
46
52
|
target: str
|
|
53
|
+
|
|
54
|
+
# node level links
|
|
55
|
+
source_origin: str
|
|
47
56
|
target_origin: str
|
|
48
|
-
type:
|
|
57
|
+
type: LinkType
|
fastapi_voyager/type_helper.py
CHANGED
|
@@ -15,24 +15,10 @@ except Exception: # pragma: no cover
|
|
|
15
15
|
TypeAliasType = _DummyTypeAliasType # type: ignore
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def
|
|
19
|
-
origin = get_origin(annotation)
|
|
20
|
-
args = get_args(annotation)
|
|
21
|
-
if origin is Union and type(None) in args:
|
|
22
|
-
return True
|
|
23
|
-
return False
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _is_list(annotation):
|
|
18
|
+
def is_list(annotation):
|
|
27
19
|
return getattr(annotation, "__origin__", None) == list
|
|
28
20
|
|
|
29
21
|
|
|
30
|
-
def shelling_type(type):
|
|
31
|
-
while _is_optional(type) or _is_list(type):
|
|
32
|
-
type = type.__args__[0]
|
|
33
|
-
return type
|
|
34
|
-
|
|
35
|
-
|
|
36
22
|
def full_class_name(cls):
|
|
37
23
|
return f"{cls.__module__}.{cls.__qualname__}"
|
|
38
24
|
|
|
@@ -42,12 +28,9 @@ def get_core_types(tp):
|
|
|
42
28
|
- get the core type
|
|
43
29
|
- always return a tuple of core types
|
|
44
30
|
"""
|
|
45
|
-
|
|
46
|
-
return tuple()
|
|
47
|
-
|
|
48
|
-
# Unwrap PEP 695 type aliases (they wrap the actual annotation in __value__)
|
|
49
|
-
# Repeat in case of nested aliasing.
|
|
31
|
+
# Helpers
|
|
50
32
|
def _unwrap_alias(t):
|
|
33
|
+
"""Unwrap PEP 695 type aliases by following __value__ repeatedly."""
|
|
51
34
|
while isinstance(t, TypeAliasType) or (
|
|
52
35
|
t.__class__.__name__ == 'TypeAliasType' and hasattr(t, '__value__')
|
|
53
36
|
):
|
|
@@ -57,47 +40,54 @@ def get_core_types(tp):
|
|
|
57
40
|
break
|
|
58
41
|
return t
|
|
59
42
|
|
|
60
|
-
|
|
43
|
+
def _enqueue(items, q):
|
|
44
|
+
for it in items:
|
|
45
|
+
if it is not type(None): # skip None in unions
|
|
46
|
+
q.append(it)
|
|
61
47
|
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
args = getattr(_tp, "__args__", ())
|
|
66
|
-
if args:
|
|
67
|
-
_tp = args[0]
|
|
68
|
-
else:
|
|
69
|
-
break
|
|
70
|
-
return _tp
|
|
71
|
-
|
|
72
|
-
tp = _shell_list(tp)
|
|
48
|
+
# Queue-based shelling to reach concrete core types
|
|
49
|
+
queue: list[object] = [tp]
|
|
50
|
+
result: list[object] = []
|
|
73
51
|
|
|
74
|
-
|
|
75
|
-
|
|
52
|
+
while queue:
|
|
53
|
+
cur = queue.pop(0)
|
|
54
|
+
if cur is type(None):
|
|
55
|
+
continue
|
|
76
56
|
|
|
77
|
-
|
|
78
|
-
return tuple()
|
|
57
|
+
cur = _unwrap_alias(cur)
|
|
79
58
|
|
|
80
|
-
|
|
81
|
-
|
|
59
|
+
# Handle Annotated[T, ...] as a shell
|
|
60
|
+
if get_origin(cur) is Annotated:
|
|
61
|
+
args = get_args(cur)
|
|
62
|
+
if args:
|
|
63
|
+
queue.append(args[0])
|
|
64
|
+
continue
|
|
82
65
|
|
|
66
|
+
# Handle Union / Optional / PEP 604 UnionType
|
|
67
|
+
orig = get_origin(cur)
|
|
83
68
|
if orig in (Union, UnionType):
|
|
84
|
-
args =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
69
|
+
args = get_args(cur)
|
|
70
|
+
# push all non-None members back for further shelling
|
|
71
|
+
_enqueue(args, queue)
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# Handle list shells
|
|
75
|
+
if is_list(cur):
|
|
76
|
+
args = getattr(cur, "__args__", ())
|
|
77
|
+
if args:
|
|
78
|
+
queue.append(args[0])
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
# If still an alias-like wrapper, unwrap again and re-process
|
|
82
|
+
_cur2 = _unwrap_alias(cur)
|
|
83
|
+
if _cur2 is not cur:
|
|
84
|
+
queue.append(_cur2)
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# Otherwise treat as a concrete core type (could be a class, typing.Final, etc.)
|
|
88
|
+
result.append(cur)
|
|
89
|
+
|
|
90
|
+
return tuple(result)
|
|
101
91
|
|
|
102
92
|
|
|
103
93
|
def get_type_name(anno):
|
|
@@ -242,6 +232,7 @@ def safe_issubclass(kls, classinfo):
|
|
|
242
232
|
|
|
243
233
|
|
|
244
234
|
def update_forward_refs(kls):
|
|
235
|
+
# TODO: refactor
|
|
245
236
|
def update_pydantic_forward_refs(kls2: Type[BaseModel]):
|
|
246
237
|
"""
|
|
247
238
|
recursively update refs.
|
fastapi_voyager/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.4.
|
|
2
|
+
__version__ = "0.4.5"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-voyager
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -144,9 +144,9 @@ dot -Tpng router_viz.dot -o router_viz.png
|
|
|
144
144
|
or you can open router_viz.dot with vscode extension `graphviz interactive preview`
|
|
145
145
|
|
|
146
146
|
|
|
147
|
-
## Plan
|
|
147
|
+
## Plan before v1.0
|
|
148
148
|
|
|
149
|
-
features
|
|
149
|
+
features
|
|
150
150
|
- [x] group schemas by module hierarchy
|
|
151
151
|
- [x] module-based coloring via Analytics(module_color={...})
|
|
152
152
|
- [x] view in web browser
|
|
@@ -162,18 +162,30 @@ features:
|
|
|
162
162
|
- [x] alt+click to show field details
|
|
163
163
|
- [x] display source code of routes (including response_model)
|
|
164
164
|
- [x] handle excluded field
|
|
165
|
-
- [ ] user can generate nodes/edges manually and connect to generated ones
|
|
166
|
-
- [ ] support dataclass
|
|
167
165
|
- [ ] group routes by module hierarchy
|
|
166
|
+
- [ ] support dataclass
|
|
167
|
+
- [ ] user can generate nodes/edges manually and connect to generated ones
|
|
168
|
+
- [ ] refactor
|
|
169
|
+
- [ ] abstract render module
|
|
170
|
+
- [ ] ui optimization
|
|
171
|
+
- [ ] fixed left/right bar show field information
|
|
172
|
+
- [ ] display standard ER diagram
|
|
173
|
+
- [ ] display potential invalid links
|
|
168
174
|
- [ ] integration with pydantic-resolve
|
|
169
175
|
- [ ] show difference between resolve, post fields
|
|
170
176
|
- [x] strikethrough for excluded fields
|
|
171
177
|
- [ ] display loader as edges
|
|
172
|
-
- [ ]
|
|
178
|
+
- [ ] add tests
|
|
173
179
|
|
|
174
180
|
bugs:
|
|
175
181
|
- [ ] fix duplicated link from class and parent class, it also break clicking highlight
|
|
176
182
|
|
|
183
|
+
## Using with pydantic-resolve
|
|
184
|
+
|
|
185
|
+
pydantic-resolve's @ensure_subset decorator is helpful to pick fields from `source class` in safe.
|
|
186
|
+
|
|
187
|
+
TODO: ...
|
|
188
|
+
|
|
177
189
|
|
|
178
190
|
## Credits
|
|
179
191
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
fastapi_voyager/__init__.py,sha256=E5WTV_sYs2LK8I6jzA7AuvFU5a8_vjnDseC3DMha0iQ,149
|
|
2
2
|
fastapi_voyager/cli.py,sha256=FuXllJtlsm33FX6VmDdAFWzzQ5TIraUizBDgvBj3vmY,10489
|
|
3
3
|
fastapi_voyager/filter.py,sha256=uZrVVMhHG5E7j1wdsiB02RAyoDdF1q8A4J04oCboYAU,4644
|
|
4
|
-
fastapi_voyager/graph.py,sha256=
|
|
5
|
-
fastapi_voyager/module.py,sha256=
|
|
4
|
+
fastapi_voyager/graph.py,sha256=0lTU2XiIer35IPXoQBuh2KajvXUGeea3-vuHyPnketA,14734
|
|
5
|
+
fastapi_voyager/module.py,sha256=NyhSpjevhYtIJdlJxpksGAzfbOV5bHZ7PWbVT4x8LIU,2664
|
|
6
6
|
fastapi_voyager/server.py,sha256=UFj6c0Yx4Rmy56eFXv65n1VdmJeiBX43KsDm6D65U28,2943
|
|
7
|
-
fastapi_voyager/type.py,sha256=
|
|
8
|
-
fastapi_voyager/type_helper.py,sha256=
|
|
9
|
-
fastapi_voyager/version.py,sha256=
|
|
7
|
+
fastapi_voyager/type.py,sha256=xkvfUL5vmfaerkaoowdSlTRsD11NP5NG1rMOXYPBJO0,1265
|
|
8
|
+
fastapi_voyager/type_helper.py,sha256=j7AiFXsfl4kaxshYtofbsqo08dIXiHvJ190soIzUdLk,8380
|
|
9
|
+
fastapi_voyager/version.py,sha256=LLMiqcKNNqxWTkirwEvOSRsPX9JD6wKMiTXX3MN-uCA,48
|
|
10
10
|
fastapi_voyager/web/graph-ui.js,sha256=eEjDnJVMvk35LdRoxcqX_fZxLFS9_bUrGAZL6K2O5C0,4176
|
|
11
11
|
fastapi_voyager/web/graphviz.svg.css,sha256=zDCjjpT0Idufu5YOiZI76PL70-avP3vTyzGPh9M85Do,1563
|
|
12
12
|
fastapi_voyager/web/graphviz.svg.js,sha256=lvAdbjHc-lMSk4GQp-iqYA2PCFX4RKnW7dFaoe0LUHs,16005
|
|
@@ -24,8 +24,8 @@ fastapi_voyager/web/icon/favicon-16x16.png,sha256=JC07jEzfIYxBIoQn_FHXvyHuxESdhW
|
|
|
24
24
|
fastapi_voyager/web/icon/favicon-32x32.png,sha256=C7v1h58cfWOsiLp9yOIZtlx-dLasBcq3NqpHVGRmpt4,1859
|
|
25
25
|
fastapi_voyager/web/icon/favicon.ico,sha256=tZolYIXkkBcFiYl1A8ksaXN2VjGamzcSdes838dLvNc,15406
|
|
26
26
|
fastapi_voyager/web/icon/site.webmanifest,sha256=ep4Hzh9zhmiZF2At3Fp1dQrYQuYF_3ZPZxc1KcGBvwQ,263
|
|
27
|
-
fastapi_voyager-0.4.
|
|
28
|
-
fastapi_voyager-0.4.
|
|
29
|
-
fastapi_voyager-0.4.
|
|
30
|
-
fastapi_voyager-0.4.
|
|
31
|
-
fastapi_voyager-0.4.
|
|
27
|
+
fastapi_voyager-0.4.5.dist-info/METADATA,sha256=aSM2jqwKxrbrr05LTzBLMGQ5wpSQC5EqyDxJFKKygZA,6508
|
|
28
|
+
fastapi_voyager-0.4.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
29
|
+
fastapi_voyager-0.4.5.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
|
|
30
|
+
fastapi_voyager-0.4.5.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
|
|
31
|
+
fastapi_voyager-0.4.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|