fastapi-voyager 0.11.10__tar.gz → 0.11.11__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 (50) hide show
  1. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/PKG-INFO +29 -15
  2. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/README.md +28 -14
  3. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/cli.py +35 -47
  4. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/filter.py +0 -1
  5. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/render.py +64 -41
  6. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/type_helper.py +6 -4
  7. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/version.py +1 -1
  8. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/tests/programatic.py +2 -2
  9. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/tests/service/schema.py +3 -2
  10. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/.github/workflows/publish.yml +0 -0
  11. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/.gitignore +0 -0
  12. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/.python-version +0 -0
  13. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/LICENSE +0 -0
  14. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/pyproject.toml +0 -0
  15. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/release.md +0 -0
  16. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/__init__.py +0 -0
  17. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/module.py +0 -0
  18. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/server.py +0 -0
  19. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/type.py +0 -0
  20. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/voyager.py +0 -0
  21. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/component/render-graph.js +0 -0
  22. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
  23. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
  24. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/component/schema-field-filter.js +0 -0
  25. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/graph-ui.js +0 -0
  26. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
  27. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
  28. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
  29. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
  30. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
  31. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
  32. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
  33. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
  34. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
  35. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/index.html +0 -0
  36. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/quasar.min.css +0 -0
  37. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/quasar.min.js +0 -0
  38. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/src/fastapi_voyager/web/vue-main.js +0 -0
  39. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/tests/__init__.py +0 -0
  40. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/tests/demo.py +0 -0
  41. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/tests/demo_anno.py +0 -0
  42. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/tests/service/__init__.py +0 -0
  43. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/tests/test_analysis.py +0 -0
  44. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/tests/test_filter.py +0 -0
  45. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/tests/test_generic.py +0 -0
  46. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/tests/test_import.py +0 -0
  47. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/tests/test_module.py +0 -0
  48. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/tests/test_type_helper.py +0 -0
  49. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/uv.lock +0 -0
  50. {fastapi_voyager-0.11.10 → fastapi_voyager-0.11.11}/voyager.jpg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.11.10
3
+ Version: 0.11.11
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
@@ -52,8 +52,11 @@ uv add fastapi-voyager
52
52
  voyager -m path.to.your.app.module --server
53
53
  ```
54
54
 
55
- > *sub_app* is not supported yet.
55
+ > [Sub-Application mounts](https://fastapi.tiangolo.com/advanced/sub-applications/) are not supported yet, but you can specify the name of the FastAPI application used with `--app`. Only a single application (default: 'app') can be selected, but in a scenario where `api` is attached through `app.mount("/api", api)`, you can select `api` like this:
56
56
 
57
+ ```shell
58
+ voyager -m path.to.your.app.module --server --app api
59
+ ```
57
60
 
58
61
  ## Mount into project
59
62
 
@@ -141,6 +144,8 @@ voyager -m tests.demo --module_color=tests.demo:red --module_color=tests.service
141
144
  voyager -m tests.demo -o my_visualization.dot
142
145
 
143
146
  voyager --version
147
+
148
+ voyager --help
144
149
  ```
145
150
 
146
151
  The tool will generate a DOT file that you can render using Graphviz:
@@ -166,8 +171,6 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
166
171
  - [ ] user can generate nodes/edges manually and connect to generated ones
167
172
  - [ ] eg: add owner
168
173
  - [ ] add extra info for schema
169
- - [ ] display standard ER diagram `hard`
170
- - [ ] display potential invalid links
171
174
  - [ ] optimize static resource (allow manually config url)
172
175
  - [ ] improve search dialog
173
176
  - [ ] add route/tag list
@@ -178,11 +181,9 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
178
181
  ### in analysis
179
182
  - [ ] click field to highlight links
180
183
  - [ ] animation effect for edges
181
- - [ ] customrized right click panel
182
- - [ ] show own dependencies
183
- - [ ] sort field name
184
- - [ ] set max limit for fields
185
- - [ ] logging information
184
+ - [ ] display standard ER diagram spec. `hard but important`
185
+ - [ ] display potential invalid links
186
+ - [ ] highlight relationship belongs to ER diagram
186
187
 
