fastapi-voyager 0.13.3__tar.gz → 0.14.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.
Files changed (62) hide show
  1. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/PKG-INFO +14 -5
  2. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/README.md +12 -3
  3. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/docs/changelog.md +14 -6
  4. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/docs/idea.md +4 -1
  5. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/pyproject.toml +1 -1
  6. fastapi_voyager-0.14.1/src/fastapi_voyager/pydantic_resolve_util.py +99 -0
  7. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/render.py +44 -5
  8. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/server.py +11 -2
  9. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/type.py +8 -0
  10. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/type_helper.py +4 -1
  11. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/version.py +1 -1
  12. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/voyager.py +8 -1
  13. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/graph-ui.js +0 -5
  14. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/index.html +20 -0
  15. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/store.js +3 -0
  16. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/vue-main.js +15 -0
  17. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/tests/demo.py +25 -9
  18. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/tests/programatic.py +2 -1
  19. fastapi_voyager-0.14.1/tests/test_resolve_util_impl.py +68 -0
  20. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  21. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  22. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/.github/workflows/publish.yml +0 -0
  23. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/.gitignore +0 -0
  24. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/.python-version +0 -0
  25. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/CONTRIBUTING.md +0 -0
  26. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/LICENSE +0 -0
  27. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/release.md +0 -0
  28. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/__init__.py +0 -0
  29. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/cli.py +0 -0
  30. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/er_diagram.py +0 -0
  31. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/filter.py +0 -0
  32. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/module.py +0 -0
  33. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/component/demo.js +0 -0
  34. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/component/render-graph.js +0 -0
  35. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
  36. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
  37. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
  38. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
  39. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
  40. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
  41. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
  42. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
  43. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
  44. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
  45. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
  46. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/quasar.min.css +0 -0
  47. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/src/fastapi_voyager/web/quasar.min.js +0 -0
  48. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/tests/__init__.py +0 -0
  49. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/tests/demo_anno.py +0 -0
  50. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/tests/service/__init__.py +0 -0
  51. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/tests/service/schema/__init__.py +0 -0
  52. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/tests/service/schema/base_entity.py +0 -0
  53. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/tests/service/schema/extra.py +0 -0
  54. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/tests/service/schema/schema.py +0 -0
  55. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/tests/test_analysis.py +0 -0
  56. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/tests/test_filter.py +0 -0
  57. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/tests/test_generic.py +0 -0
  58. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/tests/test_import.py +0 -0
  59. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/tests/test_module.py +0 -0
  60. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/tests/test_type_helper.py +0 -0
  61. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.1}/uv.lock +0 -0
  62. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.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
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.2.3
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's `@ensure_subset` decorator helps safely pick fields from the 'source class' while **indicating the reference** from the current class to the base class.
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 is a lightweight tool designed to build complex, nested data in a simple, declarative way. In version 2.0.0alpha, it will introduce an important feature: ER Diagram, and fastapi-voyager will support this feature, allowing for a clearer understanding of the business relationships between the data.
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
 
@@ -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/)
@@ -23,7 +25,8 @@ app.mount('/voyager',
23
25
  swagger_url="/docs",
24
26
  ga_id="G-XXXXXXXXVL",
25
27
  initial_page_policy='first',
26
- online_repo_url='https://github.com/allmonday/composition-oriented-development-pattern/blob/master'))
28
+ online_repo_url='https://github.com/allmonday/composition-oriented-development-pattern/blob/master',
29
+ enable_pydantic_resolve_meta=True))
27
30
  ```
28
31
 
29
32
  https://github.com/allmonday/composition-oriented-development-pattern/blob/master/src/main.py#L48
@@ -63,6 +66,7 @@ It is also an architecture tool that can identify issues inside implementation,
63
66
 
64
67
  Given ErDiagram defined by pydantic-resolve, application level entity relationship diagram can be visualized too.
65
68
 
69
+
66
70
  ### highlight nodes and links
67
71
  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
72
 
@@ -127,6 +131,11 @@ app.mount('/voyager',
127
131
 
128
132
  <img width="1276" height="613" alt="image" src="https://github.com/user-attachments/assets/ea0091bb-ee11-4f71-8be3-7129d956c910" />
129
133
 
134
+ ### Show pydantic resolve meta info
135
+
136
+ setting `enable_pydantic_resolve_meta=True` in `create_voyager`, toggle `pydantic resolve meta`.
137
+
138
+ <img width="1604" height="535" alt="image" src="https://github.com/user-attachments/assets/d1639555-af41-4a08-9970-4b8ef314596a" />
130
139
 
131
140
 
132
141
  ## Command Line Usage
@@ -162,9 +171,9 @@ voyager --help
162
171
 
163
172
  ## About pydantic-resolve
164
173
 
165
- pydantic-resolve's `@ensure_subset` decorator helps safely pick fields from the 'source class' while **indicating the reference** from the current class to the base class.
174
+ 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
175
 
167
- pydantic-resolve is a lightweight tool designed to build complex, nested data in a simple, declarative way. In version 2.0.0alpha, it will introduce an important feature: ER Diagram, and fastapi-voyager will support this feature, allowing for a clearer understanding of the business relationships between the data.
176
+ 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
177
 
169
178
  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
179
 
@@ -144,13 +144,21 @@
144
144
  - [x] show dashed line for link without dataloader
145
145
  - 0.13.3
146
146
  - [x] show field description
147
- - 0.13.4
148
- - [ ] integration with pydantic-resolve
149
- - [ ] show hint for resolve, post fields
150
- - [ ] display loader as edges
151
- - [ ] add tests
152
- - 0.13.5
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
+ - 0.14.1
153
+ - [x] minor ui enhancement
154
+
155
+ ## 0.15, internal refactor
156
+ - 0.15.0
157
+ - [ ] left panel can be toggled.
153
158
  - [ ] refactor vue-main.js, move methods to store
154
159
  - [ ] refactor render.py
160
+ - [ ] add tests
161
+
162
+ ## 1.0, release
155
163
 
156
164
 
@@ -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
@@ -9,7 +9,7 @@ requires-python = ">=3.10"
9
9
  keywords = ["fastapi", "visualization", "routing", "openapi"]
10
10
  dependencies = [
11
11
  "fastapi>=0.110",
12
- "pydantic-resolve>=2.2.3"
12
+ "pydantic-resolve>=2.3.1"
13
13
  ]
14
14
  classifiers = [
15
15
  "Programming Language :: Python :: 3",
@@ -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
- fields = [n for n in node.fields if n.from_base is False]
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
- _fields = [f for f in fields if f.is_object is True]
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} </font></td></tr>"""
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(show_fields=core_data.show_fields, module_color=core_data.module_color, schema=core_data.schema)
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.13.3"
2
+ __version__ = "0.14.1"
@@ -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(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema, show_module=self.show_module)
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)
@@ -151,11 +151,6 @@ export class GraphUI {
151
151
  } else {
152
152
  self.currentSelection = [obj];
153
153
  self._highlight();
154
- try {
155
- self.options.resetCb();
156
- } catch (e) {
157
- console.warn("resetCb callback failed", e);
158
- }
159
154
  }
160
155
  });
161
156
 
@@ -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>
@@ -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
@@ -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,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, ensure_subset
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
- @ensure_subset(Story)
43
- class PageStory(BaseModel):
44
- id: int
45
- sprint_id: int
46
- title: str = Field(exclude=True)
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"}