fastapi-voyager 0.13.0__py3-none-any.whl → 0.13.1__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/er_diagram.py +170 -16
- fastapi_voyager/type.py +1 -0
- fastapi_voyager/version.py +1 -1
- {fastapi_voyager-0.13.0.dist-info → fastapi_voyager-0.13.1.dist-info}/METADATA +1 -1
- {fastapi_voyager-0.13.0.dist-info → fastapi_voyager-0.13.1.dist-info}/RECORD +8 -8
- {fastapi_voyager-0.13.0.dist-info → fastapi_voyager-0.13.1.dist-info}/WHEEL +0 -0
- {fastapi_voyager-0.13.0.dist-info → fastapi_voyager-0.13.1.dist-info}/entry_points.txt +0 -0
- {fastapi_voyager-0.13.0.dist-info → fastapi_voyager-0.13.1.dist-info}/licenses/LICENSE +0 -0
fastapi_voyager/er_diagram.py
CHANGED
|
@@ -1,16 +1,153 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from fastapi_voyager.type import PK, FieldType, Link, LinkType, SchemaNode
|
|
4
3
|
from fastapi_voyager.type_helper import (
|
|
5
4
|
update_forward_refs,
|
|
6
5
|
full_class_name,
|
|
7
6
|
get_core_types,
|
|
8
7
|
get_type_name
|
|
9
8
|
)
|
|
10
|
-
from fastapi_voyager.
|
|
11
|
-
|
|
9
|
+
from fastapi_voyager.type import (
|
|
10
|
+
FieldInfo,
|
|
11
|
+
PK,
|
|
12
|
+
FieldType,
|
|
13
|
+
LinkType,
|
|
14
|
+
Link,
|
|
15
|
+
ModuleNode,
|
|
16
|
+
SchemaNode,
|
|
17
|
+
)
|
|
12
18
|
from pydantic import BaseModel
|
|
13
|
-
from pydantic_resolve import ErDiagram, Entity
|
|
19
|
+
from pydantic_resolve import ErDiagram, Entity, Relationship, MultipleRelationship
|
|
20
|
+
from logging import getLogger
|
|
21
|
+
from fastapi_voyager.module import build_module_schema_tree
|
|
22
|
+
|
|
23
|
+
logger = getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DiagramRenderer:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
*,
|
|
30
|
+
show_fields: FieldType = 'single',
|
|
31
|
+
show_module: bool = True
|
|
32
|
+
) -> None:
|
|
33
|
+
self.show_fields = show_fields if show_fields in ('single', 'object', 'all') else 'single'
|
|
34
|
+
self.show_module = show_module
|
|
35
|
+
|
|
36
|
+
logger.info(f'show_module: {self.show_module}')
|
|
37
|
+
|
|
38
|
+
def render_schema_label(self, node: SchemaNode, color: str | None=None) -> str:
|
|
39
|
+
has_base_fields = any(f.from_base for f in node.fields)
|
|
40
|
+
fields = [n for n in node.fields if n.from_base is False]
|
|
41
|
+
|
|
42
|
+
if self.show_fields == 'all':
|
|
43
|
+
_fields = fields
|
|
44
|
+
elif self.show_fields == 'object':
|
|
45
|
+
_fields = [f for f in fields if f.is_object is True]
|
|
46
|
+
else: # 'single'
|
|
47
|
+
_fields = []
|
|
48
|
+
|
|
49
|
+
fields_parts: list[str] = []
|
|
50
|
+
if self.show_fields == 'all' and has_base_fields:
|
|
51
|
+
fields_parts.append('<tr><td align="left" cellpadding="8"><font color="#999"> Inherited Fields ... </font></td></tr>')
|
|
52
|
+
|
|
53
|
+
for field in _fields:
|
|
54
|
+
type_name = field.type_name[:25] + '..' if len(field.type_name) > 25 else field.type_name
|
|
55
|
+
display_xml = f'<s align="left">{field.name}: {type_name}</s>' if field.is_exclude else f'{field.name}: {type_name}'
|
|
56
|
+
field_str = f"""<tr><td align="left" port="f{field.name}" cellpadding="8"><font> {display_xml} </font></td></tr>"""
|
|
57
|
+
fields_parts.append(field_str)
|
|
58
|
+
|
|
59
|
+
header_color = '#009485' if color is None else color
|
|
60
|
+
header = f"""<tr><td cellpadding="6" bgcolor="{header_color}" align="center" colspan="1" port="{PK}"> <font color="white"> {node.name} </font></td> </tr>"""
|
|
61
|
+
field_content = ''.join(fields_parts) if fields_parts else ''
|
|
62
|
+
return f"""<<table border="0" cellborder="1" cellpadding="0" cellspacing="0" bgcolor="white"> {header} {field_content} </table>>"""
|
|
63
|
+
|
|
64
|
+
def _handle_schema_anchor(self, source: str) -> str:
|
|
65
|
+
if '::' in source:
|
|
66
|
+
a, b = source.split('::', 1)
|
|
67
|
+
return f'"{a}":{b}'
|
|
68
|
+
return f'"{source}"'
|
|
69
|
+
|
|
70
|
+
def render_link(self, link: Link) -> str:
|
|
71
|
+
h = self._handle_schema_anchor
|
|
72
|
+
if link.type == 'schema':
|
|
73
|
+
return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid", label = "{link.label}", minlen=3];"""
|
|
74
|
+
else:
|
|
75
|
+
raise ValueError(f'Unknown link type: {link.type}')
|
|
76
|
+
|
|
77
|
+
def render_module_schema_content(self, nodes: list[SchemaNode]) -> str:
|
|
78
|
+
def render_node(node: SchemaNode, color: str | None=None) -> str:
|
|
79
|
+
return f'''
|
|
80
|
+
"{node.id}" [
|
|
81
|
+
label = {self.render_schema_label(node, color)}
|
|
82
|
+
shape = "plain"
|
|
83
|
+
margin="0.5,0.1"
|
|
84
|
+
];'''
|
|
85
|
+
|
|
86
|
+
def render_module_schema(mod: ModuleNode, show_cluster:bool=True) -> str:
|
|
87
|
+
inner_nodes = [ render_node(node) for node in mod.schema_nodes ]
|
|
88
|
+
inner_nodes_str = '\n'.join(inner_nodes)
|
|
89
|
+
child_str = '\n'.join(render_module_schema(mod=m, show_cluster=show_cluster) for m in mod.modules)
|
|
90
|
+
|
|
91
|
+
if show_cluster:
|
|
92
|
+
return f'''
|
|
93
|
+
subgraph cluster_module_{mod.fullname.replace('.', '_')} {{
|
|
94
|
+
tooltip="{mod.fullname}"
|
|
95
|
+
color = "#666"
|
|
96
|
+
style="rounded"
|
|
97
|
+
label = " {mod.name}"
|
|
98
|
+
labeljust = "l"
|
|
99
|
+
pencolor="#ccc"
|
|
100
|
+
penwidth=""
|
|
101
|
+
{inner_nodes_str}
|
|
102
|
+
{child_str}
|
|
103
|
+
}}'''
|
|
104
|
+
else:
|
|
105
|
+
return f'''
|
|
106
|
+
{inner_nodes_str}
|
|
107
|
+
{child_str}
|
|
108
|
+
'''
|
|
109
|
+
|
|
110
|
+
# if self.show_module:
|
|
111
|
+
module_schemas = build_module_schema_tree(nodes)
|
|
112
|
+
return '\n'.join(render_module_schema(mod=m, show_cluster=self.show_module) for m in module_schemas)
|
|
113
|
+
|
|
114
|
+
def render_dot(self, nodes: list[SchemaNode], links: list[Link], spline_line=False) -> str:
|
|
115
|
+
module_schemas_str = self.render_module_schema_content(nodes)
|
|
116
|
+
link_str = '\n'.join(self.render_link(link) for link in links)
|
|
117
|
+
|
|
118
|
+
dot_str = f'''
|
|
119
|
+
digraph world {{
|
|
120
|
+
pad="0.5"
|
|
121
|
+
nodesep=0.8
|
|
122
|
+
{'splines=line' if spline_line else ''}
|
|
123
|
+
fontname="Helvetica,Arial,sans-serif"
|
|
124
|
+
node [fontname="Helvetica,Arial,sans-serif"]
|
|
125
|
+
edge [
|
|
126
|
+
fontname="Helvetica,Arial,sans-serif"
|
|
127
|
+
color="gray"
|
|
128
|
+
]
|
|
129
|
+
graph [
|
|
130
|
+
rankdir = "LR"
|
|
131
|
+
];
|
|
132
|
+
node [
|
|
133
|
+
fontsize = "16"
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
subgraph cluster_schema {{
|
|
137
|
+
color = "#aaa"
|
|
138
|
+
margin=18
|
|
139
|
+
style="dashed"
|
|
140
|
+
label=" ER Diagram"
|
|
141
|
+
labeljust="l"
|
|
142
|
+
fontsize="20"
|
|
143
|
+
{module_schemas_str}
|
|
144
|
+
}}
|
|
145
|
+
|
|
146
|
+
{link_str}
|
|
147
|
+
}}
|
|
148
|
+
'''
|
|
149
|
+
return dot_str
|
|
150
|
+
|
|
14
151
|
|
|
15
152
|
class VoyagerErDiagram:
|
|
16
153
|
def __init__(self,
|
|
@@ -42,12 +179,26 @@ class VoyagerErDiagram:
|
|
|
42
179
|
for anno in annos:
|
|
43
180
|
self.add_to_node_set(anno, fk_set=self.fk_set.get(full_class_name(anno)))
|
|
44
181
|
source_name = f'{full_class_name(schema)}::f{relationship.field}'
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
182
|
+
if isinstance(relationship, Relationship):
|
|
183
|
+
self.add_to_link_set(
|
|
184
|
+
source=source_name,
|
|
185
|
+
source_origin=full_class_name(schema),
|
|
186
|
+
target=self.generate_node_head(full_class_name(anno)),
|
|
187
|
+
target_origin=full_class_name(anno),
|
|
188
|
+
type='schema',
|
|
189
|
+
label=get_type_name(relationship.target_kls))
|
|
190
|
+
|
|
191
|
+
elif isinstance(relationship, MultipleRelationship):
|
|
192
|
+
for link in relationship.links:
|
|
193
|
+
self.add_to_link_set(
|
|
194
|
+
source=source_name,
|
|
195
|
+
source_origin=full_class_name(schema),
|
|
196
|
+
target=self.generate_node_head(full_class_name(anno)),
|
|
197
|
+
target_origin=full_class_name(anno),
|
|
198
|
+
type='schema',
|
|
199
|
+
biz=link.biz,
|
|
200
|
+
label=f'{get_type_name(relationship.target_kls)} / {link.biz} '
|
|
201
|
+
)
|
|
51
202
|
|
|
52
203
|
def add_to_node_set(self, schema, fk_set: set[str] | None = None) -> str:
|
|
53
204
|
"""
|
|
@@ -73,13 +224,15 @@ class VoyagerErDiagram:
|
|
|
73
224
|
source_origin: str,
|
|
74
225
|
target: str,
|
|
75
226
|
target_origin: str,
|
|
76
|
-
type: LinkType
|
|
227
|
+
type: LinkType,
|
|
228
|
+
label: str,
|
|
229
|
+
biz: str | None = None
|
|
77
230
|
) -> bool:
|
|
78
231
|
"""
|
|
79
232
|
1. add link to link_set
|
|
80
233
|
2. if duplicated, do nothing, else insert
|
|
81
234
|
"""
|
|
82
|
-
pair = (source, target)
|
|
235
|
+
pair = (source, target, biz)
|
|
83
236
|
if result := pair not in self.link_set:
|
|
84
237
|
self.link_set.add(pair)
|
|
85
238
|
self.links.append(Link(
|
|
@@ -87,7 +240,8 @@ class VoyagerErDiagram:
|
|
|
87
240
|
source_origin=source_origin,
|
|
88
241
|
target=target,
|
|
89
242
|
target_origin=target_origin,
|
|
90
|
-
type=type
|
|
243
|
+
type=type,
|
|
244
|
+
label=label
|
|
91
245
|
))
|
|
92
246
|
return result
|
|
93
247
|
|
|
@@ -100,8 +254,8 @@ class VoyagerErDiagram:
|
|
|
100
254
|
|
|
101
255
|
for entity in self.er_diagram.configs:
|
|
102
256
|
self.analysis_entity(entity)
|
|
103
|
-
renderer =
|
|
104
|
-
return renderer.render_dot(
|
|
257
|
+
renderer = DiagramRenderer(show_fields=self.show_field, show_module=self.show_module)
|
|
258
|
+
return renderer.render_dot(list(self.node_set.values()), self.links)
|
|
105
259
|
|
|
106
260
|
|
|
107
261
|
def get_fields(schema: type[BaseModel], fk_set: set[str] | None = None) -> list[FieldInfo]:
|
|
@@ -116,4 +270,4 @@ def get_fields(schema: type[BaseModel], fk_set: set[str] | None = None) -> list[
|
|
|
116
270
|
type_name=get_type_name(anno),
|
|
117
271
|
is_exclude=bool(v.exclude)
|
|
118
272
|
))
|
|
119
|
-
return fields
|
|
273
|
+
return fields
|
fastapi_voyager/type.py
CHANGED
fastapi_voyager/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.13.
|
|
2
|
+
__version__ = "0.13.1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-voyager
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.1
|
|
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
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
fastapi_voyager/__init__.py,sha256=kqwzThE1YhmQ_7jPKGlnWvqRGC-hFrRqq_lKhKaleYU,229
|
|
2
2
|
fastapi_voyager/cli.py,sha256=td3yIIigEomhSdDO-Xkh-CgpEwCafwlwnpvxnT9QsBo,10488
|
|
3
|
-
fastapi_voyager/er_diagram.py,sha256=
|
|
3
|
+
fastapi_voyager/er_diagram.py,sha256=I-Fo03FMHu5INYeeycsdECN0oh7LroDHD88YlHwwUGA,10136
|
|
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/render.py,sha256=O_HR8ypOrFhjejkBpKIH_8foB78DgzH0hvO-CWeYt0w,9976
|
|
7
7
|
fastapi_voyager/server.py,sha256=UZi-VdsurjDnqDgf3l5LfgWfZ4OxzilC_A_Ju6x9jQc,8592
|
|
8
|
-
fastapi_voyager/type.py,sha256=
|
|
8
|
+
fastapi_voyager/type.py,sha256=uniDpmNSf6r-5qoGVkOP6-k0kxxJyUvXsEBgW5sDFKg,1779
|
|
9
9
|
fastapi_voyager/type_helper.py,sha256=UTCFWluFeGdGkJX3wiE_bZ2EgZsu4JkmqHjsJVdG81Q,9953
|
|
10
|
-
fastapi_voyager/version.py,sha256=
|
|
10
|
+
fastapi_voyager/version.py,sha256=XPQT-MiS5izc8d6XhjxYtP7l6JRWW4h0BgwR0e2ndGo,49
|
|
11
11
|
fastapi_voyager/voyager.py,sha256=iWt-_QsoKavhb9ZawhU3W8gv3vTwn8PWTevg8BooyV8,13923
|
|
12
12
|
fastapi_voyager/web/graph-ui.js,sha256=hTsZO1Ly1JuoRg0kZWQ62jeLiD2kbnzACfbSPd0F95U,6634
|
|
13
13
|
fastapi_voyager/web/graphviz.svg.css,sha256=zDCjjpT0Idufu5YOiZI76PL70-avP3vTyzGPh9M85Do,1563
|
|
@@ -28,8 +28,8 @@ fastapi_voyager/web/icon/favicon-16x16.png,sha256=JC07jEzfIYxBIoQn_FHXvyHuxESdhW
|
|
|
28
28
|
fastapi_voyager/web/icon/favicon-32x32.png,sha256=C7v1h58cfWOsiLp9yOIZtlx-dLasBcq3NqpHVGRmpt4,1859
|
|
29
29
|
fastapi_voyager/web/icon/favicon.ico,sha256=tZolYIXkkBcFiYl1A8ksaXN2VjGamzcSdes838dLvNc,15406
|
|
30
30
|
fastapi_voyager/web/icon/site.webmanifest,sha256=ep4Hzh9zhmiZF2At3Fp1dQrYQuYF_3ZPZxc1KcGBvwQ,263
|
|
31
|
-
fastapi_voyager-0.13.
|
|
32
|
-
fastapi_voyager-0.13.
|
|
33
|
-
fastapi_voyager-0.13.
|
|
34
|
-
fastapi_voyager-0.13.
|
|
35
|
-
fastapi_voyager-0.13.
|
|
31
|
+
fastapi_voyager-0.13.1.dist-info/METADATA,sha256=9MgyRS-Az5198n87XhUnHLF6xgkghGxBeCM6TRmCitE,6521
|
|
32
|
+
fastapi_voyager-0.13.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
33
|
+
fastapi_voyager-0.13.1.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
|
|
34
|
+
fastapi_voyager-0.13.1.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
|
|
35
|
+
fastapi_voyager-0.13.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|