187
188
  ### plan:
188
189
  #### <0.9:
@@ -279,15 +280,28 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
279
280
  - [x] replace issubclass with safe_issubclass to prevent exception.
280
281
  - 0.11.10
281
282
  - [x] fix bug during updating forward refs
283
+ - 0.11.11
284
+ - [x] replace print with logging and add `--log-level` in cli, by default info
285
+ - [x] fill node title color with module color
286
+ - [x] optimize cluster render logic
287
+
288
+ ### 0.12
289
+ - 0.12.1
290
+ - [ ] sort tag / route names in left panel
291
+ - [ ] sort field name in nodes
292
+ - [ ] set max limit for fields in nodes
293
+ - [ ] upgrade network algorithm (optional)
294
+ - [ ] refactor render.py
282
295
 
283
- #### 0.12
284
- - [ ] add tests
285
- - [ ] integration with pydantic-resolve
286
- - [ ] show hint for resolve, post fields
287
- - [ ] display loader as edges
296
+ #### 0.13
297
+ - 0.12.0
298
+ - [ ] integration with pydantic-resolve
299
+ - [ ] show hint for resolve, post fields
300
+ - [ ] display loader as edges
301
+ - [ ] add tests
288
302
 
289
303
  #### 0.13
290
- - [ ] config release pipeline
304
+ placeholder
291
305
 
292
306
 
293
307
  ## About pydantic-resolve
@@ -23,8 +23,11 @@ uv add fastapi-voyager
23
23
  voyager -m path.to.your.app.module --server
24
24
  ```
25
25
 
26
- > *sub_app* is not supported yet.
26
+ > [Sub-Application mounts](https://fastapi.tiangolo.com/advanced/sub-applications/) are not supported yet, but you can specify the name of the FastAPI application used with `--app`. Only a single application (default: 'app') can be selected, but in a scenario where `api` is attached through `app.mount("/api", api)`, you can select `api` like this:
27
27
 
28
+ ```shell
29
+ voyager -m path.to.your.app.module --server --app api
30
+ ```
28
31
 
29
32
  ## Mount into project
30
33
 
@@ -112,6 +115,8 @@ voyager -m tests.demo --module_color=tests.demo:red --module_color=tests.service
112
115
  voyager -m tests.demo -o my_visualization.dot
113
116
 
114
117
  voyager --version
118
+
119
+ voyager --help
115
120
  ```
116
121
 
117
122
  The tool will generate a DOT file that you can render using Graphviz:
@@ -137,8 +142,6 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
137
142
  - [ ] user can generate nodes/edges manually and connect to generated ones
138
143
  - [ ] eg: add owner
139
144
  - [ ] add extra info for schema
140
- - [ ] display standard ER diagram `hard`
141
- - [ ] display potential invalid links
142
145
  - [ ] optimize static resource (allow manually config url)
143
146
  - [ ] improve search dialog
144
147
  - [ ] add route/tag list
@@ -149,11 +152,9 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
149
152
  ### in analysis
150
153
  - [ ] click field to highlight links
151
154
  - [ ] animation effect for edges
152
- - [ ] customrized right click panel
153
- - [ ] show own dependencies
154
- - [ ] sort field name
155
- - [ ] set max limit for fields
156
- - [ ] logging information
155
+ - [ ] display standard ER diagram spec. `hard but important`
156
+ - [ ] display potential invalid links
157
+ - [ ] highlight relationship belongs to ER diagram
157
158
 
158
159
  ### plan:
159
160
  #### <0.9:
@@ -250,15 +251,28 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
250
251
  - [x] replace issubclass with safe_issubclass to prevent exception.
251
252
  - 0.11.10
252
253
  - [x] fix bug during updating forward refs
