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.
@@ -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.render import Renderer
11
- from fastapi_voyager.type import FieldInfo
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
- 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')
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 = Renderer(show_fields=self.show_field, show_module=self.show_module)
104
- return renderer.render_dot([], [], list(self.node_set.values()), self.links)
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
@@ -67,6 +67,7 @@ class Link:
67
67
  source_origin: str
68
68
  target_origin: str
69
69
  type: LinkType
70
+ label: str | None = None
70
71
 
71
72
  FieldType = Literal['single', 'object', 'all']
72
73
  PK = "PK"
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.13.0"
2
+ __version__ = "0.13.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.13.0
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=GoTuriRTBk0Th_-ireuLHdzncjbk3WbIns8FVzeJz28,4064
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=7EL1zaIwKVRGpLig7fqaOrZGN5k0Rm31C9COfck3CSs,1750
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=jHHU_F0-L0N4sHMgRz3qTQ0PPdki_7FNU9kR-UybYwM,49
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.0.dist-info/METADATA,sha256=uKfxGwFJQZC7LY3Es7RuWhccR5OBPm8s1m7kmrjtokI,6521
32
- fastapi_voyager-0.13.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
33
- fastapi_voyager-0.13.0.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
34
- fastapi_voyager-0.13.0.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
35
- fastapi_voyager-0.13.0.dist-info/RECORD,,
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,,