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.
Files changed (62) hide show
  1. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/PKG-INFO +7 -4
  2. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/README.md +5 -2
  3. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/docs/changelog.md +11 -6
  4. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/docs/idea.md +4 -1
  5. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/pyproject.toml +1 -1
  6. fastapi_voyager-0.14.0/src/fastapi_voyager/pydantic_resolve_util.py +99 -0
  7. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/render.py +44 -5
  8. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/server.py +11 -2
  9. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/type.py +8 -0
  10. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/type_helper.py +4 -1
  11. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/version.py +1 -1
  12. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/voyager.py +8 -1
  13. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/index.html +9 -0
  14. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/store.js +2 -0
  15. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/vue-main.js +8 -0
  16. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/demo.py +25 -9
  17. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/programatic.py +2 -1
  18. fastapi_voyager-0.14.0/tests/test_resolve_util_impl.py +68 -0
  19. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  20. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  21. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/.github/workflows/publish.yml +0 -0
  22. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/.gitignore +0 -0
  23. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/.python-version +0 -0
  24. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/CONTRIBUTING.md +0 -0
  25. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/LICENSE +0 -0
  26. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/release.md +0 -0
  27. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/__init__.py +0 -0
  28. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/cli.py +0 -0
  29. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/er_diagram.py +0 -0
  30. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/filter.py +0 -0
  31. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/module.py +0 -0
  32. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/component/demo.js +0 -0
  33. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/component/render-graph.js +0 -0
  34. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
  35. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
  36. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/graph-ui.js +0 -0
  37. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
  38. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
  39. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
  40. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
  41. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
  42. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
  43. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
  44. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
  45. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
  46. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/quasar.min.css +0 -0
  47. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/src/fastapi_voyager/web/quasar.min.js +0 -0
  48. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/__init__.py +0 -0
  49. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/demo_anno.py +0 -0
  50. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/service/__init__.py +0 -0
  51. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/service/schema/__init__.py +0 -0
  52. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/service/schema/base_entity.py +0 -0
  53. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/service/schema/extra.py +0 -0
  54. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/service/schema/schema.py +0 -0
  55. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/test_analysis.py +0 -0
  56. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/test_filter.py +0 -0
  57. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/test_generic.py +0 -0
  58. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/test_import.py +0 -0
  59. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/test_module.py +0 -0
  60. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/tests/test_type_helper.py +0 -0
  61. {fastapi_voyager-0.13.3 → fastapi_voyager-0.14.0}/uv.lock +0 -0
  62. {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.13.3
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.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/)
@@ -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's `@ensure_subset` decorator helps safely pick fields from the 'source class' while **indicating the reference** from the current class to the base class.
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 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.
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's `@ensure_subset` decorator helps safely pick fields from the 'source class' while **indicating the reference** from the current class to the base class.
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 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.
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
- - 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
+
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
@@ -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.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(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)
@@ -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, 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"}