254
+ - 0.11.11
255
+ - [x] replace print with logging and add `--log-level` in cli, by default info
256
+ - [x] fill node title color with module color
257
+ - [x] optimize cluster render logic
258
+
259
+ ### 0.12
260
+ - 0.12.1
261
+ - [ ] sort tag / route names in left panel
262
+ - [ ] sort field name in nodes
263
+ - [ ] set max limit for fields in nodes
264
+ - [ ] upgrade network algorithm (optional)
265
+ - [ ] refactor render.py
253
266
 
254
- #### 0.12
255
- - [ ] add tests
256
- - [ ] integration with pydantic-resolve
257
- - [ ] show hint for resolve, post fields
258
- - [ ] display loader as edges
267
+ #### 0.13
268
+ - 0.12.0
269
+ - [ ] integration with pydantic-resolve
270
+ - [ ] show hint for resolve, post fields
271
+ - [ ] display loader as edges
272
+ - [ ] add tests
259
273
 
260
274
  #### 0.13
261
- - [ ] config release pipeline
275
+ placeholder
262
276
 
263
277
 
264
278
  ## About pydantic-resolve
@@ -1,15 +1,18 @@
1
1
  """Command line interface for fastapi-voyager."""
2
2
  import argparse
3
- import sys
4
- import importlib.util
5
3
  import importlib
4
+ import importlib.util
5
+ import logging
6
6
  import os
7
+ import sys
7
8
  from typing import Optional
8
9
 
9
10
  from fastapi import FastAPI
10
- from fastapi_voyager.voyager import Voyager
11
- from fastapi_voyager.version import __version__
12
11
  from fastapi_voyager import server as viz_server
12
+ from fastapi_voyager.version import __version__
13
+ from fastapi_voyager.voyager import Voyager
14
+
15
+ logger = logging.getLogger(__name__)
13
16
 
14
17
 
15
18
  def load_fastapi_app_from_file(module_path: str, app_name: str = "app") -> Optional[FastAPI]:
@@ -22,7 +25,7 @@ def load_fastapi_app_from_file(module_path: str, app_name: str = "app") -> Optio
22
25
  # Load the module
23
26
  spec = importlib.util.spec_from_file_location("app_module", module_path)
24
27
  if spec is None or spec.loader is None:
25
- print(f"Error: Could not load module from {module_path}")
28
+ logger.error(f"Could not load module from {module_path}")
26
29
  return None
27
30
 
28
31
  module = importlib.util.module_from_spec(spec)
@@ -34,15 +37,13 @@ def load_fastapi_app_from_file(module_path: str, app_name: str = "app") -> Optio
34
37
  app = getattr(module, app_name)
35
38
  if isinstance(app, FastAPI):
36
39
  return app
37
- else:
38
- print(f"Error: '{app_name}' is not a FastAPI instance")
39
- return None
40
- else:
41
- print(f"Error: No attribute '{app_name}' found in the module")
40
+ logger.error(f"'{app_name}' is not a FastAPI instance")
42
41
  return None
42
+ logger.error(f"No attribute '{app_name}' found in the module")
43
+ return None
43
44
 
44
45
  except Exception as e:
45
- print(f"Error loading FastAPI app: {e}")
46
+ logger.error(f"Error loading FastAPI app: {e}")
46
47
  return None
47
48
 
48
49
 
@@ -66,22 +67,20 @@ def load_fastapi_app_from_module(module_name: str, app_name: str = "app") -> Opt
66
67
  app = getattr(module, app_name)
67
68
  if isinstance(app, FastAPI):
68
69
  return app
69
- else:
70
- print(f"Error: '{app_name}' is not a FastAPI instance")
71
- return None
72
- else:
73
- print(f"Error: No attribute '{app_name}' found in module '{module_name}'")
70
+ logger.error(f"'{app_name}' is not a FastAPI instance")
74
71
  return None
72
+ logger.error(f"No attribute '{app_name}' found in module '{module_name}'")
73
+ return None
75
74
  finally:
76
75
  # Cleanup: if we added the path, remove it
77
76
  if path_added and current_dir in sys.path:
78
77
  sys.path.remove(current_dir)
