fastapi-voyager 0.15.6__py3-none-any.whl → 0.16.0a1__py3-none-any.whl

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 (29) hide show
  1. fastapi_voyager/__init__.py +2 -2
  2. fastapi_voyager/adapters/__init__.py +16 -0
  3. fastapi_voyager/adapters/base.py +44 -0
  4. fastapi_voyager/adapters/common.py +260 -0
  5. fastapi_voyager/adapters/django_ninja_adapter.py +299 -0
  6. fastapi_voyager/adapters/fastapi_adapter.py +165 -0
  7. fastapi_voyager/adapters/litestar_adapter.py +188 -0
  8. fastapi_voyager/er_diagram.py +15 -14
  9. fastapi_voyager/introspectors/__init__.py +34 -0
  10. fastapi_voyager/introspectors/base.py +81 -0
  11. fastapi_voyager/introspectors/detector.py +123 -0
  12. fastapi_voyager/introspectors/django_ninja.py +114 -0
  13. fastapi_voyager/introspectors/fastapi.py +83 -0
  14. fastapi_voyager/introspectors/litestar.py +166 -0
  15. fastapi_voyager/pydantic_resolve_util.py +4 -2
  16. fastapi_voyager/render.py +2 -2
  17. fastapi_voyager/render_style.py +0 -1
  18. fastapi_voyager/server.py +174 -295
  19. fastapi_voyager/type_helper.py +2 -2
  20. fastapi_voyager/version.py +1 -1
  21. fastapi_voyager/voyager.py +75 -47
  22. fastapi_voyager/web/index.html +11 -14
  23. fastapi_voyager/web/store.js +2 -0
  24. fastapi_voyager/web/vue-main.js +4 -0
  25. {fastapi_voyager-0.15.6.dist-info → fastapi_voyager-0.16.0a1.dist-info}/METADATA +133 -7
  26. {fastapi_voyager-0.15.6.dist-info → fastapi_voyager-0.16.0a1.dist-info}/RECORD +29 -17
  27. {fastapi_voyager-0.15.6.dist-info → fastapi_voyager-0.16.0a1.dist-info}/WHEEL +0 -0
  28. {fastapi_voyager-0.15.6.dist-info → fastapi_voyager-0.16.0a1.dist-info}/entry_points.txt +0 -0
  29. {fastapi_voyager-0.15.6.dist-info → fastapi_voyager-0.16.0a1.dist-info}/licenses/LICENSE +0 -0
fastapi_voyager/server.py CHANGED
@@ -1,302 +1,181 @@
1
- from pathlib import Path
2
- from typing import Literal
3
-
4
- from fastapi import APIRouter, FastAPI
5
- from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
6
- from fastapi.staticfiles import StaticFiles
7
- from pydantic import BaseModel
8
- from starlette.middleware.gzip import GZipMiddleware
9
-
10
- from fastapi_voyager.render import Renderer
11
- from fastapi_voyager.type import CoreData, SchemaNode, Tag
12
- from fastapi_voyager.type_helper import get_source, get_vscode_link
13
- from fastapi_voyager.version import __version__
14
- from fastapi_voyager.voyager import Voyager
15
- from pydantic_resolve import ErDiagram
16
- from fastapi_voyager.er_diagram import VoyagerErDiagram
17
-
18
- WEB_DIR = Path(__file__).parent / "web"
19
- WEB_DIR.mkdir(exist_ok=True)
20
-
21
- GA_PLACEHOLDER = "<!-- GA_SNIPPET -->"
22
- VERSION_PLACEHOLDER = "<!-- VERSION_PLACEHOLDER -->"
23
-
24
- def _build_ga_snippet(ga_id: str | None) -> str:
25
- if not ga_id:
26
- return ""
27
-
28
- return f""" <script async src="https://www.googletagmanager.com/gtag/js?id={ga_id}"></script>
29
- <script>
30
- window.dataLayer = window.dataLayer || [];
31
- function gtag(){{dataLayer.push(arguments);}}
32
- gtag('js', new Date());
33
-
34
- gtag('config', '{ga_id}');
35
- </script>
36
1
  """
2
+ FastAPI-voyager server module with framework adapter support.
37
3
 
