fastapi-voyager 0.12.2__tar.gz → 0.12.4__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 (60) hide show
  1. fastapi_voyager-0.12.4/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  2. fastapi_voyager-0.12.4/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  3. fastapi_voyager-0.12.4/CONTRIBUTING.md +23 -0
  4. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/PKG-INFO +20 -4
  5. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/README.md +19 -2
  6. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/docs/changelog.md +11 -6
  7. fastapi_voyager-0.12.4/docs/idea.md +27 -0
  8. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/pyproject.toml +0 -1
  9. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/__init__.py +1 -2
  10. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/cli.py +3 -3
  11. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/filter.py +6 -5
  12. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/module.py +11 -10
  13. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/render.py +21 -9
  14. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/server.py +19 -18
  15. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/type.py +5 -3
  16. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/type_helper.py +13 -7
  17. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/version.py +1 -1
  18. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/voyager.py +14 -10
  19. fastapi_voyager-0.12.4/src/fastapi_voyager/web/component/demo.js +17 -0
  20. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/graph-ui.js +0 -1
  21. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/index.html +14 -10
  22. fastapi_voyager-0.12.4/src/fastapi_voyager/web/store.js +17 -0
  23. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/vue-main.js +15 -4
  24. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/tests/demo.py +15 -13
  25. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/tests/demo_anno.py +20 -27
  26. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/tests/programatic.py +2 -1
  27. fastapi_voyager-0.12.4/tests/service/schema/__init__.py +0 -0
  28. fastapi_voyager-0.12.4/tests/service/schema/extra.py +8 -0
  29. {fastapi_voyager-0.12.2/tests/service → fastapi_voyager-0.12.4/tests/service/schema}/schema.py +4 -9
  30. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/tests/test_analysis.py +6 -5
  31. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/tests/test_filter.py +4 -2
  32. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/tests/test_generic.py +10 -2
  33. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/tests/test_type_helper.py +15 -9
  34. fastapi_voyager-0.12.2/docs/idea.md +0 -22
  35. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/.github/workflows/publish.yml +0 -0
  36. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/.gitignore +0 -0
  37. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/.python-version +0 -0
  38. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/LICENSE +0 -0
  39. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/release.md +0 -0
  40. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/component/render-graph.js +0 -0
  41. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
  42. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
  43. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/component/schema-field-filter.js +0 -0
  44. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
  45. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
  46. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
  47. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
  48. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
  49. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
  50. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
  51. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
  52. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
  53. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/quasar.min.css +0 -0
  54. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/src/fastapi_voyager/web/quasar.min.js +0 -0
  55. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/tests/__init__.py +0 -0
  56. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/tests/service/__init__.py +0 -0
  57. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/tests/test_import.py +0 -0
  58. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/tests/test_module.py +0 -0
  59. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/uv.lock +0 -0
  60. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.4}/voyager.jpg +0 -0
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Describe the bug**
11
+ A clear and concise description of what the bug is.
12
+
13
+ **To Reproduce**
14
+ Steps to reproduce the behavior:
15
+ 1. Go to '...'
16
+ 2. Click on '....'
17
+ 3. Scroll down to '....'
18
+ 4. See error
19
+
20
+ **Expected behavior**
21
+ A clear and concise description of what you expected to happen.
22
+
23
+ **Screenshots**
24
+ If applicable, add screenshots to help explain your problem.
25
+
26
+ **Desktop (please complete the following information):**
27
+ - OS: [e.g. iOS]
28
+ - Browser [e.g. chrome, safari]
29
+ - Version [e.g. 22]
30
+
31
+ **Smartphone (please complete the following information):**
32
+ - Device: [e.g. iPhone6]
33
+ - OS: [e.g. iOS8.1]
34
+ - Browser [e.g. stock browser, safari]
35
+ - Version [e.g. 22]
36
+
37
+ **Additional context**
38
+ Add any other context about the problem here.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Is your feature request related to a problem? Please describe.**
11
+ A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12
+
13
+ **Describe the solution you'd like**
14
+ A clear and concise description of what you want to happen.
15
+
16
+ **Describe alternatives you've considered**
17
+ A clear and concise description of any alternative solutions or features you've considered.
18
+
19
+ **Additional context**
20
+ Add any other context or screenshots about the feature request here.
@@ -0,0 +1,23 @@
1
+ # How to develop & contribute?
2
+
3
+ fork, clone.
4
+
5
+ install uv.
6
+
7
+ ```shell
8
+ uv venv
9
+ source .venv/bin/activate
10
+ uv pip install ".[dev]"
11
+ uvicorn tests.programatic:app --reload
12
+ ```
13
+
14
+ open `localhost:8000/voyager`
15
+
16
+
17
+ frontend:
18
+ - `src/web/vue-main.js`: main js
19
+
20
+ backend:
21
+ - `voyager.py`: main entry
22
+ - `render.py`: generate dot file
23
+ - `server.py`: serve mode
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.12.2
3
+ Version: 0.12.4
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
@@ -12,7 +12,6 @@ Classifier: Framework :: FastAPI
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: MIT License
14
14
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.9
16
15
  Classifier: Programming Language :: Python :: 3.10
17
16
  Classifier: Programming Language :: Python :: 3.11
18
17
  Classifier: Programming Language :: Python :: 3.12