79
78
 
80
79
  except ImportError as e:
81
- print(f"Error: Could not import module '{module_name}': {e}")
80
+ logger.error(f"Could not import module '{module_name}': {e}")
82
81
  return None
83
82
  except Exception as e:
84
- print(f"Error loading FastAPI app from module '{module_name}': {e}")
83
+ logger.error(f"Error loading FastAPI app from module '{module_name}': {e}")
85
84
  return None
86
85
 
87
86
 
@@ -110,9 +109,9 @@ def generate_visualization(
110
109
  # Optionally write to file
111
110
  with open(output_file, 'w', encoding='utf-8') as f:
112
111
  f.write(dot_content)
113
- print(f"DOT file generated: {output_file}")
114
- print("To render the graph, use: dot -Tpng router_viz.dot -o router_viz.png")
115
- print("Or view online: https://dreampuf.github.io/GraphvizOnline/")
112
+ logger.info(f"DOT file generated: {output_file}")
113
+ logger.info("To render the graph, use: dot -Tpng router_viz.dot -o router_viz.png")
114
+ logger.info("Or view online: https://dreampuf.github.io/GraphvizOnline/")
116
115
 
117
116
 
118
117
  def main():
@@ -217,41 +216,30 @@ Examples:
217
216
  help="Filter by route id (format: <endpoint>_<path with _>)"
218
217
  )
219
218
  parser.add_argument(
220
- "--demo",
221
- action="store_true",
222
- help="Run built-in demo (equivalent to: --module tests.demo --server --module_color=tests.service:blue --module_color=tests.demo:tomato)"
219
+ "--log-level",
220
+ dest="log_level",
221
+ default="INFO",
222
+ help="Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL (default: INFO)"
223
223
  )
224
224
 
225
225
  args = parser.parse_args()
226
226
 
227
- # Handle demo mode: override module_name and defaults
228
- if args.demo:
229
- # Force module loading path
230
- args.module_name = "tests.demo"
231
- # Ensure server mode on
232
- args.server = True
233
- # Inject default module colors if absent / merge
234
- demo_defaults = ["tests.service:blue", "tests.demo:tomato"]
235
- existing = set(args.module_color or [])
236
- for d in demo_defaults:
237
- # only add if same key not already provided
238
- key = d.split(":", 1)[0]
239
- if not any(mc.startswith(key + ":") for mc in existing):
240
- args.module_color = (args.module_color or []) + [d]
241
-
242
227
  if args.module_prefix and not args.server:
243
228
  parser.error("--module_prefix can only be used together with --server")
244
229
 
245
- # Validate required target if not demo
246
- if not args.demo and not (args.module_name or args.module):
247
- parser.error("You must provide a module file, -m module name, or use --demo")
230
+ if not (args.module_name or args.module):
231
+ parser.error("You must provide a module file, -m module name")
232
+
233
+ # Configure logging based on --log-level
234
+ level_name = (args.log_level or "INFO").upper()
235
+ logging.basicConfig(level=level_name)
248
236
 
249
237
  # Load FastAPI app based on the input method (module_name takes precedence)
250
238
  if args.module_name:
251
239
  app = load_fastapi_app_from_module(args.module_name, args.app)
252
240
  else:
253
241
  if not os.path.exists(args.module):
254
- print(f"Error: File '{args.module}' not found")
242
+ logger.error(f"File '{args.module}' not found")
255
243
  sys.exit(1)
256
244
  app = load_fastapi_app_from_file(args.module, args.app)
257
245
 
@@ -279,15 +267,15 @@ Examples:
279
267
  try:
280
268
  import uvicorn
281
269
  except ImportError:
282
- print("Error: uvicorn is required to run the server. Install via 'pip install uvicorn' or 'uv add uvicorn'.")
270
+ logger.info("uvicorn is required to run the server. Install via 'pip install uvicorn' or 'uv add uvicorn'.")
283
271
  sys.exit(1)
