fastapi-voyager 0.13.3__tar.gz → 0.14.0__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.3 → fastapi_voyager-0.14.0}/PKG-INFO +7 -4
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/README.md +5 -2
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/docs/changelog.md +11 -6
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/docs/idea.md +4 -1
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/pyproject.toml +1 -1
- fastapi_voyager-0.14.0/src/fastapi_voyager/pydantic_resolve_util.py +99 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/render.py +44 -5
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/server.py +11 -2
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/type.py +8 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/type_helper.py +4 -1
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/version.py +1 -1
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/voyager.py +8 -1
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/index.html +9 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/store.js +2 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/vue-main.js +8 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/demo.py +25 -9
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/programatic.py +2 -1
- fastapi_voyager-0.14.0/tests/test_resolve_util_impl.py +68 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/.github/workflows/publish.yml +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/.gitignore +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/.python-version +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/CONTRIBUTING.md +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/LICENSE +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/release.md +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/__init__.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/cli.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/er_diagram.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/filter.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/module.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/component/demo.js +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/component/render-graph.js +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/graph-ui.js +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/quasar.min.css +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/quasar.min.js +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/__init__.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/demo_anno.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/service/__init__.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/service/schema/__init__.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/service/schema/base_entity.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/service/schema/extra.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/service/schema/schema.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/test_analysis.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/test_filter.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/test_generic.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/test_import.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/test_module.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/test_type_helper.py +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/uv.lock +0 -0
- {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/voyager.jpg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-voyager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
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/)
|
|
@@ -91,6 +93,7 @@ It is also an architecture tool that can identify issues inside implementation,
|
|
|
91
93
|
|
|
92
94
|
Given ErDiagram defined by pydantic-resolve, application level entity relationship diagram can be visualized too.
|
|
93
95
|
|
|
96
|
+
|
|
94
97
|
### highlight nodes and links
|
|
95
98
|
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
99
|
|
|
@@ -190,9 +193,9 @@ voyager --help
|
|
|
190
193
|
|
|
191
194
|
## About pydantic-resolve
|
|
192
195
|
|
|
193
|
-
pydantic-resolve
|
|
196
|
+
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
197
|
|
|
195
|
-
pydantic-resolve
|
|
198
|
+
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
199
|
|
|
197
200
|
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
201
|
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
Visualize your FastAPI endpoints, and explore them interactively.
|
|
7
7
|
|
|
8
|
+
Its vision is to make code easier to read and understand, serving as an ideal documentation tool.
|
|
9
|
+
|
|
8
10
|
> This repo is still in early stage, it supports pydantic v2 only
|
|
9
11
|
|
|
10
12
|
visit [live demo](https://www.newsyeah.fun/voyager/)
|
|
@@ -63,6 +65,7 @@ It is also an architecture tool that can identify issues inside implementation,
|
|
|
63
65
|
|
|
64
66
|
Given ErDiagram defined by pydantic-resolve, application level entity relationship diagram can be visualized too.
|
|
65
67
|
|
|
68
|
+
|
|
66
69
|
### highlight nodes and links
|
|
67
70
|
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.
|
|
68
71
|
|
|
@@ -162,9 +165,9 @@ voyager --help
|
|
|
162
165
|
|
|
163
166
|
## About pydantic-resolve
|
|
164
167
|
|
|
165
|
-
pydantic-resolve
|
|
168
|
+
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.
|
|
166
169
|
|
|
167
|
-
pydantic-resolve
|
|
170
|
+
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.
|
|
168
171
|
|
|
169
172
|
Developers can use fastapi-voyager without needing to know anything about pydantic-resolve, but I still highly recommend everyone to give it a try.
|
|
170
173
|
|
|
@@ -144,13 +144,18 @@
|
|
|
144
144
|
- [x] show dashed line for link without dataloader
|
|
145
145
|
- 0.13.3
|
|
146
146
|
- [x] show field description
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
- [
|
|
152
|
-
|
|
147
|
+
|
|
148
|
+
## 0.14, integration with pydantic-resolve
|
|
149
|
+
- 0.14.0
|
|
150
|
+
- [x] show hint for resolve (>), post fields (<), post default handler (* at title)
|
|
151
|
+
- [x] show expose and collect info
|
|
152
|
+
|
|
153
|
+
## 0.15, internal refactor
|
|
154
|
+
- 0.15.0
|
|
153
155
|
- [ ] refactor vue-main.js, move methods to store
|
|
154
156
|
- [ ] refactor render.py
|
|
157
|
+
- [ ] add tests
|
|
158
|
+
|
|
159
|
+
## 1.0, release
|
|
155
160
|
|
|
156
161
|
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
- [ ] set max limit for fields in nodes (? need further thinking)
|
|
15
15
|
- [ ] minimap (good to have)
|
|
16
16
|
- ref: https://observablehq.com/@rabelais/d3-js-zoom-minimap
|
|
17
|
-
- [ ] debug mode
|
|
17
|
+
- [ ] ~~debug mode~~
|
|
18
18
|
- [ ] export dot content, load dot content
|
|
19
|
+
- [ ] abstract voyager-core
|
|
20
|
+
- [ ] support fastapi-voyager
|
|
21
|
+
- [ ] support django-ninja-voyager
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
## in analysis
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.14.0"
|
|
@@ -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)
|
|
@@ -380,6 +380,15 @@
|
|
|
380
380
|
title="show module cluster"
|
|
381
381
|
/>
|
|
382
382
|
</div>
|
|
383
|
+
<div class="q-mt-sm" v-if="store.state.config.enable_pydantic_resolve_meta">
|
|
384
|
+
<q-toggle
|
|
385
|
+
v-model="store.state.modeControl.pydanticResolveMetaEnabled"
|
|
386
|
+
@update:model-value="(val) => togglePydanticResolveMeta(val)"
|
|
387
|
+
label="Pydantic Resolve Meta"
|
|
388
|
+
dense
|
|
389
|
+
title="show fields with pydantic resolve/post/expose/collector info"
|
|
390
|
+
/>
|
|
391
|
+
</div>
|
|
383
392
|
</div>
|
|
384
393
|
</template>
|
|
385
394
|
</q-splitter>
|
|
@@ -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
|
|
@@ -90,6 +91,7 @@ const state = reactive({
|
|
|
90
91
|
modeControl: {
|
|
91
92
|
focus: false, // control the schema param
|
|
92
93
|
briefModeEnabled: false, // show brief mode toggle
|
|
94
|
+
pydanticResolveMetaEnabled: false, // show pydantic resolve meta toggle
|
|
93
95
|
},
|
|
94
96
|
|
|
95
97
|
// api filters
|
|
@@ -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;
|
|
@@ -525,6 +532,7 @@ const app = createApp({
|
|
|
525
532
|
toggleShowModule,
|
|
526
533
|
onModeChange,
|
|
527
534
|
renderErDiagram,
|
|
535
|
+
togglePydanticResolveMeta
|
|
528
536
|
};
|
|
529
537
|
},
|
|
530
538
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from typing import Generic, TypeVar
|
|
2
|
+
from typing import Generic, TypeVar, Annotated
|
|
3
3
|
|
|
4
4
|
from fastapi import FastAPI
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
|
-
from pydantic_resolve import Resolver,
|
|
6
|
+
from pydantic_resolve import Resolver, DefineSubset, ExposeAs, SendTo, Collector
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
from tests.service.schema.schema import Member, Sprint, Story, Task
|
|
@@ -39,20 +39,36 @@ type TaskUnion = TaskA | TaskB
|
|
|
39
39
|
class PageTask(Task):
|
|
40
40
|
owner: PageMember | None
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
class MiddleStory(DefineSubset):
|
|
43
|
+
__subset__ = (Story, ('id', 'sprint_id', 'title'))
|
|
44
|
+
|
|
45
|
+
class PageStory(DefineSubset):
|
|
46
|
+
__subset__ = (Story, ('id', 'sprint_id'))
|
|
47
|
+
# __subset__ = (Story, ('id', 'sprint_id', 'title:exclude')) # expected behavior, but not supported yet
|
|
48
|
+
|
|
49
|
+
title: Annotated[str, ExposeAs('story_title')] = Field(exclude=True)
|
|
50
|
+
def post_title(self):
|
|
51
|
+
return self.title + ' (processed)'
|
|
52
|
+
|
|
53
|
+
desc: Annotated[str, ExposeAs('story_desc')] = ''
|
|
54
|
+
def resolve_desc(self):
|
|
55
|
+
return self.desc
|
|
47
56
|
|
|
48
|
-
desc: str = ''
|
|
49
57
|
def post_desc(self):
|
|
50
58
|
return self.title + ' (processed ........................)'
|
|
59
|
+
|
|
60
|
+
|
|
51
61
|
|
|
52
|
-
tasks: list[PageTask] = []
|
|
62
|
+
tasks: Annotated[list[PageTask], SendTo("SomeCollector")] = []
|
|
53
63
|
owner: PageMember | None = None
|
|
64
|
+
def resolve_owner(self):
|
|
65
|
+
return None
|
|
54
66
|
union_tasks: list[TaskUnion] = []
|
|
55
67
|
|
|
68
|
+
coll: list[str] = []
|
|
69
|
+
def post_coll(self, c=Collector(alias="top_collector")):
|
|
70
|
+
return c.values()
|
|
71
|
+
|
|
56
72
|
class PageSprint(Sprint):
|
|
57
73
|
stories: list[PageStory]
|
|
58
74
|
owner: PageMember | None = None
|
|
@@ -12,4 +12,5 @@ app.mount(
|
|
|
12
12
|
swagger_url="/docs",
|
|
13
13
|
initial_page_policy='first',
|
|
14
14
|
ga_id='G-R64S7Q49VL',
|
|
15
|
-
online_repo_url="https://github.com/allmonday/fastapi-voyager/blob/main"
|
|
15
|
+
online_repo_url="https://github.com/allmonday/fastapi-voyager/blob/main",
|
|
16
|
+
enable_pydantic_resolve_meta=True))
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
from pydantic_resolve import Collector
|
|
4
|
+
from pydantic_resolve.utils.er_diagram import LoaderInfo
|
|
5
|
+
from fastapi_voyager.pydantic_resolve_util import analysis_pydantic_resolve_fields
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SchemaA(BaseModel):
|
|
9
|
+
__pydantic_resolve_expose__ = {"exposed_field": "alias_name"}
|
|
10
|
+
__pydantic_resolve_collect__ = {
|
|
11
|
+
"collected_field": "collector_name",
|
|
12
|
+
("collected_field_a", "collected_field_b"): "collector_x",
|
|
13
|
+
("collected_field_c", "collected_field_d"): ("collector_y", "collector_z"),
|
|
14
|
+
|
|
15
|
+
("collected_field", "collected_field_c"): ("collector_u", "collector_v"),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
id: int
|
|
19
|
+
resolved_field: Annotated[str, LoaderInfo(field="id")] = ""
|
|
20
|
+
exposed_field: str = ""
|
|
21
|
+
collected_field: str = ""
|
|
22
|
+
|
|
23
|
+
collected_field_a: str = ""
|
|
24
|
+
collected_field_b: str = ""
|
|
25
|
+
|
|
26
|
+
collected_field_c: str = ""
|
|
27
|
+
collected_field_d: str = ""
|
|
28
|
+
|
|
29
|
+
post_field: str = ""
|
|
30
|
+
|
|
31
|
+
def resolve_resolved_field(self):
|
|
32
|
+
return "resolved"
|
|
33
|
+
|
|
34
|
+
def post_post_field(self):
|
|
35
|
+
return "posted"
|
|
36
|
+
|
|
37
|
+
collector: list[str] = []
|
|
38
|
+
def post_collector(self, collector=Collector(alias="top_collector")):
|
|
39
|
+
return collector.values()
|
|
40
|
+
|
|
41
|
+
def test_resolve_util():
|
|
42
|
+
# Test resolved field
|
|
43
|
+
res = analysis_pydantic_resolve_fields(SchemaA, "resolved_field")
|
|
44
|
+
assert res["is_resolve"] is True
|
|
45
|
+
|
|
46
|
+
# Test exposed field
|
|
47
|
+
res = analysis_pydantic_resolve_fields(SchemaA, "exposed_field")
|
|
48
|
+
assert res["expose_as_info"] == "alias_name"
|
|
49
|
+
|
|
50
|
+
# Test collected field
|
|
51
|
+
res = analysis_pydantic_resolve_fields(SchemaA, "collected_field")
|
|
52
|
+
assert set(res["send_to_info"]) == {"collector_name", "collector_u", "collector_v"}
|
|
53
|
+
|
|
54
|
+
# Test collected field a (tuple key)
|
|
55
|
+
res = analysis_pydantic_resolve_fields(SchemaA, "collected_field_a")
|
|
56
|
+
assert set(res["send_to_info"]) == {"collector_x"}
|
|
57
|
+
|
|
58
|
+
# Test collected field c (tuple key and tuple value)
|
|
59
|
+
res = analysis_pydantic_resolve_fields(SchemaA, "collected_field_c")
|
|
60
|
+
assert set(res["send_to_info"]) == {"collector_y", "collector_z", "collector_u", "collector_v"}
|
|
61
|
+
|
|
62
|
+
# Test post field
|
|
63
|
+
res = analysis_pydantic_resolve_fields(SchemaA, "post_field")
|
|
64
|
+
assert res["is_post"] is True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
res = analysis_pydantic_resolve_fields(SchemaA, "collector")
|
|
68
|
+
assert set(res["collect_info"]) == {"top_collector"}
|
|
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.3 → fastapi_voyager-0.14.0}/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.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/apple-touch-icon.png
RENAMED
|
File without changes
|
{fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/favicon-16x16.png
RENAMED
|
File without changes
|
{fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/favicon-32x32.png
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/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
|