fastapi-voyager 0.12.2__tar.gz → 0.12.3__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 (52) hide show
  1. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/PKG-INFO +1 -2
  2. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/docs/changelog.md +2 -0
  3. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/docs/idea.md +2 -0
  4. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/pyproject.toml +0 -1
  5. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/__init__.py +1 -2
  6. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/cli.py +3 -3
  7. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/filter.py +6 -5
  8. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/module.py +11 -10
  9. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/render.py +17 -8
  10. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/server.py +19 -18
  11. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/type.py +5 -3
  12. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/type_helper.py +13 -7
  13. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/version.py +1 -1
  14. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/voyager.py +14 -10
  15. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/tests/demo.py +11 -9
  16. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/tests/demo_anno.py +16 -23
  17. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/tests/programatic.py +3 -2
  18. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/tests/service/schema.py +4 -2
  19. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/tests/test_analysis.py +6 -5
  20. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/tests/test_filter.py +4 -2
  21. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/tests/test_generic.py +10 -2
  22. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/tests/test_type_helper.py +15 -9
  23. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/.github/workflows/publish.yml +0 -0
  24. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/.gitignore +0 -0
  25. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/.python-version +0 -0
  26. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/LICENSE +0 -0
  27. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/README.md +0 -0
  28. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/release.md +0 -0
  29. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/component/render-graph.js +0 -0
  30. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
  31. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
  32. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/component/schema-field-filter.js +0 -0
  33. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/graph-ui.js +0 -0
  34. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
  35. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
  36. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
  37. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
  38. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
  39. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
  40. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
  41. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
  42. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
  43. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/index.html +0 -0
  44. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/quasar.min.css +0 -0
  45. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/quasar.min.js +0 -0
  46. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/src/fastapi_voyager/web/vue-main.js +0 -0
  47. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/tests/__init__.py +0 -0
  48. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/tests/service/__init__.py +0 -0
  49. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/tests/test_import.py +0 -0
  50. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/tests/test_module.py +0 -0
  51. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/uv.lock +0 -0
  52. {fastapi_voyager-0.12.2 → fastapi_voyager-0.12.3}/voyager.jpg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.12.2
3
+ Version: 0.12.3
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
@@ -108,6 +108,8 @@
108
108
  - 0.12.2
109
109
  - [x] add google analytics
110
110
  - 0.12.3
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
111
113
  - [ ] search tag/ route
112
114
  - [ ] refactor render.py
113
115
  - [ ] reorg: move variable into reactive in vue-main.js
@@ -12,6 +12,8 @@
12
12
  - [ ] support Google analysis config
13
13
  - [ ] sort field name in nodes (only table inside right panel), pending
14
14
  - [ ] set max limit for fields in nodes
15
+ - [ ] minimap
16
+ - ref: https://observablehq.com/@rabelais/d3-js-zoom-minimap
15
17
 
16
18
  ## in analysis
17
19
  - [ ] upgrade network algorithm (optional)
@@ -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,8 @@ 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
87
96
 
88
97
  # recursively vist module from short to long: 'a', 'a.b', 'a.b.c'
89
98
  # color_flag: {'a', 'a.b.c'}
@@ -109,7 +118,7 @@ class Renderer:
109
118
  label = " {mod.name}"
110
119
  labeljust = "l"
111
120
  {(f'pencolor = "{color}"' if color else 'pencolor="#ccc"')}
112
- {(f'penwidth = 3' if color else 'penwidth=""')}
121
+ {('penwidth = 3' if color else 'penwidth=""')}
113
122
  {inner_nodes_str}
114
123
  {child_str}
115
124
  }}'''
@@ -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(shelled_type.__qualname__, 'visited')
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.3"
@@ -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:
@@ -1,10 +1,12 @@
1
- from pydantic import BaseModel, Field
1
+ from dataclasses import dataclass
2
+ from typing import Generic, TypeVar
3
+
2
4
  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
5
+ from pydantic import BaseModel, Field
6
+ from pydantic_resolve import Resolver, ensure_subset
7
+
6
8
  import tests.service.schema as serv
7
- from dataclasses import dataclass
9
+ from tests.service.schema import A, Story, Task
8
10
 
9
11
  app = FastAPI(title="Demo API", description="A demo FastAPI application for router visualization")
10
12
 
@@ -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
52
  class PageSprint(serv.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,10 +1,12 @@
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
5
+ from pydantic import BaseModel, Field
6
+ from pydantic_resolve import Resolver, ensure_subset
7
+
7
8
  import tests.service.schema as serv
9
+ from tests.service.schema import Story, Task
8
10
 
9
11
  app = FastAPI(title="Demo API", description="A demo FastAPI application for router visualization")
10
12
 
@@ -12,11 +14,6 @@ app = FastAPI(title="Demo API", description="A demo FastAPI application for rout
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
17
  class PageMember(serv.Member):
21
18
  fullname: str = ''
22
19
  def post_fullname(self):
@@ -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
37
  class PageSprint(serv.Sprint):
41
- stories: list[PageStory]
42
- owner: Optional[PageMember] = None
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 import app
2
+ from tests.demo_anno import app
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',
@@ -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
@@ -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
+