284
272
  app_server = viz_server.create_voyager(
285
273
  app,
286
274
  module_color=module_color,
287
275
  module_prefix=args.module_prefix,
288
276
  )
289
- print(f"Starting preview server at http://{args.host}:{args.port} ... (Ctrl+C to stop)")
290
- uvicorn.run(app_server, host=args.host, port=args.port)
277
+ logger.info(f"Starting preview server at http://{args.host}:{args.port} ... (Ctrl+C to stop)")
278
+ uvicorn.run(app_server, host=args.host, port=args.port, log_level=level_name.lower())
291
279
  else:
292
280
  # Generate and write dot file locally
293
281
  generate_visualization(
@@ -300,7 +288,7 @@ Examples:
300
288
  route_name=args.route_name,
301
289
  )
302
290
  except Exception as e:
303
- print(f"Error generating visualization: {e}")
291
+ logger.info(f"Error generating visualization: {e}")
304
292
  sys.exit(1)
305
293
 
306
294
 
@@ -236,7 +236,6 @@ def filter_subgraph_from_tag_to_schema_by_module_prefix(
236
236
  seen_pairs: set[tuple[str, str]] = set()
237
237
 
238
238
  for link in tag_route_links:
239
- # print(link)
240
239
  tag_id = link.source_origin
241
240
  start_node_id = link.target_origin
242
241
  if tag_id is None or start_node_id is None:
@@ -1,6 +1,9 @@
1
1
  from typing import Optional
2
2
  from fastapi_voyager.type import SchemaNode, ModuleNode, Link, Tag, Route, FieldType, PK, ModuleRoute
3
3
  from fastapi_voyager.module import build_module_schema_tree, build_module_route_tree
4
+ from logging import getLogger
5
+
6
+ logger = getLogger(__name__)
4
7
 
5
8
 
6
9
  class Renderer:
@@ -17,7 +20,10 @@ class Renderer:
17
20
  self.schema = schema
18
21
  self.show_module = show_module
19
22
 
20
- def render_schema_label(self, node: SchemaNode) -> str:
23
+ logger.info(f'show_module: {self.show_module}')
24
+ logger.info(f'module_color: {self.module_color}')
25
+
26
+ def render_schema_label(self, node: SchemaNode, color: Optional[str]=None) -> str:
21
27
  has_base_fields = any(f.from_base for f in node.fields)
22
28
  fields = [n for n in node.fields if n.from_base is False]
23
29
 
@@ -38,7 +44,8 @@ class Renderer:
38
44
  field_str = f"""<tr><td align="left" port="f{field.name}" cellpadding="8"><font> {display_xml} </font></td></tr>"""
39
45
  fields_parts.append(field_str)
40
46
 
41
- header_color = 'tomato' if node.id == self.schema else '#009485'
47
+ default_color = '#009485' if color is None else color
48
+ header_color = 'tomato' if node.id == self.schema else default_color
42
49
  header = f"""<tr><td cellpadding="6" bgcolor="{header_color}" align="center" colspan="1" port="{PK}"> <font color="white"> {node.name} </font></td> </tr>"""
43
50
  field_content = ''.join(fields_parts) if fields_parts else ''
44
51
  return f"""<<table border="1" cellborder="0" cellpadding="0" bgcolor="white"> {header} {field_content} </table>>"""
@@ -67,43 +74,56 @@ class Renderer:
67
74
  raise ValueError(f'Unknown link type: {link.type}')
68
75
 
69
76
  def render_module_schema_content(self, nodes: list[SchemaNode]) -> str:
70
- def render_node(node: SchemaNode) -> str:
77
+ def render_node(node: SchemaNode, color: Optional[str]=None) -> str:
71
78
  return f'''
72
79
  "{node.id}" [
73
- label = {self.render_schema_label(node)}
80
+ label = {self.render_schema_label(node, color)}
74
81
  shape = "plain"
75
82
  margin="0.5,0.1"
76
83
  ];'''
77
- def render_module_schema(mod: ModuleNode) -> str:
78
- color: Optional[str] = None
79
84
 
85
+ def render_module_schema(mod: ModuleNode, inherit_color: Optional[str]=None, show_cluster:bool=True) -> str:
86
+ color: Optional[str] = inherit_color
87
+
88
+ # recursively vist module from short to long: 'a', 'a.b', 'a.b.c'
89
+ # color_flag: {'a', 'a.b.c'}
90
+ # at first 'a', match 'a' -> color, remove 'a' from color_flag
91
+ # at 'a.b', no match
92
+ # at 'a.b.c', match 'a.b.c' -> color, remove 'a.b.c' from color_flag
80
93
  for k in module_color_flag:
81
- if mod.fullname.startswith(k):
94
+ if mod.fullname.startswith(k):
82
95
  module_color_flag.remove(k)
83
96
  color = self.module_color[k]
84
97
  break
85
98
 
86
- inner_nodes = [ render_node(node) for node in mod.schema_nodes ]
99
+ inner_nodes = [ render_node(node, color) for node in mod.schema_nodes ]
87
100
  inner_nodes_str = '\n'.join(inner_nodes)
88
- child_str = '\n'.join(render_module_schema(m) for m in mod.modules)
89
- return f'''
90
- subgraph cluster_module_{mod.fullname.replace('.', '_')} {{
91
- tooltip="{mod.fullname}"
92
- color = "#666"
93
- style="rounded"
94
- label = " {mod.name}"
95
- labeljust = "l"
96
- {(f'pencolor = "{color}"' if color else 'pencolor="#ccc"')}
97
- {(f'penwidth = 3' if color else 'penwidth=""')}
101
+ child_str = '\n'.join(render_module_schema(mod=m, inherit_color=color, show_cluster=show_cluster) for m in mod.modules)
102
+
103
+ if show_cluster:
104
+ return f'''
105
+ subgraph cluster_module_{mod.fullname.replace('.', '_')} {{
106
+ tooltip="{mod.fullname}"
107
+ color = "#666"
108
+ style="rounded"
109
+ label = " {mod.name}"
110
+ labeljust = "l"
111
+ {(f'pencolor = "{color}"' if color else 'pencolor="#ccc"')}
112
+ {(f'penwidth = 3' if color else 'penwidth=""')}
113
+ {inner_nodes_str}
114
+ {child_str}
115
+ }}'''
116
+ else:
117
+ return f'''
98
118
  {inner_nodes_str}
99
119
  {child_str}
100
- }}'''
101
- if self.show_module:
102
- module_schemas = build_module_schema_tree(nodes)
103
- module_color_flag = set(self.module_color.keys())
104
- return '\n'.join(render_module_schema(m) for m in module_schemas)
105
- else:
106
- return '\n'.join(render_node(n) for n in nodes)
120
+ '''
121
+
122
+ # if self.show_module:
123
+ module_schemas = build_module_schema_tree(nodes)
124
+ module_color_flag = set(self.module_color.keys())
125
+ return '\n'.join(render_module_schema(mod=m, show_cluster=self.show_module) for m in module_schemas)
126
+
107
127
 
