fastapi-voyager 0.13.0__tar.gz → 0.13.1__tar.gz
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-0.13.0 → fastapi_voyager-0.13.1}/PKG-INFO +1 -1
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/docs/changelog.md +3 -1
- fastapi_voyager-0.13.1/src/fastapi_voyager/er_diagram.py +273 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/type.py +1 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/version.py +1 -1
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/tests/service/schema/schema.py +5 -2
- fastapi_voyager-0.13.0/src/fastapi_voyager/er_diagram.py +0 -119
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/.github/workflows/publish.yml +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/.gitignore +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/.python-version +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/CONTRIBUTING.md +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/LICENSE +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/README.md +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/docs/idea.md +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/pyproject.toml +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/release.md +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/__init__.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/cli.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/filter.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/module.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/render.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/server.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/type_helper.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/voyager.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/component/demo.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/component/render-graph.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/graph-ui.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/index.html +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/quasar.min.css +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/quasar.min.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/store.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/vue-main.js +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/tests/__init__.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/tests/demo.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/tests/demo_anno.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/tests/programatic.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/tests/service/__init__.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/tests/service/schema/__init__.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/tests/service/schema/base_entity.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/tests/service/schema/extra.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/tests/test_analysis.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/tests/test_filter.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/tests/test_generic.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/tests/test_import.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/tests/test_module.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/tests/test_type_helper.py +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/uv.lock +0 -0
- {fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/voyager.jpg +0 -0
|
@@ -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
|
|
@@ -139,11 +139,13 @@
|
|
|
139
139
|
- 0.13.0
|
|
140
140
|
- [x] if er diagram is provided, show it first.
|
|
141
141
|
- 0.13.1
|
|
142
|
+
- [x] show more details in er diagram
|
|
143
|
+
- 0.13.2
|
|
142
144
|
- [ ] integration with pydantic-resolve
|
|
143
145
|
- [ ] show hint for resolve, post fields
|
|
144
146
|
- [ ] display loader as edges
|
|
145
147
|
- [ ] add tests
|
|
146
|
-
- 0.13.
|
|
148
|
+
- 0.13.3
|
|
147
149
|
- [ ] refactor vue-main.js, move methods to store
|
|
148
150
|
- [ ] refactor render.py
|
|
149
151
|
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi_voyager.type_helper import (
|
|
4
|
+
update_forward_refs,
|
|
5
|
+
full_class_name,
|
|
6
|
+
get_core_types,
|
|
7
|
+
get_type_name
|
|
8
|
+
)
|
|
9
|
+
from fastapi_voyager.type import (
|
|
10
|
+
FieldInfo,
|
|
11
|
+
PK,
|
|
12
|
+
FieldType,
|
|
13
|
+
LinkType,
|
|
14
|
+
Link,
|
|
15
|
+
ModuleNode,
|
|
16
|
+
SchemaNode,
|
|
17
|
+
)
|
|
18
|
+
from pydantic import BaseModel
|
|
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
|
+
|
|
151
|
+
|
|
152
|
+
class VoyagerErDiagram:
|
|
153
|
+
def __init__(self,
|
|
154
|
+
er_diagram: ErDiagram,
|
|
155
|
+
show_fields: FieldType = 'single',
|
|
156
|
+
show_module: bool = False):
|
|
157
|
+
self.er_diagram = er_diagram
|
|
158
|
+
self.nodes: list[SchemaNode] = []
|
|
159
|
+
self.node_set: dict[str, SchemaNode] = {}
|
|
160
|
+
|
|
161
|
+
self.links: list[Link] = []
|
|
162
|
+
self.link_set: set[tuple[str, str]] = set()
|
|
163
|
+
|
|
164
|
+
self.fk_set: dict[str, set[str]] = {}
|
|
165
|
+
|
|
166
|
+
self.show_field = show_fields
|
|
167
|
+
self.show_module = show_module
|
|
168
|
+
|
|
169
|
+
def generate_node_head(self, link_name: str):
|
|
170
|
+
return f'{link_name}::{PK}'
|
|
171
|
+
|
|
172
|
+
def analysis_entity(self, entity: Entity):
|
|
173
|
+
schema = entity.kls
|
|
174
|
+
update_forward_refs(schema)
|
|
175
|
+
self.add_to_node_set(schema, fk_set=self.fk_set.get(full_class_name(schema)))
|
|
176
|
+
|
|
177
|
+
for relationship in entity.relationships:
|
|
178
|
+
annos = get_core_types(relationship.target_kls)
|
|
179
|
+
for anno in annos:
|
|
180
|
+
self.add_to_node_set(anno, fk_set=self.fk_set.get(full_class_name(anno)))
|
|
181
|
+
source_name = f'{full_class_name(schema)}::f{relationship.field}'
|
|
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
|
+
)
|
|
202
|
+
|
|
203
|
+
def add_to_node_set(self, schema, fk_set: set[str] | None = None) -> str:
|
|
204
|
+
"""
|
|
205
|
+
1. calc full_path, add to node_set
|
|
206
|
+
2. if duplicated, do nothing, else insert
|
|
207
|
+
2. return the full_path
|
|
208
|
+
"""
|
|
209
|
+
full_name = full_class_name(schema)
|
|
210
|
+
|
|
211
|
+
if full_name not in self.node_set:
|
|
212
|
+
# skip meta info for normal queries
|
|
213
|
+
self.node_set[full_name] = SchemaNode(
|
|
214
|
+
id=full_name,
|
|
215
|
+
module=schema.__module__,
|
|
216
|
+
name=schema.__name__,
|
|
217
|
+
fields=get_fields(schema, fk_set)
|
|
218
|
+
)
|
|
219
|
+
return full_name
|
|
220
|
+
|
|
221
|
+
def add_to_link_set(
|
|
222
|
+
self,
|
|
223
|
+
source: str,
|
|
224
|
+
source_origin: str,
|
|
225
|
+
target: str,
|
|
226
|
+
target_origin: str,
|
|
227
|
+
type: LinkType,
|
|
228
|
+
label: str,
|
|
229
|
+
biz: str | None = None
|
|
230
|
+
) -> bool:
|
|
231
|
+
"""
|
|
232
|
+
1. add link to link_set
|
|
233
|
+
2. if duplicated, do nothing, else insert
|
|
234
|
+
"""
|
|
235
|
+
pair = (source, target, biz)
|
|
236
|
+
if result := pair not in self.link_set:
|
|
237
|
+
self.link_set.add(pair)
|
|
238
|
+
self.links.append(Link(
|
|
239
|
+
source=source,
|
|
240
|
+
source_origin=source_origin,
|
|
241
|
+
target=target,
|
|
242
|
+
target_origin=target_origin,
|
|
243
|
+
type=type,
|
|
244
|
+
label=label
|
|
245
|
+
))
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def render_dot(self):
|
|
250
|
+
self.fk_set = {
|
|
251
|
+
full_class_name(entity.kls): set([rel.field for rel in entity.relationships])
|
|
252
|
+
for entity in self.er_diagram.configs
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for entity in self.er_diagram.configs:
|
|
256
|
+
self.analysis_entity(entity)
|
|
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)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_fields(schema: type[BaseModel], fk_set: set[str] | None = None) -> list[FieldInfo]:
|
|
262
|
+
|
|
263
|
+
fields: list[FieldInfo] = []
|
|
264
|
+
for k, v in schema.model_fields.items():
|
|
265
|
+
anno = v.annotation
|
|
266
|
+
fields.append(FieldInfo(
|
|
267
|
+
is_object=k in fk_set if fk_set is not None else False,
|
|
268
|
+
name=k,
|
|
269
|
+
from_base=False,
|
|
270
|
+
type_name=get_type_name(anno),
|
|
271
|
+
is_exclude=bool(v.exclude)
|
|
272
|
+
))
|
|
273
|
+
return fields
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.13.
|
|
2
|
+
__version__ = "0.13.1"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from typing import Literal
|
|
3
3
|
from pydantic import BaseModel
|
|
4
|
-
from pydantic_resolve import Relationship
|
|
4
|
+
from pydantic_resolve import Relationship, MultipleRelationship, Link
|
|
5
5
|
from .base_entity import BaseEntity
|
|
6
6
|
|
|
7
7
|
|
|
@@ -34,7 +34,10 @@ class Story(BaseModel, BaseEntity):
|
|
|
34
34
|
|
|
35
35
|
class Sprint(BaseModel, BaseEntity):
|
|
36
36
|
__pydantic_resolve_relationships__ = [
|
|
37
|
-
|
|
37
|
+
MultipleRelationship(field='id', target_kls=list[Story], links=[
|
|
38
|
+
Link(biz='all'),
|
|
39
|
+
Link(biz='done'),
|
|
40
|
+
])
|
|
38
41
|
]
|
|
39
42
|
id: int
|
|
40
43
|
name: str
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from fastapi_voyager.type import PK, FieldType, Link, LinkType, SchemaNode
|
|
4
|
-
from fastapi_voyager.type_helper import (
|
|
5
|
-
update_forward_refs,
|
|
6
|
-
full_class_name,
|
|
7
|
-
get_core_types,
|
|
8
|
-
get_type_name
|
|
9
|
-
)
|
|
10
|
-
from fastapi_voyager.render import Renderer
|
|
11
|
-
from fastapi_voyager.type import FieldInfo
|
|
12
|
-
from pydantic import BaseModel
|
|
13
|
-
from pydantic_resolve import ErDiagram, Entity
|
|
14
|
-
|
|
15
|
-
class VoyagerErDiagram:
|
|
16
|
-
def __init__(self,
|
|
17
|
-
er_diagram: ErDiagram,
|
|
18
|
-
show_fields: FieldType = 'single',
|
|
19
|
-
show_module: bool = False):
|
|
20
|
-
self.er_diagram = er_diagram
|
|
21
|
-
self.nodes: list[SchemaNode] = []
|
|
22
|
-
self.node_set: dict[str, SchemaNode] = {}
|
|
23
|
-
|
|
24
|
-
self.links: list[Link] = []
|
|
25
|
-
self.link_set: set[tuple[str, str]] = set()
|
|
26
|
-
|
|
27
|
-
self.fk_set: dict[str, set[str]] = {}
|
|
28
|
-
|
|
29
|
-
self.show_field = show_fields
|
|
30
|
-
self.show_module = show_module
|
|
31
|
-
|
|
32
|
-
def generate_node_head(self, link_name: str):
|
|
33
|
-
return f'{link_name}::{PK}'
|
|
34
|
-
|
|
35
|
-
def analysis_entity(self, entity: Entity):
|
|
36
|
-
schema = entity.kls
|
|
37
|
-
update_forward_refs(schema)
|
|
38
|
-
self.add_to_node_set(schema, fk_set=self.fk_set.get(full_class_name(schema)))
|
|
39
|
-
|
|
40
|
-
for relationship in entity.relationships:
|
|
41
|
-
annos = get_core_types(relationship.target_kls)
|
|
42
|
-
for anno in annos:
|
|
43
|
-
self.add_to_node_set(anno, fk_set=self.fk_set.get(full_class_name(anno)))
|
|
44
|
-
source_name = f'{full_class_name(schema)}::f{relationship.field}'
|
|
45
|
-
self.add_to_link_set(
|
|
46
|
-
source=source_name,
|
|
47
|
-
source_origin=full_class_name(schema),
|
|
48
|
-
target=self.generate_node_head(full_class_name(anno)),
|
|
49
|
-
target_origin=full_class_name(anno),
|
|
50
|
-
type='schema')
|
|
51
|
-
|
|
52
|
-
def add_to_node_set(self, schema, fk_set: set[str] | None = None) -> str:
|
|
53
|
-
"""
|
|
54
|
-
1. calc full_path, add to node_set
|
|
55
|
-
2. if duplicated, do nothing, else insert
|
|
56
|
-
2. return the full_path
|
|
57
|
-
"""
|
|
58
|
-
full_name = full_class_name(schema)
|
|
59
|
-
|
|
60
|
-
if full_name not in self.node_set:
|
|
61
|
-
# skip meta info for normal queries
|
|
62
|
-
self.node_set[full_name] = SchemaNode(
|
|
63
|
-
id=full_name,
|
|
64
|
-
module=schema.__module__,
|
|
65
|
-
name=schema.__name__,
|
|
66
|
-
fields=get_fields(schema, fk_set)
|
|
67
|
-
)
|
|
68
|
-
return full_name
|
|
69
|
-
|
|
70
|
-
def add_to_link_set(
|
|
71
|
-
self,
|
|
72
|
-
source: str,
|
|
73
|
-
source_origin: str,
|
|
74
|
-
target: str,
|
|
75
|
-
target_origin: str,
|
|
76
|
-
type: LinkType
|
|
77
|
-
) -> bool:
|
|
78
|
-
"""
|
|
79
|
-
1. add link to link_set
|
|
80
|
-
2. if duplicated, do nothing, else insert
|
|
81
|
-
"""
|
|
82
|
-
pair = (source, target)
|
|
83
|
-
if result := pair not in self.link_set:
|
|
84
|
-
self.link_set.add(pair)
|
|
85
|
-
self.links.append(Link(
|
|
86
|
-
source=source,
|
|
87
|
-
source_origin=source_origin,
|
|
88
|
-
target=target,
|
|
89
|
-
target_origin=target_origin,
|
|
90
|
-
type=type
|
|
91
|
-
))
|
|
92
|
-
return result
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def render_dot(self):
|
|
96
|
-
self.fk_set = {
|
|
97
|
-
full_class_name(entity.kls): set([rel.field for rel in entity.relationships])
|
|
98
|
-
for entity in self.er_diagram.configs
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
for entity in self.er_diagram.configs:
|
|
102
|
-
self.analysis_entity(entity)
|
|
103
|
-
renderer = Renderer(show_fields=self.show_field, show_module=self.show_module)
|
|
104
|
-
return renderer.render_dot([], [], list(self.node_set.values()), self.links)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def get_fields(schema: type[BaseModel], fk_set: set[str] | None = None) -> list[FieldInfo]:
|
|
108
|
-
|
|
109
|
-
fields: list[FieldInfo] = []
|
|
110
|
-
for k, v in schema.model_fields.items():
|
|
111
|
-
anno = v.annotation
|
|
112
|
-
fields.append(FieldInfo(
|
|
113
|
-
is_object=k in fk_set if fk_set is not None else False,
|
|
114
|
-
name=k,
|
|
115
|
-
from_base=False,
|
|
116
|
-
type_name=get_type_name(anno),
|
|
117
|
-
is_exclude=bool(v.exclude)
|
|
118
|
-
))
|
|
119
|
-
return fields
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/component/render-graph.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/icon/apple-touch-icon.png
RENAMED
|
File without changes
|
{fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/icon/favicon-16x16.png
RENAMED
|
File without changes
|
{fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/icon/favicon-32x32.png
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.13.0 → fastapi_voyager-0.13.1}/src/fastapi_voyager/web/icon/site.webmanifest
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|