38
- INITIAL_PAGE_POLICY = Literal['first', 'full', 'empty']
39
-
40
- # ---------- setup ----------
41
-
42
- class OptionParam(BaseModel):
43
- tags: list[Tag]
44
- schemas: list[SchemaNode]
45
- dot: str
46
- enable_brief_mode: bool
47
- version: str
48
- initial_page_policy: INITIAL_PAGE_POLICY
49
- swagger_url: str | None = None
50
- has_er_diagram: bool = False
51
- enable_pydantic_resolve_meta: bool = False
52
-
53
- class Payload(BaseModel):
54
- tags: list[str] | None = None
55
- schema_name: str | None = None
56
- schema_field: str | None = None
57
- route_name: str | None = None
58
- show_fields: str = 'object'
59
- brief: bool = False
60
- hide_primitive_route: bool = False
61
- show_module: bool = True
62
- show_pydantic_resolve_meta: bool = False
63
-
64
- # ---------- search ----------
65
- class SearchResultOptionParam(BaseModel):
66
- tags: list[Tag]
4
+ This module provides the main `create_voyager` function that automatically
5
+ detects the framework type and returns an appropriately configured voyager UI.
6
+ """
7
+ from typing import Any, Literal
67
8
 
68
- class SchemaSearchPayload(BaseModel): # leave tag, route out
69
- schema_name: str | None = None
70
- schema_field: str | None = None
71
- show_fields: str = 'object'
72
- brief: bool = False
73
- hide_primitive_route: bool = False
74
- show_module: bool = True
75
- show_pydantic_resolve_meta: bool = False
9
+ from pydantic_resolve import ErDiagram
76
10
 
11
+ from fastapi_voyager.adapters import DjangoNinjaAdapter, FastAPIAdapter, LitestarAdapter
12
+ from fastapi_voyager.introspectors import FrameworkType, detect_framework
13
+
14
+ INITIAL_PAGE_POLICY = Literal["first", "full", "empty"]
15
+
16
+
17
+ def _get_adapter(
18
+ target_app: Any,
19
+ module_color: dict[str, str] | None = None,
20
+ gzip_minimum_size: int | None = 500,
21
+ module_prefix: str | None = None,
22
+ swagger_url: str | None = None,
23
+ online_repo_url: str | None = None,
24
+ initial_page_policy: INITIAL_PAGE_POLICY = "first",
25
+ ga_id: str | None = None,
26
+ er_diagram: ErDiagram | None = None,
27
+ enable_pydantic_resolve_meta: bool = False,
28
+ ) -> Any:
29
+ """
30
+ Get the appropriate adapter for the given target app.
31
+
32
+ Automatically detects the framework type and returns the matching adapter.
33
+
34
+ Args:
35
+ target_app: The web application instance to introspect
36
+ module_color: Optional color mapping for modules
37
+ gzip_minimum_size: Minimum size for gzip compression
38
+ module_prefix: Optional module prefix for filtering
39
+ swagger_url: Optional custom URL to Swagger documentation
40
+ online_repo_url: Optional online repository URL for source links
41
+ initial_page_policy: Initial page display policy
42
+ ga_id: Optional Google Analytics ID
43
+ er_diagram: Optional ER diagram from pydantic-resolve
44
+ enable_pydantic_resolve_meta: Enable pydantic-resolve metadata display
45
+
46
+ Returns:
47
+ An adapter instance for the detected framework
48
+
49
+ Raises:
50
+ TypeError: If the app type is not supported
51
+ """
52
+ # Use centralized framework detection from introspectors
53
+ framework = detect_framework(target_app)
54
+
55
+ if framework == FrameworkType.FASTAPI:
56
+ return FastAPIAdapter(
57
+ target_app=target_app,
58
+ module_color=module_color,
59
+ gzip_minimum_size=gzip_minimum_size,
60
+ module_prefix=module_prefix,
61
+ swagger_url=swagger_url,
62
+ online_repo_url=online_repo_url,
63
+ initial_page_policy=initial_page_policy,
64
+ ga_id=ga_id,
65
+ er_diagram=er_diagram,
66
+ enable_pydantic_resolve_meta=enable_pydantic_resolve_meta,
67
+ )
68
+
69
+ elif framework == FrameworkType.LITESTAR:
70
+ return LitestarAdapter(
71
+ target_app=target_app,
72
+ module_color=module_color,
73
+ gzip_minimum_size=gzip_minimum_size,
74
+ module_prefix=module_prefix,
75
+ swagger_url=swagger_url,
76
+ online_repo_url=online_repo_url,
77
+ initial_page_policy=initial_page_policy,
78
+ ga_id=ga_id,
79
+ er_diagram=er_diagram,
80
+ enable_pydantic_resolve_meta=enable_pydantic_resolve_meta,
81
+ )
82
+
83
+ elif framework == FrameworkType.DJANGO_NINJA:
84
+ return DjangoNinjaAdapter(
85
+ target_app=target_app,
86
+ module_color=module_color,
87
+ gzip_minimum_size=gzip_minimum_size, # Note: ignored for Django
88
+ module_prefix=module_prefix,
89
+ swagger_url=swagger_url,
90
+ online_repo_url=online_repo_url,
91
+ initial_page_policy=initial_page_policy,
92
+ ga_id=ga_id,
93
+ er_diagram=er_diagram,
94
+ enable_pydantic_resolve_meta=enable_pydantic_resolve_meta,
95
+ )
96
+
97
+ # If we get here, the app type is not supported
98
+ raise TypeError(
99
+ f"Unsupported app type: {type(target_app).__name__}. "
100
+ f"Supported types: FastAPI, Django Ninja API, Litestar. "
101
+ f"If you're using a different framework, please implement a VoyagerAdapter for that framework. "
102
+ f"See fastapi_voyager/adapters/ for examples."
103
+ )
77
104
 
78
- # ---------- er diagram ----------
79
- class ErDiagramPayload(BaseModel):
80
- show_fields: str = 'object'
81
- show_module: bool = True
82
105
 
83
106
  def create_voyager(
84
- target_app: FastAPI,
85
- module_color: dict[str, str] | None = None,
86
- gzip_minimum_size: int | None = 500,
87
- module_prefix: str | None = None,
88
- swagger_url: str | None = None,
89
- online_repo_url: str | None = None,
90
- initial_page_policy: INITIAL_PAGE_POLICY = 'first',
91
- ga_id: str | None = None,
92
- er_diagram: ErDiagram | None = None,
93
- enable_pydantic_resolve_meta: bool = False,
94
- ) -> FastAPI:
95
- router = APIRouter(tags=['fastapi-voyager'])
96
-
97
- @router.post("/er-diagram", response_class=PlainTextResponse)
98
- def get_er_diagram(payload: ErDiagramPayload) -> str:
99
- if er_diagram:
100
- return VoyagerErDiagram(
101
- er_diagram,
102
- show_fields=payload.show_fields,
103
- show_module=payload.show_module ).render_dot()
104
- return ''
105
-
106
- @router.get("/dot", response_model=OptionParam)
107
- def get_dot() -> str:
108
- voyager = Voyager(module_color=module_color)
109
- voyager.analysis(target_app)
110
- dot = voyager.render_dot()
111
-
112
- # include tags and their routes
113
- tags = voyager.tags
114
- for t in tags:
115
- t.routes.sort(key=lambda r: r.name)
116
- tags.sort(key=lambda t: t.name)
117
-
118
- schemas = voyager.nodes[:]
119
- schemas.sort(key=lambda s: s.name)
120
-
121
- return OptionParam(
122
- tags=tags,
123
- schemas=schemas,
124
- dot=dot,
125
- enable_brief_mode=bool(module_prefix),
126
- version=__version__,
127
- swagger_url=swagger_url,
128
- initial_page_policy=initial_page_policy,
129
- has_er_diagram=er_diagram is not None,
130
- enable_pydantic_resolve_meta=enable_pydantic_resolve_meta)
131
-
132
-
133
- @router.post("/dot-search", response_model=SearchResultOptionParam)
134
- def get_search_dot(payload: SchemaSearchPayload):
135
- voyager = Voyager(
136
- schema=payload.schema_name,
137
- schema_field=payload.schema_field,
138
- show_fields=payload.show_fields,
139
- module_color=module_color,
140
- hide_primitive_route=payload.hide_primitive_route,
141
- show_module=payload.show_module,
142
- show_pydantic_resolve_meta=payload.show_pydantic_resolve_meta,
143
- )
144
- voyager.analysis(target_app)
145
- tags = voyager.calculate_filtered_tag_and_route()
146
-
147
- for t in tags:
148
- t.routes.sort(key=lambda r: r.name)
149
- tags.sort(key=lambda t: t.name)
150
-
151
- return SearchResultOptionParam(tags=tags)
152
-
153
- @router.post("/dot", response_class=PlainTextResponse)
154
- def get_filtered_dot(payload: Payload) -> str:
155
- voyager = Voyager(
156
- include_tags=payload.tags,
157
- schema=payload.schema_name,
158
- schema_field=payload.schema_field,
159
- show_fields=payload.show_fields,
160
- module_color=module_color,
161
- route_name=payload.route_name,
162
- hide_primitive_route=payload.hide_primitive_route,
163
- show_module=payload.show_module,
164
- show_pydantic_resolve_meta=payload.show_pydantic_resolve_meta,
165
- )
166
- voyager.analysis(target_app)
167
- if payload.brief:
168
- if payload.tags:
169
- return voyager.render_tag_level_brief_dot(module_prefix=module_prefix)
170
- else:
171
- return voyager.render_overall_brief_dot(module_prefix=module_prefix)
172
- else:
173
- return voyager.render_dot()
174
-
175
- @router.post("/dot-core-data", response_model=CoreData)
176
- def get_filtered_dot_core_data(payload: Payload) -> str:
177
- voyager = Voyager(
178
- include_tags=payload.tags,
179
- schema=payload.schema_name,
180
- schema_field=payload.schema_field,
181
- show_fields=payload.show_fields,
182
- module_color=module_color,
183
- route_name=payload.route_name,
184
- )
185
- voyager.analysis(target_app)
186
- return voyager.dump_core_data()
187
-
188
- @router.post('/dot-render-core-data', response_class=PlainTextResponse)
189
- def render_dot_from_core_data(core_data: CoreData) -> str:
190
- renderer = Renderer(
191
- show_fields=core_data.show_fields,
192
- module_color=core_data.module_color,
193
- schema=core_data.schema)
194
- return renderer.render_dot(core_data.tags, core_data.routes, core_data.nodes, core_data.links)
195
-
196
- @router.get("/", response_class=HTMLResponse)
197
- def index():
198
- index_file = WEB_DIR / "index.html"
199
- if index_file.exists():
200
- content = index_file.read_text(encoding="utf-8")
201
- content = content.replace(GA_PLACEHOLDER, _build_ga_snippet(ga_id))
202
- content = content.replace(VERSION_PLACEHOLDER, f"?v={__version__}")
203
- return content
204
- # fallback simple page if index.html missing
205
- return """
206
- <!doctype html>
207
- <html>
208
- <head><meta charset=\"utf-8\"><title>Graphviz Preview</title></head>
209
- <body>
210
- <p>index.html not found. Create one under src/fastapi_voyager/web/index.html</p>
211
- </body>
212
- </html>
213
- """
214
-
215
- class SourcePayload(BaseModel):
216
- schema_name: str
217
-
218
- @router.post("/source")
219
- def get_object_by_module_name(payload: SourcePayload):
220
- """
221
- input: __module__ + __name__, eg: tests.demo.PageStories
222
- output: source code of the object
223
- """
224
- try:
225
- components = payload.schema_name.split('.')
226
- if len(components) < 2:
227
- return JSONResponse(
228
- status_code=400,
229
- content={"error": "Invalid schema name format. Expected format: module.ClassName"}
230
- )
231
-
232
- module_name = '.'.join(components[:-1])
233
- class_name = components[-1]
234
-
235
- mod = __import__(module_name, fromlist=[class_name])
236
- obj = getattr(mod, class_name)
237
- source_code = get_source(obj)
238
-
239
- return JSONResponse(content={"source_code": source_code})
240
- except ImportError as e:
241
- return JSONResponse(
242
- status_code=404,
243
- content={"error": f"Module not found: {e}"}
244
- )
245
- except AttributeError as e:
246
- return JSONResponse(
247
- status_code=404,
248
- content={"error": f"Class not found: {e}"}
249
- )
250
- except Exception as e:
251
- return JSONResponse(
252
- status_code=500,
253
- content={"error": f"Internal error: {str(e)}"}
254
- )
255
-
256
- @router.post("/vscode-link")
257
- def get_vscode_link_by_module_name(payload: SourcePayload):
258
- """
259
- input: __module__ + __name__, eg: tests.demo.PageStories
260
- output: source path of the object
261
- """
262
- try:
263
- components = payload.schema_name.split('.')
264
- if len(components) < 2:
265
- return JSONResponse(
266
- status_code=400,
267
- content={"error": "Invalid schema name format. Expected format: module.ClassName"}
268
- )
269
-
270
- module_name = '.'.join(components[:-1])
271
- class_name = components[-1]
272
-
273
- mod = __import__(module_name, fromlist=[class_name])
274
- obj = getattr(mod, class_name)
275
- link = get_vscode_link(obj, online_repo_url=online_repo_url)
276
-
277
- return JSONResponse(content={"link": link})
278
- except ImportError as e:
279
- return JSONResponse(
280
- status_code=404,
281
- content={"error": f"Module not found: {e}"}
282
- )
283
- except AttributeError as e:
284
- return JSONResponse(
285
- status_code=404,
286
- content={"error": f"Class not found: {e}"}
287
- )
288
- except Exception as e:
289
- return JSONResponse(
290
- status_code=500,
291
- content={"error": f"Internal error: {str(e)}"}
292
- )
293
-
294
- app = FastAPI(title="fastapi-voyager demo server")
295
- if gzip_minimum_size is not None and gzip_minimum_size >= 0:
296
- app.add_middleware(GZipMiddleware, minimum_size=gzip_minimum_size)
297
-
298
- app.mount("/fastapi-voyager-static", StaticFiles(directory=str(WEB_DIR)), name="static")
299
- app.include_router(router)
300
-
301
- return app
302
-
107
+ target_app: Any,
108
+ module_color: dict[str, str] | None = None,
109
+ gzip_minimum_size: int | None = 500,
110
+ module_prefix: str | None = None,
111
+ swagger_url: str | None = None,
112
+ online_repo_url: str | None = None,
113
+ initial_page_policy: INITIAL_PAGE_POLICY = "first",
114
+ ga_id: str | None = None,
115
+ er_diagram: ErDiagram | None = None,
116
+ enable_pydantic_resolve_meta: bool = False,
117
+ ) -> Any:
118
+ """
119
+ Create a voyager UI application for the given target app.
120
+
121
+ This function automatically detects the framework type (FastAPI, Django Ninja, or Litestar)
122
+ and returns an appropriately configured voyager UI application.
123
+
124
+ For FastAPI: Returns a FastAPI app that can be mounted
125
+ For Django Ninja: Returns an ASGI application
126
+ For Litestar: Returns a Litestar app
127
+
128
+ Args:
129
+ target_app: The web application to visualize
130
+ module_color: Optional color mapping for modules (e.g., {"myapp": "blue"})
131
+ gzip_minimum_size: Minimum response size for gzip compression (set to <0 to disable)
132
+ module_prefix: Optional module prefix for filtering/organization
133
+ swagger_url: Optional custom URL to Swagger/OpenAPI documentation
134
+ online_repo_url: Optional base URL for online repository source links
135
+ initial_page_policy: Initial page display policy ('first', 'full', or 'empty')
136
+ ga_id: Optional Google Analytics tracking ID
137
+ er_diagram: Optional ER diagram from pydantic-resolve
138
+ enable_pydantic_resolve_meta: Enable display of pydantic-resolve metadata
139
+
140
+ Returns:
141
+ A framework-specific application object that provides the voyager UI
142
+
143
+ Example:
144
+ # FastAPI
145
+ from fastapi import FastAPI
146
+ from fastapi_voyager import create_voyager
147
+
148
+ app = FastAPI()
149
+ voyager_app = create_voyager(app)
150
+ app.mount("/voyager", voyager_app)
151
+
152
+ # Django Ninja
153
+ from ninja import NinjaAPI
154
+ from fastapi_voyager import create_voyager
155
+
156
+ api = NinjaAPI()
157
+ voyager_asgi_app = create_voyager(api)
158
+ # See django_ninja tests for integration examples
159
+
160
+ # Litestar
161
+ from litestar import Litestar
162
+ from fastapi_voyager import create_voyager
163
+
164
+ app = Litestar()
165
+ voyager_app = create_voyager(app)
166
+ # Mount or integrate as needed
167
+ """
168
+ adapter = _get_adapter(
169
+ target_app=target_app,
170
+ module_color=module_color,
171
+ gzip_minimum_size=gzip_minimum_size,
172
+ module_prefix=module_prefix,
173
+ swagger_url=swagger_url,
174
+ online_repo_url=online_repo_url,
175
+ initial_page_policy=initial_page_policy,
176
+ ga_id=ga_id,
177
+ er_diagram=er_diagram,
178
+ enable_pydantic_resolve_meta=enable_pydantic_resolve_meta,
179
+ )
180
+
181
+ return adapter.create_app()
@@ -2,13 +2,13 @@ import inspect
2
2
  import logging
3
3
  import os
4
4
  from types import UnionType
5
- from typing import Annotated, Any, Generic, Union, get_args, get_origin, ForwardRef
5
+ from typing import Annotated, Any, ForwardRef, Generic, Union, get_args, get_origin
6
6
 
7
7
  import pydantic_resolve.constant as const
8
8
  from pydantic import BaseModel
9
9
 
10
- from fastapi_voyager.type import FieldInfo
11
10
  from fastapi_voyager.pydantic_resolve_util import analysis_pydantic_resolve_fields
11
+ from fastapi_voyager.type import FieldInfo
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
 
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.15.6"
2
+ __version__ = "0.16.0alpha-1"