108
128
  def render_module_route_content(self, routes: list[Route]) -> str:
109
129
  def render_route(route: Route) -> str:
@@ -115,29 +135,32 @@ class Renderer:
115
135
  shape = "record"
116
136
  ];'''
117
137
 
118
- def render_module_route(mod: ModuleRoute) -> str:
138
+ def render_module_route(mod: ModuleRoute, show_cluster: bool=True) -> str:
119
139
  # Inner route nodes, same style as flat route_str
120
140
  inner_nodes = [
121
141
  render_route(r) for r in mod.routes
122
142
  ]
123
143
  inner_nodes_str = '\n'.join(inner_nodes)
124
- child_str = '\n'.join(render_module_route(m) for m in mod.modules)
125
- return f'''
126
- subgraph cluster_route_module_{mod.fullname.replace('.', '_')} {{
127
- tooltip="{mod.fullname}"
128
- color = "#666"
129
- style="rounded"
130
- label = " {mod.name}"
131
- labeljust = "l"
144
+ child_str = '\n'.join(render_module_route(m, show_cluster=show_cluster) for m in mod.modules)
145
+ if show_cluster:
146
+ return f'''
147
+ subgraph cluster_route_module_{mod.fullname.replace('.', '_')} {{
148
+ tooltip="{mod.fullname}"
149
+ color = "#666"
150
+ style="rounded"
151
+ label = " {mod.name}"
152
+ labeljust = "l"
153
+ {inner_nodes_str}
154
+ {child_str}
155
+ }}'''
156
+ else:
157
+ return f'''
132
158
  {inner_nodes_str}
133
159
  {child_str}
134
- }}'''
135
- if self.show_module:
136
- module_routes = build_module_route_tree(routes)
137
- module_routes_str = '\n'.join(render_module_route(m) for m in module_routes)
138
- return module_routes_str
139
- else:
140
- return '\n'.join(render_route(r) for r in routes)
160
+ '''
161
+ module_routes = build_module_route_tree(routes)
162
+ module_routes_str = '\n'.join(render_module_route(m, show_cluster=self.show_module) for m in module_routes)
163
+ return module_routes_str
141
164
 
142
165
 
143
166
  def render_dot(self, tags: list[Tag], routes: list[Route], nodes: list[SchemaNode], links: list[Link], spline_line=False) -> str:
@@ -1,4 +1,5 @@
1
1
  import inspect
2
+ import logging
2
3
  import os
3
4
  from pydantic import BaseModel
4
5
  from typing import get_origin, get_args, Union, Annotated, Any, Type, Generic, Optional
@@ -6,6 +7,8 @@ from fastapi_voyager.type import FieldInfo
6
7
  from types import UnionType
7
8
  import pydantic_resolve.constant as const
8
9
 
10
+ logger = logging.getLogger(__name__)
11
+
9
12
  # Python <3.12 compatibility: TypeAliasType exists only from 3.12 (PEP 695)
10
13
  try: # pragma: no cover - import guard
11
14
  from typing import TypeAliasType # type: ignore
@@ -228,12 +231,11 @@ def get_source(kls):
228
231
  return "failed to get source"
229
232
 
230
233
 
231
- def safe_issubclass(kls, classinfo):
234
+ def safe_issubclass(kls, target_kls):
232
235
  try:
233
- return issubclass(kls, classinfo)
236
+ return issubclass(kls, target_kls)
234
237
  except TypeError:
235
- # may raise error for corner case such as ForwardRef
236
- print(str(kls), 'is not a target class')
238
+ logger.debug(f'{kls.__module__}:{kls.__qualname__} is not subclass of {target_kls.__module__}:{target_kls.__qualname__}')
237
239
  return False
238
240
 
239
241
 
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.11.10"
2
+ __version__ = "0.11.11"
@@ -5,8 +5,8 @@ app.mount(
5
5
  '/voyager',
6
6
  create_voyager(
7
7
  app,
8
- module_color={"tests.service": "purple"},
8
+ module_color={"tests.service": "purple", "tests.demo": "#ccaa00", "tests": "green"},
9
9
  module_prefix="tests.service",
10
10
  swagger_url="/docs",
11
- initial_page_policy='first',
11
+ initial_page_policy='full',
12
12
  online_repo_url="https://github.com/allmonday/fastapi-voyager/blob/main"))
@@ -1,6 +1,5 @@
1
- from fastapi_voyager.voyager import Voyager
2
1
  from pydantic import BaseModel
3
- from pydantic_resolve import ensure_subset
2
+ from typing import Literal, Dict
4
3
 
5
4
  class Sprint(BaseModel):
6
5
  id: int
@@ -8,6 +7,8 @@ class Sprint(BaseModel):
8
7
 
9
8
  class Story(BaseModel):
10
9
  id: int
10
+ type: Literal['feature', 'bugfix']
11
+ dct: Dict
11
12
  sprint_id: int
12
13
  title: str
13
14
  description: str