@@ -32,14 +31,31 @@ Description-Content-Type: text/markdown
32
31
  [![PyPI Downloads](https://static.pepy.tech/badge/fastapi-voyager/month)](https://pepy.tech/projects/fastapi-voyager)
33
32
 
34
33
 
35
- > This repo is still in early stage, it supports pydantic v2 only
36
34
 
37
35
  Visualize your FastAPI endpoints, and explore them interactively.
38
36
 
39
- [visit online demo](https://www.newsyeah.fun/voyager/) of project: [composition oriented development pattern](https://github.com/allmonday/composition-oriented-development-pattern)
37
+ > This repo is still in early stage, it supports pydantic v2 only
38
+
39
+ [live demo](https://www.newsyeah.fun/voyager/) of project: [composition oriented development pattern](https://github.com/allmonday/composition-oriented-development-pattern)
40
40
 
41
41
  <img width="1600" height="986" alt="image" src="https://github.com/user-attachments/assets/8829cda0-f42d-4c84-be2f-b019bb5fe7e1" />
42
42
 
43
+ with configuration:
44
+
45
+ ```python
46
+ app.mount('/voyager',
47
+ create_voyager(
48
+ app,
49
+ module_color={'src.services': 'tomato'},
50
+ module_prefix='src.services',
51
+ swagger_url="/docs",
52
+ ga_id="G-R64S7Q49VL",
53
+ initial_page_policy='first',
54
+ online_repo_url='https://github.com/allmonday/composition-oriented-development-pattern/blob/master'))
55
+ ```
56
+
57
+ https://github.com/allmonday/composition-oriented-development-pattern/blob/master/src/main.py#L48
58
+
43
59
  ## Plan & Raodmap
44
60
  - [ideas](./docs/idea.md)
45
61
  - [changelog & roadmap](./docs/changelog.md)
@@ -3,14 +3,31 @@
3
3
  [![PyPI Downloads](https://static.pepy.tech/badge/fastapi-voyager/month)](https://pepy.tech/projects/fastapi-voyager)
4
4
 
5
5
 
6
- > This repo is still in early stage, it supports pydantic v2 only
7
6
 
8
7
  Visualize your FastAPI endpoints, and explore them interactively.
9
8
 
10
- [visit online demo](https://www.newsyeah.fun/voyager/) of project: [composition oriented development pattern](https://github.com/allmonday/composition-oriented-development-pattern)
9
+ > This repo is still in early stage, it supports pydantic v2 only
10
+
11
+ [live demo](https://www.newsyeah.fun/voyager/) of project: [composition oriented development pattern](https://github.com/allmonday/composition-oriented-development-pattern)
11
12
 
12
13
  <img width="1600" height="986" alt="image" src="https://github.com/user-attachments/assets/8829cda0-f42d-4c84-be2f-b019bb5fe7e1" />
13
14
 
15
+ with configuration:
16
+
17
+ ```python
18
+ app.mount('/voyager',
19
+ create_voyager(
20
+ app,
21
+ module_color={'src.services': 'tomato'},
22
+ module_prefix='src.services',
23
+ swagger_url="/docs",
24
+ ga_id="G-R64S7Q49VL",
25
+ initial_page_policy='first',
26
+ online_repo_url='https://github.com/allmonday/composition-oriented-development-pattern/blob/master'))
27
+ ```
28
+
29
+ https://github.com/allmonday/composition-oriented-development-pattern/blob/master/src/main.py#L48
30
+
14
31
  ## Plan & Raodmap
15
32
  - [ideas](./docs/idea.md)
16
33
  - [changelog & roadmap](./docs/changelog.md)
@@ -1,4 +1,4 @@
1
- # Changelog & roadmap
1
+ # Changelog & plan
2
2
 
3
3
  ## <0.9:
4
4
  - [x] group schemas by module hierarchy
@@ -108,17 +108,22 @@
108
108
  - 0.12.2
109
109
  - [x] add google analytics
110
110
  - 0.12.3
111
- - [ ] search tag/ route
111
+ - [x] fix bug in `update_forward_refs`, class should not be skipped if it's parent class has been visited.
112
+ - 0.12.4
113
+ - [x] fix logger exception
114
+ - 0.12.5
115
+ - [x] fix nested cluster with same color
112
116
  - [ ] refactor render.py
113
- - [ ] reorg: move variable into reactive in vue-main.js
117
+ - [ ] refactor fe with store based on reactive (demo.js)
118
+ - [ ] remove search component, integrated into main page
114
119
 
115
120
  ## 0.13
116
- - 0.12.0
121
+ - 0.13.2
122
+ - [ ] if er diagram is provided, show it first.
123
+ - 0.13.1
117
124
  - [ ] integration with pydantic-resolve
118
125
  - [ ] show hint for resolve, post fields
119
126
  - [ ] display loader as edges
120
127
  - [ ] add tests
121
128
 
122
- ## 0.13
123
- todo
124
129
 
@@ -0,0 +1,27 @@
1
+ # Idea
2
+
3
+ ## backlog
4
+ - [ ] user can generate nodes/edges manually and connect to generated ones
5
+ - [ ] eg: add owner
6
+ - [ ] add extra info for schema
7
+ - [ ] optimize static resource (allow manually config url)
8
+ - [ ] improve search dialog
9
+ - [ ] add route/tag list
10
+ - [ ] type alias should not be kept as node instead of compiling to original type
11
+ - [ ] how to correctly handle the generic type ?
12
+ - for example `Page[Student]` of `Page[T]` will be marked in `Page[T]`'s module
13
+ - [ ] sort field name in nodes (only table inside right panel)
14
+ - [ ] set max limit for fields in nodes (? need further thinking)
15
+ - [ ] minimap (good to have)
16
+ - ref: https://observablehq.com/@rabelais/d3-js-zoom-minimap
17
+ - [ ] debug mode
18
+ - [ ] export dot content, load dot content
19
+
20
+
21
+ ## in analysis
22
+ - [ ] upgrade network algorithm (optional, for example networkx)
23
+ - [ ] click field to highlight links or click link to highlight related nodes
24
+ - [ ] animation effect for edges
25
+ - [ ] display standard ER diagram spec. `hard but important`
26
+ - [ ] display potential invalid links
27
+ - [ ] highlight relationship belongs to ER diagram
@@ -13,7 +13,6 @@ dependencies = [
13
13
  ]
14
14
  classifiers = [
15
15
  "Programming Language :: Python :: 3",
16
- "Programming Language :: Python :: 3.9",
17
16
  "Programming Language :: Python :: 3.10",
18
17
  "Programming Language :: Python :: 3.11",
19
18
  "Programming Language :: Python :: 3.12",
@@ -2,8 +2,7 @@
2
2
 
3
3
  Utilities to introspect a FastAPI application and visualize its routing tree.
4
4
  """
5
- from .version import __version__ # noqa: F401
6
-
7
5
  from .server import create_voyager
6
+ from .version import __version__ # noqa: F401
8
7
 
9
8
  __all__ = ["__version__", "create_voyager"]
@@ -5,9 +5,9 @@ import importlib.util
5
5
  import logging
6
6
  import os
7
7
  import sys
8
- from typing import Optional
9
8
 
10
9
  from fastapi import FastAPI
10
+
11
11
  from fastapi_voyager import server as viz_server
12
12
  from fastapi_voyager.version import __version__
13
13
  from fastapi_voyager.voyager import Voyager
@@ -15,7 +15,7 @@ from fastapi_voyager.voyager import Voyager
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
17
 
18
- def load_fastapi_app_from_file(module_path: str, app_name: str = "app") -> Optional[FastAPI]:
18
+ def load_fastapi_app_from_file(module_path: str, app_name: str = "app") -> FastAPI | None:
19
19
  """Load FastAPI app from a Python module file."""
20
20
  try:
21
21
  # Convert relative path to absolute path
@@ -47,7 +47,7 @@ def load_fastapi_app_from_file(module_path: str, app_name: str = "app") -> Optio
47
47
  return None
48
48
 
49
49
 
50
- def load_fastapi_app_from_module(module_name: str, app_name: str = "app") -> Optional[FastAPI]:
50
+ def load_fastapi_app_from_module(module_name: str, app_name: str = "app") -> FastAPI | None:
51
51
  """Load FastAPI app from a Python module name."""
52
52
  try:
53
53
  # Temporarily add the current working directory to sys.path
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
+
2
3
  from collections import deque
3
- from typing import Tuple
4
- from fastapi_voyager.type import Tag, Route, SchemaNode, Link, PK
4
+
5
+ from fastapi_voyager.type import PK, Link, Route, SchemaNode, Tag
5
6
 
6
7
 
7
8
  def filter_graph(
@@ -13,7 +14,7 @@ def filter_graph(
13
14
  nodes: list[SchemaNode],
14
15
  links: list[Link],
15
16
  node_set: dict[str, SchemaNode],
16
- ) -> Tuple[list[Tag], list[Route], list[SchemaNode], list[Link]]:
17
+ ) -> tuple[list[Tag], list[Route], list[SchemaNode], list[Link]]:
17
18
  """Filter tags, routes, schema nodes and links based on a target schema and optional field.
18
19
 
19
20
  Behaviour summary (mirrors previous Analytics.filter_nodes_and_schemas_based_on_schemas):
@@ -113,7 +114,7 @@ def filter_subgraph_by_module_prefix(
113
114
  links: list[Link],
114
115
  nodes: list[SchemaNode],
115
116
  module_prefix: str
116
- ) -> Tuple[list[Tag], list[Route], list[SchemaNode], list[Link]]:
117
+ ) -> tuple[list[Tag], list[Route], list[SchemaNode], list[Link]]:
117
118
  """Collapse schema graph so routes link directly to nodes whose module matches ``module_prefix``.
118
119
 
119
120
  The routine keeps tag→route links untouched, prunes schema nodes whose module does not start
@@ -202,7 +203,7 @@ def filter_subgraph_from_tag_to_schema_by_module_prefix(
202
203
  links: list[Link],
203
204
  nodes: list[SchemaNode],
204
205
  module_prefix: str
205
- ) -> Tuple[list[Tag], list[Route], list[SchemaNode], list[Link]]:
206
+ ) -> tuple[list[Tag], list[Route], list[SchemaNode], list[Link]]:
206
207
  """Collapse schema graph so routes link directly to nodes whose module matches ``module_prefix``.
207
208
 
208
209
  The routine keeps tag→route links untouched, prunes schema nodes whose module does not start
@@ -1,6 +1,7 @@
1
- from typing import Callable, Type, TypeVar, Any
2
- from fastapi_voyager.type import SchemaNode, ModuleNode, Route, ModuleRoute
1
+ from collections.abc import Callable
2
+ from typing import Any, TypeVar
3
3
 
4
+ from fastapi_voyager.type import ModuleNode, ModuleRoute, Route, SchemaNode
4
5
 
5
6
  N = TypeVar('N') # Node type: ModuleNode or ModuleRoute
6
7
  I = TypeVar('I') # Item type: SchemaNode or Route
@@ -10,7 +11,7 @@ def _build_module_tree(
10
11
  items: list[I],
11
12
  *,
12
13
  get_module_path: Callable[[I], str | None],
13
- NodeClass: Type[N],
14
+ NodeClass: type[N],
14
15
  item_list_attr: str,
15
16
  ) -> list[N]:
16
17
  """
@@ -34,13 +35,13 @@ def _build_module_tree(
34
35
  return NodeClass(**kwargs) # type: ignore[arg-type]
35
36
 
36
37
  def get_or_create(child_name: str, parent: N) -> N:
37
- for m in getattr(parent, 'modules'):
38
+ for m in parent.modules:
38
39
  if m.name == child_name:
39
40
  return m
40
- parent_full = getattr(parent, 'fullname')
41
+ parent_full = parent.fullname
41
42
  fullname = child_name if not parent_full or parent_full == "__root__" else f"{parent_full}.{child_name}"
42
43
  new_node = make_node(child_name, fullname)
43
- getattr(parent, 'modules').append(new_node)
44
+ parent.modules.append(new_node)
44
45
  return new_node
45
46
 
46
47
  # Build the tree
@@ -65,13 +66,13 @@ def _build_module_tree(
65
66
 
66
67
  # Collapse linear chains: no items on node and exactly one child module
67
68
  def collapse(node: N) -> None:
68
- while len(getattr(node, 'modules')) == 1 and len(getattr(node, item_list_attr)) == 0:
69
- child = getattr(node, 'modules')[0]
69
+ while len(node.modules) == 1 and len(getattr(node, item_list_attr)) == 0:
70
+ child = node.modules[0]
70
71
  node.name = f"{node.name}.{child.name}"
71
72
  node.fullname = child.fullname
72
73
  setattr(node, item_list_attr, getattr(child, item_list_attr))
73
- setattr(node, 'modules', getattr(child, 'modules'))
74
- for m in getattr(node, 'modules'):
74
+ node.modules = child.modules
75
+ for m in node.modules:
75
76
  collapse(m)
76
77
 
77
78
  for top in result:
@@ -1,8 +1,17 @@
1
- from typing import Optional
2
- from fastapi_voyager.type import SchemaNode, ModuleNode, Link, Tag, Route, FieldType, PK, ModuleRoute
3
- from fastapi_voyager.module import build_module_schema_tree, build_module_route_tree
4
1
  from logging import getLogger
5
2
 
3
+ from fastapi_voyager.module import build_module_route_tree, build_module_schema_tree
4
+ from fastapi_voyager.type import (
5
+ PK,
6
+ FieldType,
7
+ Link,
8
+ ModuleNode,
9
+ ModuleRoute,
10
+ Route,
11
+ SchemaNode,
12
+ Tag,
13
+ )
14
+
6
15
  logger = getLogger(__name__)
7
16
 
8
17
 
@@ -23,7 +32,7 @@ class Renderer:
23
32
  logger.info(f'show_module: {self.show_module}')
24
33
  logger.info(f'module_color: {self.module_color}')
25
34
 
26
- def render_schema_label(self, node: SchemaNode, color: Optional[str]=None) -> str:
35
+ def render_schema_label(self, node: SchemaNode, color: str | None=None) -> str:
27
36
  has_base_fields = any(f.from_base for f in node.fields)
28
37
  fields = [n for n in node.fields if n.from_base is False]
29
38
 
@@ -74,7 +83,7 @@ class Renderer:
74
83
  raise ValueError(f'Unknown link type: {link.type}')
75
84
 
76
85
  def render_module_schema_content(self, nodes: list[SchemaNode]) -> str:
77
- def render_node(node: SchemaNode, color: Optional[str]=None) -> str:
86
+ def render_node(node: SchemaNode, color: str | None=None) -> str:
78
87
  return f'''
79
88
  "{node.id}" [
80
89
  label = {self.render_schema_label(node, color)}
@@ -82,8 +91,10 @@ class Renderer:
82
91
  margin="0.5,0.1"
83
92
  ];'''
84
93
 
85
- def render_module_schema(mod: ModuleNode, inherit_color: Optional[str]=None, show_cluster:bool=True) -> str:
86
- color: Optional[str] = inherit_color
94
+ def render_module_schema(mod: ModuleNode, inherit_color: str | None=None, show_cluster:bool=True) -> str:
95
+ color: str | None = inherit_color
96
+ cluster_color: str | None = None
97
+
87
98
 
88
99
  # recursively vist module from short to long: 'a', 'a.b', 'a.b.c'
89
100
  # color_flag: {'a', 'a.b.c'}
@@ -94,6 +105,7 @@ class Renderer:
94
105
  if mod.fullname.startswith(k):
95
106
  module_color_flag.remove(k)
96
107
  color = self.module_color[k]
108
+ cluster_color = color if color != inherit_color else None
97
109
  break
98
110
 
99
111
  inner_nodes = [ render_node(node, color) for node in mod.schema_nodes ]
@@ -108,8 +120,8 @@ class Renderer:
108
120
  style="rounded"
109
121
  label = " {mod.name}"
110
122
  labeljust = "l"
111
- {(f'pencolor = "{color}"' if color else 'pencolor="#ccc"')}
112
- {(f'penwidth = 3' if color else 'penwidth=""')}
123
+ {(f'pencolor = "{cluster_color}"' if cluster_color else 'pencolor="#ccc"')}
124
+ {('penwidth = 3' if color else 'penwidth=""')}
113
125
  {inner_nodes_str}
114
126
  {child_str}
115
127
  }}'''
@@ -1,23 +1,24 @@
1
1
  from pathlib import Path
2
- from typing import Optional, Literal
3
- from fastapi import FastAPI, APIRouter
4
- from starlette.middleware.gzip import GZipMiddleware
5
- from pydantic import BaseModel
6
- from fastapi.responses import HTMLResponse, PlainTextResponse, JSONResponse
2
+ from typing import Literal
3
+
4
+ from fastapi import APIRouter, FastAPI
5
+ from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
7
6
  from fastapi.staticfiles import StaticFiles
8
- from fastapi_voyager.voyager import Voyager
9
- from fastapi_voyager.type import Tag, CoreData, SchemaNode
7
+ from pydantic import BaseModel
8
+ from starlette.middleware.gzip import GZipMiddleware
9
+
10
10
  from fastapi_voyager.render import Renderer
11
+ from fastapi_voyager.type import CoreData, SchemaNode, Tag
11
12
  from fastapi_voyager.type_helper import get_source, get_vscode_link
12
13
  from fastapi_voyager.version import __version__
13
-
14
+ from fastapi_voyager.voyager import Voyager
14
15
 
15
16
  WEB_DIR = Path(__file__).parent / "web"
16
17
  WEB_DIR.mkdir(exist_ok=True)
17
18
 
18
19
  GA_PLACEHOLDER = "<!-- GA_SNIPPET -->"
19
20
 
20
- def _build_ga_snippet(ga_id: Optional[str]) -> str:
21
+ def _build_ga_snippet(ga_id: str | None) -> str:
21
22
  if not ga_id:
22
23
  return ""
23
24
 
@@ -40,13 +41,13 @@ class OptionParam(BaseModel):
40
41
  enable_brief_mode: bool
41
42
  version: str
42
43
  initial_page_policy: INITIAL_PAGE_POLICY
43
- swagger_url: Optional[str] = None
44
+ swagger_url: str | None = None
44
45
 
45
46
  class Payload(BaseModel):
46
- tags: Optional[list[str]] = None
47
- schema_name: Optional[str] = None
48
- schema_field: Optional[str] = None
49
- route_name: Optional[str] = None
47
+ tags: list[str] | None = None
48
+ schema_name: str | None = None
49
+ schema_field: str | None = None
50
+ route_name: str | None = None
50
51
  show_fields: str = 'object'
51
52
  show_meta: bool = False
52
53
  brief: bool = False
@@ -58,11 +59,11 @@ def create_voyager(
58
59
  target_app: FastAPI,
59
60
  module_color: dict[str, str] | None = None,
60
61
  gzip_minimum_size: int | None = 500,
61
- module_prefix: Optional[str] = None,
62
- swagger_url: Optional[str] = None,
63
- online_repo_url: Optional[str] = None,
62
+ module_prefix: str | None = None,
63
+ swagger_url: str | None = None,
64
+ online_repo_url: str | None = None,
64
65
  initial_page_policy: INITIAL_PAGE_POLICY = 'first',
65
- ga_id: Optional[str] = None,
66
+ ga_id: str | None = None,
66
67
  ) -> FastAPI:
67
68
  router = APIRouter(tags=['fastapi-voyager'])
68
69
 
@@ -1,6 +1,8 @@
1
1
  from dataclasses import field
2
+ from typing import Literal
3
+
2
4
  from pydantic.dataclasses import dataclass
3
- from typing import Literal, Optional
5
+
4
6
 
5
7
  @dataclass
6
8
  class NodeBase:
@@ -76,5 +78,5 @@ class CoreData:
76
78
  nodes: list[SchemaNode]
77
79
  links: list[Link]
78
80
  show_fields: FieldType
79
- module_color: Optional[dict[str, str]] = None
80
- schema: Optional[str] = None
81
+ module_color: dict[str, str] | None = None
82
+ schema: str | None = None
@@ -1,11 +1,13 @@
1
1
  import inspect
2
2
  import logging
3
3
  import os
4
- from pydantic import BaseModel
5
- from typing import get_origin, get_args, Union, Annotated, Any, Type, Generic, Optional
6
- from fastapi_voyager.type import FieldInfo
7
4
  from types import UnionType
5
+ from typing import Annotated, Any, Generic, Union, get_args, get_origin
6
+
8
7
  import pydantic_resolve.constant as const
8
+ from pydantic import BaseModel
9
+
10
+ from fastapi_voyager.type import FieldInfo
9
11
 
10
12
  logger = logging.getLogger(__name__)
11
13
 
@@ -185,7 +187,7 @@ def get_pydantic_fields(schema: type[BaseModel], bases_fields: set[str]) -> list
185
187
  return fields
186
188
 
187
189
 
188
- def get_vscode_link(kls, online_repo_url: Optional[str] = None) -> str:
190
+ def get_vscode_link(kls, online_repo_url: str | None = None) -> str:
189
191
  """Build a VSCode deep link to the class definition.
190
192
 
191
193
  Priority:
@@ -241,7 +243,7 @@ def safe_issubclass(kls, target_kls):
241
243
 
242
244
  def update_forward_refs(kls):
243
245
  # TODO: refactor
244
- def update_pydantic_forward_refs(pydantic_kls: Type[BaseModel]):
246
+ def update_pydantic_forward_refs(pydantic_kls: type[BaseModel]):
245
247
  """
246
248
  recursively update refs.
247
249
  """
@@ -254,7 +256,11 @@ def update_forward_refs(kls):
254
256
  update_forward_refs(field.annotation)
255
257
 
256
258
  for shelled_type in get_core_types(kls):
257
- if getattr(shelled_type, const.PYDANTIC_FORWARD_REF_UPDATED, False):
259
+ # Only treat as updated if the flag is set on the class itself, not via inheritance
260
+
261
+ local_attrs = getattr(shelled_type, '__dict__', {})
262
+ if local_attrs.get(const.PYDANTIC_FORWARD_REF_UPDATED, False):
263
+ logger.debug("%s visited", shelled_type.__qualname__)
258
264
  continue
259
265
  if safe_issubclass(shelled_type, BaseModel):
260
266
  update_pydantic_forward_refs(shelled_type)
@@ -287,7 +293,7 @@ def is_non_pydantic_type(tp):
287
293
  return True
288
294
 
289
295
  if __name__ == "__main__":
290
- from tests.demo_anno import PageSprint, PageOverall
296
+ from tests.demo_anno import PageOverall, PageSprint
291
297
 
292
298
  update_forward_refs(PageOverall)
293
299
  update_forward_refs(PageSprint)
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.12.2"
2
+ __version__ = "0.12.4"
@@ -1,21 +1,25 @@
1
- from pydantic import BaseModel
1
+
2
+ import pydantic_resolve.constant as const
2
3
  from fastapi import FastAPI, routing
3
- from typing import Callable
4
+ from pydantic import BaseModel
5
+
6
+ from fastapi_voyager.filter import (
7
+ filter_graph,
8
+ filter_subgraph_by_module_prefix,
9
+ filter_subgraph_from_tag_to_schema_by_module_prefix,
10
+ )
11
+ from fastapi_voyager.render import Renderer
12
+ from fastapi_voyager.type import PK, CoreData, FieldType, Link, LinkType, Route, SchemaNode, Tag
4
13
  from fastapi_voyager.type_helper import (
5
- get_core_types,
6
14
  full_class_name,
7
15
  get_bases_fields,
8
- is_inheritance_of_pydantic_base,
16
+ get_core_types,
9
17
  get_pydantic_fields,
10
18
  get_type_name,
19
+ is_inheritance_of_pydantic_base,
20
+ is_non_pydantic_type,
11
21
  update_forward_refs,
12
- is_non_pydantic_type
13
22
  )
14
- from pydantic import BaseModel
15
- from fastapi_voyager.type import Route, SchemaNode, Link, Tag, LinkType, FieldType, PK, CoreData
16
- from fastapi_voyager.filter import filter_graph, filter_subgraph_from_tag_to_schema_by_module_prefix, filter_subgraph_by_module_prefix
17
- from fastapi_voyager.render import Renderer
18
- import pydantic_resolve.constant as const
19
23
 
20
24
 
21
25
  class Voyager:
@@ -0,0 +1,17 @@
1
+ const { defineComponent, computed } = window.Vue;
2
+
3
+ import { store } from '../store.js'
4
+
5
+ export default defineComponent({
6
+ name: "Demo",
7
+ emits: ["close"],
8
+ setup() {
9
+ return { store };
10
+ },
11
+ template: `
12
+ <div>
13
+ <p>Count: {{ store.state.count }}</p>
14
+ <button @click="store.mutations.increment()">Add</button>
15
+ </div>
16
+ `
17
+ });
@@ -132,7 +132,6 @@ export class GraphUI {
132
132
  });
133
133
 
134
134
  // svg 背景点击高亮清空
135
-
136
135
  $(document)
137
136
  .off("click.graphui")
138
137
  .on("click.graphui", function (evt) {
@@ -298,7 +298,7 @@
298
298
  <template #after>
299
299
  <div style="position: relative; width: 100%; height: 100%;">
300
300
  <div id="graph" class="adjust-fit"></div>
301
- <div style="position: absolute; left: 8px; bottom: 8px; z-index: 10; background: rgba(255,255,255,0.85); border-radius: 4px; padding: 2px 8px;">
301
+ <div style="position: absolute; left: 8px; top: 8px; z-index: 10; background: rgba(255,255,255,0.85); border-radius: 4px; padding: 2px 8px;">
302
302
  <div class="q-mt-sm">
303
303
  <q-toggle
304
304
  v-model="state.focus"
@@ -337,21 +337,25 @@
337
337
  title="show module cluster"
338
338
  />
339
339
  </div>
340
+ <div class="q-mt-sm">
341
+ <q-toggle
342
+ v-model="state.focus"
343
+ v-show="schemaCodeName"
344
+ @update:model-value="val => onFocusChange(val)"
345
+ label="Focus"
346
+ dense
347
+ title="pick a schema and toggle focus on to display related nodes only"
348
+ />
349
+ </div>
350
+ <!-- <div class="q-mt-sm">
351
+ <demo-component></demo-component>
352
+ </div> -->
340
353
  </div>
341
354
  </div>
342
355
  </template>
343
356
  </q-splitter>
344
357
  </q-page-container>
345
358
  </q-layout>
346
- <!-- Detail Dialog -->
347
- <q-dialog v-model="showDetail" :persistent="true" :maximized="true">
348
- <detail-dialog
349
- :schema-name="schemaName"
350
- :show-fields="state.showFields"
351
- :model-value="showDetail"
352
- @close="closeDetail"
353
- />
354
- </q-dialog>
355
359
 
356
360
  <!-- Schema Field Filter Dialog -->
357
361
  <q-dialog
@@ -0,0 +1,17 @@
1
+ const { reactive, watch, ref } = window.Vue;
2
+
3
+ const state = reactive({
4
+ count: 0
5
+ })
6
+
7
+ const mutations = {
8
+ increment() {
9
+ state.count += 1
10
+ }
11
+ }
12
+
13
+
14
+ export const store = {
15
+ state,
16
+ mutations
17
+ }
@@ -1,6 +1,7 @@
1
1
  import SchemaFieldFilter from "./component/schema-field-filter.js";
2
2
  import SchemaCodeDisplay from "./component/schema-code-display.js";
3
3
  import RouteCodeDisplay from "./component/route-code-display.js";
4
+ import Demo from './component/demo.js'
4
5
  import RenderGraph from "./component/render-graph.js";
5
6
  import { GraphUI } from "./graph-ui.js";
6
7
  const { createApp, reactive, onMounted, watch, ref } = window.Vue;
@@ -39,17 +40,22 @@ const app = createApp({
39
40
 
40
41
  const showDetail = ref(false);
41
42
  const showSchemaFieldFilter = ref(false);
43
+
42
44
  const showDumpDialog = ref(false);
43
45
  const dumpJson = ref("");
44
46
  const showImportDialog = ref(false);
45
47
  const importJsonText = ref("");
48
+
46
49
  const showRenderGraph = ref(false);
50
+
47
51
  const renderCoreData = ref(null);
52
+
48
53
  const schemaName = ref(""); // used by detail dialog
49
54
  const schemaFieldFilterSchema = ref(null); // external schemaName for schema-field-filter
50
55
  const schemaCodeName = ref("");
51
56
  const routeCodeId = ref("");
52
57
  const showRouteDetail = ref(false);
58
+
53
59
  let graphUI = null;
54
60
 
55
61
  function openDetail() {
@@ -435,13 +441,18 @@ const app = createApp({
435
441
  };
436
442
  },
437
443
  });
444
+
438
445
  app.use(window.Quasar);
446
+
439
447
  // Set Quasar primary theme color to green
440
448
  if (window.Quasar && typeof window.Quasar.setCssVar === "function") {
441
449
  window.Quasar.setCssVar("primary", "#009485");
442
450
  }
443
- app.component("schema-field-filter", SchemaFieldFilter);
444
- app.component("schema-code-display", SchemaCodeDisplay);
445
- app.component("route-code-display", RouteCodeDisplay);
446
- app.component("render-graph", RenderGraph);
451
+
452
+ app.component("schema-field-filter", SchemaFieldFilter); // shift click and see relationships
453
+ app.component("schema-code-display", SchemaCodeDisplay); // double click to see node details
454
+ app.component("route-code-display", RouteCodeDisplay); // double click to see route details
455
+ app.component("render-graph", RenderGraph); // for debug, render pasted dot content
456
+ app.component('demo-component', Demo)
457
+
447
458
  app.mount("#q-app");
@@ -1,18 +1,20 @@
1
- from pydantic import BaseModel, Field
2
- from fastapi import FastAPI
3
- from typing import Optional, Generic, TypeVar
4
- from pydantic_resolve import ensure_subset, Resolver
5
- from tests.service.schema import Story, Task, A
6
- import tests.service.schema as serv
7
1
  from dataclasses import dataclass
2
+ from typing import Generic, TypeVar
3
+
4
+ from fastapi import FastAPI
5
+ from pydantic import BaseModel, Field
6
+ from pydantic_resolve import Resolver, ensure_subset
7
+
8
+ from tests.service.schema.schema import Member, Sprint, Story, Task
9
+ from tests.service.schema.extra import A, B
8
10
 
9
11
  app = FastAPI(title="Demo API", description="A demo FastAPI application for router visualization")
10
12
 
11
- @app.get("/sprints", tags=['for-restapi', 'group_a'], response_model=list[serv.Sprint])
13
+ @app.get("/sprints", tags=['for-restapi', 'group_a'], response_model=list[Sprint])
12
14
  def get_sprint():
13
15
  return []
14
16
 
15
- class PageMember(serv.Member):
17
+ class PageMember(Member):
16
18
  fullname: str = ''
17
19
  def post_fullname(self):
18
20
  return self.first_name + ' ' + self.last_name
@@ -31,7 +33,7 @@ class TaskB(Task):
31
33
 
32
34
  type TaskUnion = TaskA | TaskB
33
35
  class PageTask(Task):
34
- owner: Optional[PageMember]
36
+ owner: PageMember | None
35
37
 
36
38
  @ensure_subset(Story)
37
39
  class PageStory(BaseModel):
@@ -44,12 +46,12 @@ class PageStory(BaseModel):
44
46
  return self.title + ' (processed ........................)'
45
47
 
46
48
  tasks: list[PageTask] = []
47
- owner: Optional[PageMember] = None
49
+ owner: PageMember | None = None
48
50
  union_tasks: list[TaskUnion] = []
49
51
 
50
- class PageSprint(serv.Sprint):
52
+ class PageSprint(Sprint):
51
53
  stories: list[PageStory]
52
- owner: Optional[PageMember] = None
54
+ owner: PageMember | None = None
53
55
 
54
56
  class PageOverall(BaseModel):
55
57
  sprints: list[PageSprint]
@@ -109,7 +111,7 @@ def get_page_test_3_no_response_model():
109
111
  return True
110
112
 
111
113
  @app.get("/page_test_5/", tags=['long_long_long_tag_name', 'group_b'])
112
- def get_page_test_3_no_response_model():
114
+ def get_page_test_3_no_response_model_long_long_long_name():
113
115
  return True
114
116
 
115
117
 
@@ -1,23 +1,20 @@
1
1
  from __future__ import annotations
2
- from pydantic import BaseModel, Field
2
+
3
+ from typing import Annotated
3
4
  from fastapi import FastAPI
4
- from typing import Optional
5
- from pydantic_resolve import ensure_subset, Resolver
6
- from tests.service.schema import Story, Task
7
- import tests.service.schema as serv
5
+ from pydantic import BaseModel, Field
6
+ from pydantic_resolve import Resolver, ensure_subset
7
+
8
+ from tests.service.schema.schema import Member, Sprint, Story, Task
9
+ from tests.service.schema.extra import A, B
8
10
 
9
11
  app = FastAPI(title="Demo API", description="A demo FastAPI application for router visualization")
10
12
 
11
- @app.get("/sprints", tags=['for-restapi'], response_model=list[serv.Sprint])
13
+ @app.get("/sprints", tags=['for-restapi'], response_model=list[Sprint])
12
14
  def get_sprint():
13
15
  return []
14
16
 
15
- class Tree(BaseModel):
16
- id: int
17
- name: str
18
- children: list[Tree] = []
19
-
20
- class PageMember(serv.Member):
17
+ class PageMember(Member):
21
18
  fullname: str = ''
22
19
  def post_fullname(self):
23
20
  return self.first_name + ' ' + self.last_name
@@ -31,15 +28,15 @@ class TaskB(Task):
31
28
 
32
29
  type TaskUnion = TaskA | TaskB
33
30
  class PageTask(Task):
34
- owner: Optional[PageMember]
31
+ owner: PageMember | None
35
32
 
36
33
 
37
34
  class PageOverall(BaseModel):
38
- sprints: list[PageSprint]
35
+ sprints: Annotated[list[PageSprint], Field(description="List of sprints")]
39
36
 
40
- class PageSprint(serv.Sprint):
41
- stories: list[PageStory]
42
- owner: Optional[PageMember] = None
37
+ class PageSprint(Sprint):
38
+ stories: Annotated[list[PageStory], Field(description="List of stories")]
39
+ owner: Annotated[PageMember | None, Field(description="Owner of the sprint")] = None
43
40
 
44
41
 
45
42
  @ensure_subset(Story)
@@ -53,7 +50,7 @@ class PageStory(BaseModel):
53
50
  return self.title + ' (processed)'
54
51
 
55
52
  tasks: list[PageTask] = []
56
- owner: Optional[PageMember] = None
53
+ owner: PageMember | None = None
57
54
  union_tasks: list[TaskUnion] = []
58
55
 
59
56
  @app.get("/page_overall", tags=['for-page'], response_model=PageOverall)
@@ -62,13 +59,9 @@ async def get_page_info():
62
59
  return await Resolver().resolve(page_overall)
63
60
 
64
61
 
65
- class PageStories(BaseModel):
66
- stories: list[PageStory]
67
-
68
- @app.get("/page_info/", tags=['for-page'], response_model=PageStories)
69
- def get_page_stories():
70
- return {} # no implementation
62
+ # class PageStories(BaseModel):
63
+ # stories: list[PageStory]
71
64
 
72
- @app.get("/rest-tree/", tags=['for-restapi'], response_model=Tree)
73
- def get_tree():
74
- return {} # no implementation
65
+ # @app.get("/page_info/", tags=['for-page'], response_model=PageStories)
66
+ # def get_page_stories():
67
+ # return {} # no implementation
@@ -1,11 +1,12 @@
1
1
  from fastapi_voyager import create_voyager
2
+ # from tests.demo_anno import app
2
3
  from tests.demo import app
3
4
 
4
5
  app.mount(
5
6
  '/voyager',
6
7
  create_voyager(
7
8
  app,
8
- module_color={"tests.service": "purple", "tests.demo": "#ccaa00", "tests": "green"},
9
+ module_color={"tests.service": "purple", "tests.demo": "#00b1cc", "tests": "green"},
9
10
  module_prefix="tests.service",
10
11
  swagger_url="/docs",
11
12
  initial_page_policy='first',
@@ -0,0 +1,8 @@
1
+ from pydantic import BaseModel
2
+
3
+ class B(BaseModel):
4
+ id: int
5
+
6
+ class A(BaseModel):
7
+ id: int
8
+ b: B
@@ -1,5 +1,7 @@
1
+ from typing import Literal
2
+
1
3
  from pydantic import BaseModel
2
- from typing import Literal, Dict
4
+
3
5
 
4
6
  class Sprint(BaseModel):
5
7
  id: int
@@ -8,7 +10,7 @@ class Sprint(BaseModel):
8
10
  class Story(BaseModel):
9
11
  id: int
10
12
  type: Literal['feature', 'bugfix']
11
- dct: Dict
13
+ dct: dict
12
14
  sprint_id: int
13
15
  title: str
14
16
  description: str
@@ -24,10 +26,3 @@ class Member(BaseModel):
24
26
  first_name: str
25
27
  last_name: str
26
28
 
27
-
28
- class B(BaseModel):
29
- id: int
30
-
31
- class A(BaseModel):
32
- id: int
33
- b: B
@@ -1,7 +1,8 @@
1
- from fastapi_voyager.voyager import Voyager
2
- from pydantic import BaseModel
1
+
3
2
  from fastapi import FastAPI
4
- from typing import Optional
3
+ from pydantic import BaseModel
4
+
5
+ from fastapi_voyager.voyager import Voyager
5
6
 
6
7
 
7
8
  def test_analysis():
@@ -22,11 +23,11 @@ def test_analysis():
22
23
 
23
24
  app = FastAPI()
24
25
 
25
- @app.get("/test", response_model=Optional[A])
26
+ @app.get("/test", response_model=A | None)
26
27
  def home():
27
28
  return None
28
29
 
29
- @app.get("/test2", response_model=Optional[C])
30
+ @app.get("/test2", response_model=C | None)
30
31
  def home2():
31
32
  return None
32
33
 
@@ -1,5 +1,5 @@
1
1
  from fastapi_voyager.filter import filter_subgraph_by_module_prefix
2
- from fastapi_voyager.type import Tag, Route, SchemaNode, Link, PK
2
+ from fastapi_voyager.type import PK, Link, Route, SchemaNode, Tag
3
3
 
4
4
 
5
5
  def _make_tag_route_link(tag: Tag, route: Route) -> Link:
@@ -52,7 +52,9 @@ def test_filter_subgraph_filters_nodes_and_links():
52
52
 
53
53
  assert filtered_nodes == [node_b]
54
54
  assert any(
55
- lk.type == "route_to_schema" and lk.source_origin == route.id and lk.target_origin == node_b.id
55
+ lk.type == "route_to_schema" and \
56
+ lk.source_origin == route.id and \
57
+ lk.target_origin == node_b.id
56
58
  for lk in filtered_links
57
59
  )
58
60
  assert len(filtered_links) == 2 # tag -> route and merged route -> filtered node
@@ -1,7 +1,11 @@
1
- from pydantic import BaseModel
1
+ import sys
2
2
  from typing import Generic, TypeVar
3
+
4
+ from pydantic import BaseModel
5
+
3
6
  from fastapi_voyager.type_helper import is_generic_container
4
7
 
8
+
5
9
  class PageStory(BaseModel):
6
10
  id: int
7
11
  title: str
@@ -11,7 +15,11 @@ class DataModel(BaseModel, Generic[T]):
11
15
  data: T
12
16
  id: int
13
17
 
14
- type DataModelPageStory = DataModel[PageStory]
18
+ DataModelPageStory: object # Stub declaration for static analysis
19
+ if sys.version_info >= (3, 12):
20
+ exec("type DataModelPageStory = DataModel[PageStory]")
21
+ else:
22
+ DataModelPageStory = DataModel[PageStory]
15
23
 
16
24
  def test_is_generic_container():
17
25
  print(DataModelPageStory.__value__.__bases__)
@@ -1,15 +1,14 @@
1
1
  import sys
2
- import typing
3
2
  import pytest
4
-
5
3
  from fastapi_voyager.type_helper import get_core_types
4
+ from typing import Annotated
6
5
 
7
6
 
8
7
  def test_optional_and_list_core_types():
9
8
  class T: ...
10
9
 
11
10
  # Optional[T] -> (T,)
12
- opt = typing.Optional[T]
11
+ opt = T | None
13
12
  core = get_core_types(opt)
14
13
  assert core == (T,)
15
14
 
@@ -23,7 +22,7 @@ def test_typing_union_core_types():
23
22
  class A: ...
24
23
  class B: ...
25
24
 
26
- u = typing.Union[A, B]
25
+ u = A | B
27
26
  core = get_core_types(u)
28
27
  # order preserved
29
28
  assert core == (A, B)
@@ -43,7 +42,7 @@ def test_mixed_optional_list():
43
42
  class T: ...
44
43
 
45
44
  # Optional[list[T]] -> (T,) (list unwrapped after removing None)
46
- anno = typing.Optional[list[T]]
45
+ anno = list[T] | None
47
46
  core = get_core_types(anno)
48
47
  assert core == (T,)
49
48
 
@@ -53,7 +52,7 @@ def test_nested_union_flattening():
53
52
  class B: ...
54
53
  class C: ...
55
54
 
56
- anno = typing.Union[A, typing.Union[B, C]]
55
+ anno = A | (B | C)
57
56
  core = get_core_types(anno)
58
57
  # typing normalizes nested unions -> (A, B, C)
59
58
  assert core == (A, B, C)
@@ -72,12 +71,11 @@ def test_uniontype_with_list_member():
72
71
  assert core2 == (A, B)
73
72
 
74
73
 
75
-
76
-
77
74
  # Only Python 3.12+ supports the PEP 695 `type` statement producing TypeAliasType
78
75
  @pytest.mark.skipif(sys.version_info < (3, 12), reason="PEP 695 type aliases require Python 3.12+")
79
76
  def test_union_type_alias_and_list():
80
- # Dynamically exec a type alias using the new syntax so test file stays valid on <3.12 (even though skipped)
77
+ # Dynamically exec a type alias using the new syntax
78
+ # so test file stays valid on <3.12 (even though skipped)
81
79
  ns: dict = {}
82
80
  code = """
83
81
  class A: ...
@@ -97,3 +95,11 @@ type MyAlias = A | B
97
95
  # Direct alias should also work
98
96
  core2 = get_core_types(MyAlias)
99
97
  assert set(core2) == {A, B}
98
+
99
+
100
+ def test_annotated():
101
+ class A: ...
102
+
103
+ core = get_core_types(Annotated[A, 'hello'])
104
+ assert set(core) == {A}
105
+
@@ -1,22 +0,0 @@
1
- # Idea
2
-
3
- ## backlog
4
- - [ ] user can generate nodes/edges manually and connect to generated ones
5
- - [ ] eg: add owner
6
- - [ ] add extra info for schema
7
- - [ ] optimize static resource (allow manually config url)
8
- - [ ] improve search dialog
9
- - [ ] add route/tag list
10
- - [ ] type alias should not be kept as node instead of compiling to original type
11
- - [ ] how to correctly handle the generic type ?
12
- - [ ] support Google analysis config
13
- - [ ] sort field name in nodes (only table inside right panel), pending
14
- - [ ] set max limit for fields in nodes
15
-
16
- ## in analysis
17
- - [ ] upgrade network algorithm (optional)
18
- - [ ] click field to highlight links
19
- - [ ] animation effect for edges
20
- - [ ] display standard ER diagram spec. `hard but important`
21
- - [ ] display potential invalid links
22
- - [ ] highlight relationship belongs to ER diagram