fastapi-voyager 0.13.3__py3-none-any.whl → 0.14.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/pydantic_resolve_util.py +99 -0
- fastapi_voyager/render.py +44 -5
- fastapi_voyager/server.py +11 -2
- fastapi_voyager/type.py +8 -0
- fastapi_voyager/type_helper.py +4 -1
- fastapi_voyager/version.py +1 -1
- fastapi_voyager/voyager.py +8 -1
- fastapi_voyager/web/graph-ui.js +0 -5
- fastapi_voyager/web/index.html +20 -0
- fastapi_voyager/web/store.js +3 -0
- fastapi_voyager/web/vue-main.js +15 -0
- {fastapi_voyager-0.13.3.dist-info → fastapi_voyager-0.14.1.dist-info}/METADATA +14 -5
- {fastapi_voyager-0.13.3.dist-info → fastapi_voyager-0.14.1.dist-info}/RECORD +16 -15
- {fastapi_voyager-0.13.3.dist-info → fastapi_voyager-0.14.1.dist-info}/WHEEL +0 -0
- {fastapi_voyager-0.13.3.dist-info → fastapi_voyager-0.14.1.dist-info}/entry_points.txt +0 -0
- {fastapi_voyager-0.13.3.dist-info → fastapi_voyager-0.14.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from pydantic.fields import FieldInfo
|
|
4
|
+
from pydantic_resolve.utils.er_diagram import LoaderInfo
|
|
5
|
+
import pydantic_resolve.constant as const
|
|
6
|
+
from pydantic_resolve.utils.expose import ExposeInfo
|
|
7
|
+
from pydantic_resolve.utils.collector import SendToInfo, ICollector
|
|
8
|
+
|
|
9
|
+
def analysis_pydantic_resolve_fields(schema: type[BaseModel], field: str):
|
|
10
|
+
"""
|
|
11
|
+
get information for pydantic resolve specific info
|
|
12
|
+
in future, this function will be provide by pydantic-resolve package
|
|
13
|
+
|
|
14
|
+
is_resolve: bool = False
|
|
15
|
+
- check existence of def resolve_{field} method
|
|
16
|
+
- check existence of LoaderInfo in field.metadata
|
|
17
|
+
|
|
18
|
+
is_post: bool = False
|
|
19
|
+
- check existence of def post_{field} method
|
|
20
|
+
|
|
21
|
+
expose_as_info: str | None = None
|
|
22
|
+
- check ExposeInfo in field.metadata
|
|
23
|
+
- check field in schema.__pydantic_resolve_expose__ (const.EXPOSE_TO_DESCENDANT)
|
|
24
|
+
|
|
25
|
+
send_to_info: list[str] | None = None
|
|
26
|
+
- check SendToInfo in field.metadata
|
|
27
|
+
- check field in schema.__pydantic_resolve_collect__ (const.COLLECTOR_CONFIGURATION)
|
|
28
|
+
|
|
29
|
+
collect_info: list[str] | None = None
|
|
30
|
+
- 1. check existence of def post_{field} method
|
|
31
|
+
- 2. get the signature of this method
|
|
32
|
+
- 3. extrace the collector names from the parameters with ICollector metadata
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
return dict in form of
|
|
37
|
+
{
|
|
38
|
+
"is_resolve": True,
|
|
39
|
+
...
|
|
40
|
+
}
|
|
41
|
+
"""
|
|
42
|
+
has_meta = False
|
|
43
|
+
field_info: FieldInfo = schema.model_fields.get(field)
|
|
44
|
+
|
|
45
|
+
is_resolve = hasattr(schema, f'{const.RESOLVE_PREFIX}{field}')
|
|
46
|
+
is_post = hasattr(schema, f'{const.POST_PREFIX}{field}')
|
|
47
|
+
expose_as_info = None
|
|
48
|
+
send_to_info = None
|
|
49
|
+
post_collector = []
|
|
50
|
+
|
|
51
|
+
send_to_info_list = []
|
|
52
|
+
|
|
53
|
+
if field_info:
|
|
54
|
+
# Check metadata
|
|
55
|
+
for meta in field_info.metadata:
|
|
56
|
+
if isinstance(meta, LoaderInfo):
|
|
57
|
+
is_resolve = True
|
|
58
|
+
if isinstance(meta, ExposeInfo):
|
|
59
|
+
expose_as_info = meta.alias
|
|
60
|
+
if isinstance(meta, SendToInfo):
|
|
61
|
+
if isinstance(meta.collector_name, str):
|
|
62
|
+
send_to_info_list.append(meta.collector_name)
|
|
63
|
+
else:
|
|
64
|
+
send_to_info_list.extend(list(meta.collector_name))
|
|
65
|
+
|
|
66
|
+
# Check class attributes
|
|
67
|
+
expose_dict = getattr(schema, const.EXPOSE_TO_DESCENDANT, {})
|
|
68
|
+
if field in expose_dict:
|
|
69
|
+
expose_as_info = expose_dict[field]
|
|
70
|
+
|
|
71
|
+
collect_dict = getattr(schema, const.COLLECTOR_CONFIGURATION, {})
|
|
72
|
+
|
|
73
|
+
for keys, collectors in collect_dict.items():
|
|
74
|
+
target_keys = [keys] if isinstance(keys, str) else list(keys)
|
|
75
|
+
if field in target_keys:
|
|
76
|
+
if isinstance(collectors, str):
|
|
77
|
+
send_to_info_list.append(collectors)
|
|
78
|
+
else:
|
|
79
|
+
send_to_info_list.extend(list(collectors))
|
|
80
|
+
|
|
81
|
+
if send_to_info_list:
|
|
82
|
+
send_to_info = list(set(send_to_info_list)) # unique collectors
|
|
83
|
+
|
|
84
|
+
if is_post:
|
|
85
|
+
post_method = getattr(schema, f'{const.POST_PREFIX}{field}')
|
|
86
|
+
for _, param in inspect.signature(post_method).parameters.items():
|
|
87
|
+
if isinstance(param.default, ICollector):
|
|
88
|
+
post_collector.append(param.default.alias)
|
|
89
|
+
|
|
90
|
+
has_meta = any([is_resolve, is_post, expose_as_info, send_to_info])
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"has_pydantic_resolve_meta": has_meta,
|
|
94
|
+
"is_resolve": is_resolve,
|
|
95
|
+
"is_post": is_post,
|
|
96
|
+
"expose_as_info": expose_as_info,
|
|
97
|
+
"send_to_info": send_to_info,
|
|
98
|
+
"collect_info": None if len(post_collector) == 0 else post_collector
|
|
99
|
+
}
|
fastapi_voyager/render.py
CHANGED
|
@@ -5,6 +5,7 @@ from fastapi_voyager.type import (
|
|
|
5
5
|
PK,
|
|
6
6
|
FieldType,
|
|
7
7
|
Link,
|
|
8
|
+
FieldInfo,
|
|
8
9
|
ModuleNode,
|
|
9
10
|
ModuleRoute,
|
|
10
11
|
Route,
|
|
@@ -22,24 +23,62 @@ class Renderer:
|
|
|
22
23
|
show_fields: FieldType = 'single',
|
|
23
24
|
module_color: dict[str, str] | None = None,
|
|
24
25
|
schema: str | None = None,
|
|
25
|
-
show_module: bool = True
|
|
26
|
+
show_module: bool = True,
|
|
27
|
+
show_pydantic_resolve_meta: bool = False,
|
|
26
28
|
) -> None:
|
|
27
29
|
self.show_fields = show_fields if show_fields in ('single', 'object', 'all') else 'single'
|
|
28
30
|
self.module_color = module_color or {}
|
|
29
31
|
self.schema = schema
|
|
30
32
|
self.show_module = show_module
|
|
33
|
+
self.show_pydantic_resolve_meta = show_pydantic_resolve_meta
|
|
31
34
|
|
|
32
35
|
logger.info(f'show_module: {self.show_module}')
|
|
33
36
|
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
|
+
|
|
42
|
+
parts: list[str] = []
|
|
43
|
+
if field.is_resolve:
|
|
44
|
+
parts.append('<font color="#47a80f"> ● resolve</font>')
|
|
45
|
+
if field.is_post:
|
|
46
|
+
parts.append('<font color="#427fa4"> ● post</font>')
|
|
47
|
+
if field.expose_as_info:
|
|
48
|
+
parts.append(f'<font color="#895cb9"> ● expose as: {field.expose_as_info}</font>')
|
|
49
|
+
if field.send_to_info:
|
|
50
|
+
to_collectors = ', '.join(field.send_to_info)
|
|
51
|
+
parts.append(f'<font color="#ca6d6d"> ● send to: {to_collectors}</font>')
|
|
52
|
+
if field.collect_info:
|
|
53
|
+
defined_collectors = ', '.join(field.collect_info)
|
|
54
|
+
parts.append(f'<font color="#777"> ● collectors: {defined_collectors}</font>')
|
|
55
|
+
|
|
56
|
+
if not parts:
|
|
57
|
+
return ''
|
|
58
|
+
|
|
59
|
+
return '<br align="left"/><br align="left"/>' + '<br align="left"/>'.join(parts) + '<br align="left"/>'
|
|
34
60
|
|
|
35
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
|
+
"""
|
|
65
|
+
|
|
36
66
|
has_base_fields = any(f.from_base for f in node.fields)
|
|
37
|
-
|
|
67
|
+
|
|
68
|
+
# if self.show_pydantic_resolve_meta, show all fields with resolve/post/expose/collector info
|
|
69
|
+
if self.show_pydantic_resolve_meta:
|
|
70
|
+
fields = [n for n in node.fields if n.has_pydantic_resolve_meta is True or n.from_base is False]
|
|
71
|
+
else:
|
|
72
|
+
fields = [n for n in node.fields if n.from_base is False]
|
|
38
73
|
|
|
39
74
|
if self.show_fields == 'all':
|
|
40
75
|
_fields = fields
|
|
41
76
|
elif self.show_fields == 'object':
|
|
42
|
-
|
|
77
|
+
if self.show_pydantic_resolve_meta:
|
|
78
|
+
# to better display resolve meta info
|
|
79
|
+
_fields = [f for f in fields if f.is_object is True or f.has_pydantic_resolve_meta is True]
|
|
80
|
+
else:
|
|
81
|
+
_fields = [f for f in fields if f.is_object is True]
|
|
43
82
|
else: # 'single'
|
|
44
83
|
_fields = []
|
|
45
84
|
|
|
@@ -49,8 +88,8 @@ class Renderer:
|
|
|
49
88
|
|
|
50
89
|
for field in _fields:
|
|
51
90
|
type_name = field.type_name[:25] + '..' if len(field.type_name) > 25 else field.type_name
|
|
52
|
-
display_xml = f'<s align="left">{field.name}: {type_name}</s>' if field.is_exclude else f'{field.name}: {type_name}'
|
|
53
|
-
field_str = f"""<tr><td align="left" port="f{field.name}" cellpadding="8"><font> {display_xml}
|
|
91
|
+
display_xml = f'<s align="left">{field.name}: {type_name} </s>' if field.is_exclude else f'{field.name}: {type_name}'
|
|
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>"""
|
|
54
93
|
fields_parts.append(field_str)
|
|
55
94
|
|
|
56
95
|
default_color = '#009485' if color is None else color
|
fastapi_voyager/server.py
CHANGED
|
@@ -47,6 +47,7 @@ class OptionParam(BaseModel):
|
|
|
47
47
|
initial_page_policy: INITIAL_PAGE_POLICY
|
|
48
48
|
swagger_url: str | None = None
|
|
49
49
|
has_er_diagram: bool = False
|
|
50
|
+
enable_pydantic_resolve_meta: bool = False
|
|
50
51
|
|
|
51
52
|
class Payload(BaseModel):
|
|
52
53
|
tags: list[str] | None = None
|
|
@@ -57,6 +58,7 @@ class Payload(BaseModel):
|
|
|
57
58
|
brief: bool = False
|
|
58
59
|
hide_primitive_route: bool = False
|
|
59
60
|
show_module: bool = True
|
|
61
|
+
show_pydantic_resolve_meta: bool = False
|
|
60
62
|
|
|
61
63
|
# ---------- search ----------
|
|
62
64
|
class SearchResultOptionParam(BaseModel):
|
|
@@ -86,6 +88,7 @@ def create_voyager(
|
|
|
86
88
|
initial_page_policy: INITIAL_PAGE_POLICY = 'first',
|
|
87
89
|
ga_id: str | None = None,
|
|
88
90
|
er_diagram: ErDiagram | None = None,
|
|
91
|
+
enable_pydantic_resolve_meta: bool = False,
|
|
89
92
|
) -> FastAPI:
|
|
90
93
|
router = APIRouter(tags=['fastapi-voyager'])
|
|
91
94
|
|
|
@@ -121,7 +124,8 @@ def create_voyager(
|
|
|
121
124
|
version=__version__,
|
|
122
125
|
swagger_url=swagger_url,
|
|
123
126
|
initial_page_policy=initial_page_policy,
|
|
124
|
-
has_er_diagram=er_diagram is not None
|
|
127
|
+
has_er_diagram=er_diagram is not None,
|
|
128
|
+
enable_pydantic_resolve_meta=enable_pydantic_resolve_meta)
|
|
125
129
|
|
|
126
130
|
|
|
127
131
|
@router.post("/dot-search", response_model=SearchResultOptionParam)
|
|
@@ -133,6 +137,7 @@ def create_voyager(
|
|
|
133
137
|
module_color=module_color,
|
|
134
138
|
hide_primitive_route=payload.hide_primitive_route,
|
|
135
139
|
show_module=payload.show_module,
|
|
140
|
+
show_pydantic_resolve_meta=payload.show_pydantic_resolve_meta,
|
|
136
141
|
)
|
|
137
142
|
voyager.analysis(target_app)
|
|
138
143
|
tags = voyager.calculate_filtered_tag_and_route()
|
|
@@ -154,6 +159,7 @@ def create_voyager(
|
|
|
154
159
|
route_name=payload.route_name,
|
|
155
160
|
hide_primitive_route=payload.hide_primitive_route,
|
|
156
161
|
show_module=payload.show_module,
|
|
162
|
+
show_pydantic_resolve_meta=payload.show_pydantic_resolve_meta,
|
|
157
163
|
)
|
|
158
164
|
voyager.analysis(target_app)
|
|
159
165
|
if payload.brief:
|
|
@@ -179,7 +185,10 @@ def create_voyager(
|
|
|
179
185
|
|
|
180
186
|
@router.post('/dot-render-core-data', response_class=PlainTextResponse)
|
|
181
187
|
def render_dot_from_core_data(core_data: CoreData) -> str:
|
|
182
|
-
renderer = Renderer(
|
|
188
|
+
renderer = Renderer(
|
|
189
|
+
show_fields=core_data.show_fields,
|
|
190
|
+
module_color=core_data.module_color,
|
|
191
|
+
schema=core_data.schema)
|
|
183
192
|
return renderer.render_dot(core_data.tags, core_data.routes, core_data.nodes, core_data.links)
|
|
184
193
|
|
|
185
194
|
@router.get("/", response_class=HTMLResponse)
|
fastapi_voyager/type.py
CHANGED
|
@@ -18,6 +18,14 @@ class FieldInfo:
|
|
|
18
18
|
is_exclude: bool = False
|
|
19
19
|
desc: str = ''
|
|
20
20
|
|
|
21
|
+
# pydantic resolve specific fields
|
|
22
|
+
has_pydantic_resolve_meta: bool = False # overall flag
|
|
23
|
+
is_resolve: bool = False
|
|
24
|
+
is_post: bool = False
|
|
25
|
+
expose_as_info: str | None = None
|
|
26
|
+
send_to_info: list[str] | None = None
|
|
27
|
+
collect_info: list[str] | None = None
|
|
28
|
+
|
|
21
29
|
@dataclass
|
|
22
30
|
class Tag(NodeBase):
|
|
23
31
|
routes: list['Route'] # route.id
|
fastapi_voyager/type_helper.py
CHANGED
|
@@ -8,6 +8,7 @@ import pydantic_resolve.constant as const
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
10
|
from fastapi_voyager.type import FieldInfo
|
|
11
|
+
from fastapi_voyager.pydantic_resolve_util import analysis_pydantic_resolve_fields
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
13
14
|
|
|
@@ -177,13 +178,15 @@ def get_pydantic_fields(schema: type[BaseModel], bases_fields: set[str]) -> list
|
|
|
177
178
|
fields: list[FieldInfo] = []
|
|
178
179
|
for k, v in schema.model_fields.items():
|
|
179
180
|
anno = v.annotation
|
|
181
|
+
pydantic_resolve_specific_params = analysis_pydantic_resolve_fields(schema, k)
|
|
180
182
|
fields.append(FieldInfo(
|
|
181
183
|
is_object=_is_object(anno),
|
|
182
184
|
name=k,
|
|
183
185
|
from_base=k in bases_fields,
|
|
184
186
|
type_name=get_type_name(anno),
|
|
185
187
|
is_exclude=bool(v.exclude),
|
|
186
|
-
desc=v.description or ''
|
|
188
|
+
desc=v.description or '',
|
|
189
|
+
**pydantic_resolve_specific_params
|
|
187
190
|
))
|
|
188
191
|
return fields
|
|
189
192
|
|
fastapi_voyager/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.14.1"
|
fastapi_voyager/voyager.py
CHANGED
|
@@ -33,6 +33,7 @@ class Voyager:
|
|
|
33
33
|
route_name: str | None = None,
|
|
34
34
|
hide_primitive_route: bool = False,
|
|
35
35
|
show_module: bool = True,
|
|
36
|
+
show_pydantic_resolve_meta: bool = False,
|
|
36
37
|
):
|
|
37
38
|
|
|
38
39
|
self.routes: list[Route] = []
|
|
@@ -55,6 +56,7 @@ class Voyager:
|
|
|
55
56
|
self.route_name = route_name
|
|
56
57
|
self.hide_primitive_route = hide_primitive_route
|
|
57
58
|
self.show_module = show_module
|
|
59
|
+
self.show_pydantic_resolve_meta = show_pydantic_resolve_meta
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
def _get_available_route(self, app: FastAPI):
|
|
@@ -319,7 +321,12 @@ class Voyager:
|
|
|
319
321
|
node_set=self.node_set,
|
|
320
322
|
)
|
|
321
323
|
|
|
322
|
-
renderer = Renderer(
|
|
324
|
+
renderer = Renderer(
|
|
325
|
+
show_fields=self.show_fields,
|
|
326
|
+
module_color=self.module_color,
|
|
327
|
+
schema=self.schema,
|
|
328
|
+
show_module=self.show_module,
|
|
329
|
+
show_pydantic_resolve_meta=self.show_pydantic_resolve_meta)
|
|
323
330
|
|
|
324
331
|
_tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
|
|
325
332
|
return renderer.render_dot(_tags, _routes, _nodes, _links)
|
fastapi_voyager/web/graph-ui.js
CHANGED
fastapi_voyager/web/index.html
CHANGED
|
@@ -62,6 +62,15 @@
|
|
|
62
62
|
border-top: 0; */
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
.inherit-flow {
|
|
66
|
+
/* stroke-width:2; */
|
|
67
|
+
stroke-dasharray: 8 6; /* dash pattern */
|
|
68
|
+
stroke-linecap: round;
|
|
69
|
+
animation: dash 2s linear infinite;
|
|
70
|
+
animation-direction: reverse;
|
|
71
|
+
}
|
|
72
|
+
@keyframes dash { to { stroke-dashoffset: -14; } }
|
|
73
|
+
|
|
65
74
|
.adjust-fit {
|
|
66
75
|
height: calc(100vh - 54px);
|
|
67
76
|
}
|
|
@@ -140,6 +149,7 @@
|
|
|
140
149
|
</div>
|
|
141
150
|
<div class="col-auto row items-center q-gutter-sm">
|
|
142
151
|
<q-select
|
|
152
|
+
v-show="!store.state.search.invisible"
|
|
143
153
|
dense
|
|
144
154
|
outlined
|
|
145
155
|
use-input
|
|
@@ -170,6 +180,7 @@
|
|
|
170
180
|
</q-select>
|
|
171
181
|
|
|
172
182
|
<q-select
|
|
183
|
+
v-show="!store.state.search.invisible"
|
|
173
184
|
dense
|
|
174
185
|
outlined
|
|
175
186
|
v-model="store.state.search.fieldName"
|
|
@@ -380,6 +391,15 @@
|
|
|
380
391
|
title="show module cluster"
|
|
381
392
|
/>
|
|
382
393
|
</div>
|
|
394
|
+
<div class="q-mt-sm" v-if="store.state.mode == 'voyager' && store.state.config.enable_pydantic_resolve_meta">
|
|
395
|
+
<q-toggle
|
|
396
|
+
v-model="store.state.modeControl.pydanticResolveMetaEnabled"
|
|
397
|
+
@update:model-value="(val) => togglePydanticResolveMeta(val)"
|
|
398
|
+
label="Pydantic Resolve Meta"
|
|
399
|
+
dense
|
|
400
|
+
title="show fields with pydantic resolve/post/expose/collector info"
|
|
401
|
+
/>
|
|
402
|
+
</div>
|
|
383
403
|
</div>
|
|
384
404
|
</template>
|
|
385
405
|
</q-splitter>
|
fastapi_voyager/web/store.js
CHANGED
|
@@ -9,6 +9,7 @@ const state = reactive({
|
|
|
9
9
|
config: {
|
|
10
10
|
initial_page_policy: 'first',
|
|
11
11
|
has_er_diagram: false,
|
|
12
|
+
enable_pydantic_resolve_meta: false,
|
|
12
13
|
},
|
|
13
14
|
|
|
14
15
|
mode: 'voyager', // voyager / er-diagram
|
|
@@ -55,6 +56,7 @@ const state = reactive({
|
|
|
55
56
|
// schema options, schema, fields
|
|
56
57
|
search: {
|
|
57
58
|
mode: false,
|
|
59
|
+
invisible: false,
|
|
58
60
|
schemaName: null,
|
|
59
61
|
fieldName: null,
|
|
60
62
|
schemaOptions: [],
|
|
@@ -90,6 +92,7 @@ const state = reactive({
|
|
|
90
92
|
modeControl: {
|
|
91
93
|
focus: false, // control the schema param
|
|
92
94
|
briefModeEnabled: false, // show brief mode toggle
|
|
95
|
+
pydanticResolveMetaEnabled: false, // show pydantic resolve meta toggle
|
|
93
96
|
},
|
|
94
97
|
|
|
95
98
|
// api filters
|
fastapi_voyager/web/vue-main.js
CHANGED
|
@@ -244,6 +244,7 @@ const app = createApp({
|
|
|
244
244
|
store.state.version = data.version || "";
|
|
245
245
|
store.state.swagger.url = data.swagger_url || null;
|
|
246
246
|
store.state.config.has_er_diagram = data.has_er_diagram || false;
|
|
247
|
+
store.state.config.enable_pydantic_resolve_meta = data.enable_pydantic_resolve_meta || false;
|
|
247
248
|
|
|
248
249
|
rebuildSchemaOptions();
|
|
249
250
|
|
|
@@ -314,6 +315,7 @@ const app = createApp({
|
|
|
314
315
|
brief: store.state.filter.brief,
|
|
315
316
|
hide_primitive_route: store.state.filter.hidePrimitiveRoute,
|
|
316
317
|
show_module: store.state.filter.showModule,
|
|
318
|
+
show_pydantic_resolve_meta: store.state.modeControl.pydanticResolveMetaEnabled
|
|
317
319
|
};
|
|
318
320
|
initGraphUI();
|
|
319
321
|
const res = await fetch("dot", {
|
|
@@ -345,6 +347,11 @@ const app = createApp({
|
|
|
345
347
|
onGenerate()
|
|
346
348
|
}
|
|
347
349
|
|
|
350
|
+
async function togglePydanticResolveMeta(val) {
|
|
351
|
+
store.state.modeControl.pydanticResolveMetaEnabled = val;
|
|
352
|
+
onGenerate();
|
|
353
|
+
}
|
|
354
|
+
|
|
348
355
|
async function renderErDiagram(resetZoom = true) {
|
|
349
356
|
initGraphUI();
|
|
350
357
|
erDiagramLoading.value = true;
|
|
@@ -373,12 +380,19 @@ const app = createApp({
|
|
|
373
380
|
|
|
374
381
|
async function onModeChange(val) {
|
|
375
382
|
if (val === "er-diagram") {
|
|
383
|
+
// clear search
|
|
384
|
+
store.state.search.schemaName = null
|
|
385
|
+
store.state.search.fieldName = null
|
|
386
|
+
store.state.search.invisible = true
|
|
387
|
+
|
|
376
388
|
if (store.state.leftPanel.width > 0) {
|
|
377
389
|
store.state.leftPanel.previousWidth = store.state.leftPanel.width;
|
|
378
390
|
}
|
|
379
391
|
store.state.leftPanel.width = 0;
|
|
380
392
|
await renderErDiagram();
|
|
381
393
|
} else {
|
|
394
|
+
store.state.search.invisible = false
|
|
395
|
+
|
|
382
396
|
const fallbackWidth = store.state.leftPanel.previousWidth || 300;
|
|
383
397
|
store.state.leftPanel.width = fallbackWidth;
|
|
384
398
|
await onGenerate();
|
|
@@ -525,6 +539,7 @@ const app = createApp({
|
|
|
525
539
|
toggleShowModule,
|
|
526
540
|
onModeChange,
|
|
527
541
|
renderErDiagram,
|
|
542
|
+
togglePydanticResolveMeta
|
|
528
543
|
};
|
|
529
544
|
},
|
|
530
545
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-voyager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.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
|
|
@@ -19,7 +19,7 @@ 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: pydantic-resolve>=2.
|
|
22
|
+
Requires-Dist: pydantic-resolve>=2.3.1
|
|
23
23
|
Provides-Extra: dev
|
|
24
24
|
Requires-Dist: pytest; extra == 'dev'
|
|
25
25
|
Requires-Dist: ruff; extra == 'dev'
|
|
@@ -33,6 +33,8 @@ Description-Content-Type: text/markdown
|
|
|
33
33
|
|
|
34
34
|
Visualize your FastAPI endpoints, and explore them interactively.
|
|
35
35
|
|
|
36
|
+
Its vision is to make code easier to read and understand, serving as an ideal documentation tool.
|
|
37
|
+
|
|
36
38
|
> This repo is still in early stage, it supports pydantic v2 only
|
|
37
39
|
|
|
38
40
|
visit [live demo](https://www.newsyeah.fun/voyager/)
|
|
@@ -51,7 +53,8 @@ app.mount('/voyager',
|
|
|
51
53
|
swagger_url="/docs",
|
|
52
54
|
ga_id="G-XXXXXXXXVL",
|
|
53
55
|
initial_page_policy='first',
|
|
54
|
-
online_repo_url='https://github.com/allmonday/composition-oriented-development-pattern/blob/master'
|
|
56
|
+
online_repo_url='https://github.com/allmonday/composition-oriented-development-pattern/blob/master',
|
|
57
|
+
enable_pydantic_resolve_meta=True))
|
|
55
58
|
```
|
|
56
59
|
|
|
57
60
|
https://github.com/allmonday/composition-oriented-development-pattern/blob/master/src/main.py#L48
|
|
@@ -91,6 +94,7 @@ It is also an architecture tool that can identify issues inside implementation,
|
|
|
91
94
|
|
|
92
95
|
Given ErDiagram defined by pydantic-resolve, application level entity relationship diagram can be visualized too.
|
|
93
96
|
|
|
97
|
+
|
|
94
98
|
### highlight nodes and links
|
|
95
99
|
click a node to highlight it's upperstream and downstream nodes. figure out the related models of one page, or homw many pages are related with one model.
|
|
96
100
|
|
|
@@ -155,6 +159,11 @@ app.mount('/voyager',
|
|
|
155
159
|
|
|
156
160
|
<img width="1276" height="613" alt="image" src="https://github.com/user-attachments/assets/ea0091bb-ee11-4f71-8be3-7129d956c910" />
|
|
157
161
|
|
|
162
|
+
### Show pydantic resolve meta info
|
|
163
|
+
|
|
164
|
+
setting `enable_pydantic_resolve_meta=True` in `create_voyager`, toggle `pydantic resolve meta`.
|
|
165
|
+
|
|
166
|
+
<img width="1604" height="535" alt="image" src="https://github.com/user-attachments/assets/d1639555-af41-4a08-9970-4b8ef314596a" />
|
|
158
167
|
|
|
159
168
|
|
|
160
169
|
## Command Line Usage
|
|
@@ -190,9 +199,9 @@ voyager --help
|
|
|
190
199
|
|
|
191
200
|
## About pydantic-resolve
|
|
192
201
|
|
|
193
|
-
pydantic-resolve
|
|
202
|
+
pydantic-resolve is a lightweight tool designed to build complex, nested data in a simple, declarative way. In v2 it introduced an important feature: ER Diagram, and fastapi-voyager has supported this feature, allowing for a clearer understanding of the business relationships.
|
|
194
203
|
|
|
195
|
-
pydantic-resolve
|
|
204
|
+
pydantic-resolve's ~~`@ensure_subset` decorator~~ `DefineSubset` metaclass helps safely pick fields from the 'source class' while **indicating the reference** from the current class to the base class.
|
|
196
205
|
|
|
197
206
|
Developers can use fastapi-voyager without needing to know anything about pydantic-resolve, but I still highly recommend everyone to give it a try.
|
|
198
207
|
|
|
@@ -3,20 +3,21 @@ fastapi_voyager/cli.py,sha256=td3yIIigEomhSdDO-Xkh-CgpEwCafwlwnpvxnT9QsBo,10488
|
|
|
3
3
|
fastapi_voyager/er_diagram.py,sha256=cMiNKk4ufSM147ldvvfdqfv34Q5mj533VcELsp4Gwwc,10384
|
|
4
4
|
fastapi_voyager/filter.py,sha256=AN_HIu8-DtKisIq5mFt7CnqRHtxKewedNGyyaI82hSY,11529
|
|
5
5
|
fastapi_voyager/module.py,sha256=h9YR3BpS-CAcJW9WCdVkF4opqwY32w9T67g9GfdLytk,3425
|
|
6
|
-
fastapi_voyager/
|
|
7
|
-
fastapi_voyager/
|
|
8
|
-
fastapi_voyager/
|
|
9
|
-
fastapi_voyager/
|
|
10
|
-
fastapi_voyager/
|
|
11
|
-
fastapi_voyager/
|
|
12
|
-
fastapi_voyager/
|
|
6
|
+
fastapi_voyager/pydantic_resolve_util.py,sha256=r4Rq7BtBcFOMV7O2Ab9TwLyRNL1yNDiQlGUVybf-sXs,3524
|
|
7
|
+
fastapi_voyager/render.py,sha256=jST413qpp4JEkgc6OftpWXYnjLkjTM4zUagAmZpQkLo,11791
|
|
8
|
+
fastapi_voyager/server.py,sha256=DN4KP37lD_lt4ISg7yoJwzVvw3nKn6qr9C6aH75mR-g,8928
|
|
9
|
+
fastapi_voyager/type.py,sha256=zluWvh5vpnjXJ9aAmyNJTSmXZPjAHCvgRT5oQRAjHrg,2104
|
|
10
|
+
fastapi_voyager/type_helper.py,sha256=FmfrZAI3Z4uDdh3sH_kH7UGoY6yNVPapneSN86qY_wo,10209
|
|
11
|
+
fastapi_voyager/version.py,sha256=MpoZvuQGxW8JYonuGmdFFp029C74ulzgiWGtVznOmBw,49
|
|
12
|
+
fastapi_voyager/voyager.py,sha256=4vonmL-xt54C5San-DRBq4mjoV8Q96eoWRy68MJ1IJw,14169
|
|
13
|
+
fastapi_voyager/web/graph-ui.js,sha256=DlSRHoTCpWMS6EQsW8naLr8yRn6ofF6wIUL-OsvEjvs,6480
|
|
13
14
|
fastapi_voyager/web/graphviz.svg.css,sha256=zDCjjpT0Idufu5YOiZI76PL70-avP3vTyzGPh9M85Do,1563
|
|
14
15
|
fastapi_voyager/web/graphviz.svg.js,sha256=wZwz_lBztoXmujEN21P0w-HMpdmbqPwTQQ6Ebxd9rGo,18569
|
|
15
|
-
fastapi_voyager/web/index.html,sha256=
|
|
16
|
+
fastapi_voyager/web/index.html,sha256=wRfWeAEJI0ZGUPjIlVNn0S6NfHb59FOgnTB_o0kvtY4,20264
|
|
16
17
|
fastapi_voyager/web/quasar.min.css,sha256=F5jQe7X2XT54VlvAaa2V3GsBFdVD-vxDZeaPLf6U9CU,203145
|
|
17
18
|
fastapi_voyager/web/quasar.min.js,sha256=h0ftyPMW_CRiyzeVfQqiup0vrVt4_QWojpqmpnpn07E,502974
|
|
18
|
-
fastapi_voyager/web/store.js,sha256=
|
|
19
|
-
fastapi_voyager/web/vue-main.js,sha256=
|
|
19
|
+
fastapi_voyager/web/store.js,sha256=vdQ-375K1AQcSdR0qTKkYzNTKOdJLwu6teHdpifN7cE,2217
|
|
20
|
+
fastapi_voyager/web/vue-main.js,sha256=Cuf1gfb1kDEBgGciFaoqMfQICQnCzAcs8QyillLq2VU,17521
|
|
20
21
|
fastapi_voyager/web/component/demo.js,sha256=bQb16Un4XZ3Mf8qL6gvyrXe_mmA3V3mSIRMQAWg2MNk,352
|
|
21
22
|
fastapi_voyager/web/component/render-graph.js,sha256=e8Xgh2Kl-nYU0P1gstEmAepCgFnk2J6UdxW8TlMafGs,2322
|
|
22
23
|
fastapi_voyager/web/component/route-code-display.js,sha256=8NJPPjNRUC21gjpY8XYEQs4RBbhX1pCiqEhJp39ku6k,3678
|
|
@@ -28,8 +29,8 @@ fastapi_voyager/web/icon/favicon-16x16.png,sha256=JC07jEzfIYxBIoQn_FHXvyHuxESdhW
|
|
|
28
29
|
fastapi_voyager/web/icon/favicon-32x32.png,sha256=C7v1h58cfWOsiLp9yOIZtlx-dLasBcq3NqpHVGRmpt4,1859
|
|
29
30
|
fastapi_voyager/web/icon/favicon.ico,sha256=tZolYIXkkBcFiYl1A8ksaXN2VjGamzcSdes838dLvNc,15406
|
|
30
31
|
fastapi_voyager/web/icon/site.webmanifest,sha256=ep4Hzh9zhmiZF2At3Fp1dQrYQuYF_3ZPZxc1KcGBvwQ,263
|
|
31
|
-
fastapi_voyager-0.
|
|
32
|
-
fastapi_voyager-0.
|
|
33
|
-
fastapi_voyager-0.
|
|
34
|
-
fastapi_voyager-0.
|
|
35
|
-
fastapi_voyager-0.
|
|
32
|
+
fastapi_voyager-0.14.1.dist-info/METADATA,sha256=jYSRfxQ8vzsbmklJqGbFNJy4k5JeDNQLYyh8ra23I4A,8184
|
|
33
|
+
fastapi_voyager-0.14.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
34
|
+
fastapi_voyager-0.14.1.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
|
|
35
|
+
fastapi_voyager-0.14.1.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
|
|
36
|
+
fastapi_voyager-0.14.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|