fastapi-voyager 0.15.5__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 (31) 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/graph-ui.js +102 -69
  23. fastapi_voyager/web/graphviz.svg.js +79 -30
  24. fastapi_voyager/web/index.html +11 -14
  25. fastapi_voyager/web/store.js +2 -0
  26. fastapi_voyager/web/vue-main.js +4 -0
  27. {fastapi_voyager-0.15.5.dist-info → fastapi_voyager-0.16.0a1.dist-info}/METADATA +133 -7
  28. {fastapi_voyager-0.15.5.dist-info → fastapi_voyager-0.16.0a1.dist-info}/RECORD +31 -19
  29. {fastapi_voyager-0.15.5.dist-info → fastapi_voyager-0.16.0a1.dist-info}/WHEEL +0 -0
  30. {fastapi_voyager-0.15.5.dist-info → fastapi_voyager-0.16.0a1.dist-info}/entry_points.txt +0 -0
  31. {fastapi_voyager-0.15.5.dist-info → fastapi_voyager-0.16.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,165 @@
1
+ """
2
+ FastAPI adapter for fastapi-voyager.
3
+
4
+ This module provides the FastAPI-specific implementation of the voyager server.
5
+ """
6
+ from typing import Any, Literal
7
+
8
+ from fastapi import APIRouter, FastAPI
9
+ from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
10
+ from fastapi.staticfiles import StaticFiles
11
+ from pydantic import BaseModel
12
+ from starlette.middleware.gzip import GZipMiddleware
13
+
14
+ from fastapi_voyager.adapters.base import VoyagerAdapter
15
+ from fastapi_voyager.adapters.common import STATIC_FILES_PATH, VoyagerContext
16
+ from fastapi_voyager.type import CoreData, SchemaNode, Tag
17
+
18
+
19
+ class OptionParam(BaseModel):
20
+ tags: list[Tag]
21
+ schemas: list[SchemaNode]
22
+ dot: str
23
+ enable_brief_mode: bool
24
+ version: str
25
+ initial_page_policy: Literal["first", "full", "empty"]
26
+ swagger_url: str | None = None
27
+ has_er_diagram: bool = False
28
+ enable_pydantic_resolve_meta: bool = False
29
+ framework_name: str = "API"
30
+
31
+
32
+ class Payload(BaseModel):
33
+ tags: list[str] | None = None
34
+ schema_name: str | None = None
35
+ schema_field: str | None = None
36
+ route_name: str | None = None
37
+ show_fields: str = "object"
38
+ brief: bool = False
39
+ hide_primitive_route: bool = False
40
+ show_module: bool = True
41
+ show_pydantic_resolve_meta: bool = False
42
+
43
+
44
+ class SearchResultOptionParam(BaseModel):
45
+ tags: list[Tag]
46
+
47
+
48
+ class SchemaSearchPayload(BaseModel):
49
+ schema_name: str | None = None
50
+ schema_field: str | None = None
51
+ show_fields: str = "object"
52
+ brief: bool = False
53
+ hide_primitive_route: bool = False
54
+ show_module: bool = True
55
+ show_pydantic_resolve_meta: bool = False
56
+
57
+
58
+ class ErDiagramPayload(BaseModel):
59
+ show_fields: str = "object"
60
+ show_module: bool = True
61
+
62
+
63
+ class SourcePayload(BaseModel):
64
+ schema_name: str
65
+
66
+
67
+ class FastAPIAdapter(VoyagerAdapter):
68
+ """
69
+ FastAPI-specific implementation of VoyagerAdapter.
70
+
71
+ Creates a FastAPI application with voyager endpoints.
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ target_app: Any,
77
+ module_color: dict[str, str] | None = None,
78
+ gzip_minimum_size: int | None = 500,
79
+ module_prefix: str | None = None,
80
+ swagger_url: str | None = None,
81
+ online_repo_url: str | None = None,
82
+ initial_page_policy: str = "first",
83
+ ga_id: str | None = None,
84
+ er_diagram: Any = None,
85
+ enable_pydantic_resolve_meta: bool = False,
86
+ ):
87
+ self.ctx = VoyagerContext(
88
+ target_app=target_app,
89
+ module_color=module_color,
90
+ module_prefix=module_prefix,
91
+ swagger_url=swagger_url,
92
+ online_repo_url=online_repo_url,
93
+ initial_page_policy=initial_page_policy,
94
+ ga_id=ga_id,
95
+ er_diagram=er_diagram,
96
+ enable_pydantic_resolve_meta=enable_pydantic_resolve_meta,
97
+ framework_name="FastAPI",
98
+ )
99
+ self.gzip_minimum_size = gzip_minimum_size
100
+
101
+ def create_app(self) -> FastAPI:
102
+ """Create and return a FastAPI application with voyager endpoints."""
103
+ router = APIRouter(tags=["fastapi-voyager"])
104
+
105
+ @router.post("/er-diagram", response_class=PlainTextResponse)
106
+ def get_er_diagram(payload: ErDiagramPayload) -> str:
107
+ return self.ctx.get_er_diagram_dot(payload.model_dump())
108
+
109
+ @router.get("/dot", response_model=OptionParam)
110
+ def get_dot() -> OptionParam:
111
+ data = self.ctx.get_option_param()
112
+ return OptionParam(**data)
113
+
114
+ @router.post("/dot-search", response_model=SearchResultOptionParam)
115
+ def get_search_dot(payload: SchemaSearchPayload) -> SearchResultOptionParam:
116
+ tags = self.ctx.get_search_dot(payload.model_dump())
117
+ return SearchResultOptionParam(tags=tags)
118
+
119
+ @router.post("/dot", response_class=PlainTextResponse)
120
+ def get_filtered_dot(payload: Payload) -> str:
121
+ return self.ctx.get_filtered_dot(payload.model_dump())
122
+
123
+ @router.post("/dot-core-data", response_model=CoreData)
124
+ def get_filtered_dot_core_data(payload: Payload) -> CoreData:
125
+ return self.ctx.get_core_data(payload.model_dump())
126
+
127
+ @router.post("/dot-render-core-data", response_class=PlainTextResponse)
128
+ def render_dot_from_core_data(core_data: CoreData) -> str:
129
+ return self.ctx.render_dot_from_core_data(core_data)
130
+
131
+ @router.get("/", response_class=HTMLResponse)
132
+ def index() -> str:
133
+ return self.ctx.get_index_html()
134
+
135
+ @router.post("/source")
136
+ def get_object_by_module_name(payload: SourcePayload) -> JSONResponse:
137
+ result = self.ctx.get_source_code(payload.schema_name)
138
+ status_code = 200 if "error" not in result else 400
139
+ if "error" in result and "not found" in result["error"]:
140
+ status_code = 404
141
+ return JSONResponse(content=result, status_code=status_code)
142
+
143
+ @router.post("/vscode-link")
144
+ def get_vscode_link_by_module_name(payload: SourcePayload) -> JSONResponse:
145
+ result = self.ctx.get_vscode_link(payload.schema_name)
146
+ status_code = 200 if "error" not in result else 400
147
+ if "error" in result and "not found" in result["error"]:
148
+ status_code = 404
149
+ return JSONResponse(content=result, status_code=status_code)
150
+
151
+ app = FastAPI(title="fastapi-voyager demo server")
152
+
153
+ if self.gzip_minimum_size is not None and self.gzip_minimum_size >= 0:
154
+ app.add_middleware(GZipMiddleware, minimum_size=self.gzip_minimum_size)
155
+
156
+ from fastapi_voyager.adapters.common import WEB_DIR
157
+
158
+ app.mount(STATIC_FILES_PATH, StaticFiles(directory=str(WEB_DIR)), name="static")
159
+ app.include_router(router)
160
+
161
+ return app
162
+
163
+ def get_mount_path(self) -> str:
164
+ """Get the recommended mount path for voyager."""
165
+ return "/voyager"
@@ -0,0 +1,188 @@
1
+ """
2
+ Litestar adapter for fastapi-voyager.
3
+
4
+ This module provides the Litestar-specific implementation of the voyager server.
5
+ """
6
+ from typing import Any
7
+
8
+ from litestar import Litestar, MediaType, Request, Response, get, post
9
+ from litestar.static_files import create_static_files_router
10
+
11
+ from fastapi_voyager.adapters.base import VoyagerAdapter
12
+ from fastapi_voyager.adapters.common import STATIC_FILES_PATH, WEB_DIR, VoyagerContext
13
+ from fastapi_voyager.type import CoreData, SchemaNode, Tag
14
+
15
+
16
+ class LitestarAdapter(VoyagerAdapter):
17
+ """
18
+ Litestar-specific implementation of VoyagerAdapter.
19
+
20
+ Creates a Litestar application with voyager endpoints.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ target_app: Any,
26
+ module_color: dict[str, str] | None = None,
27
+ gzip_minimum_size: int | None = 500,
28
+ module_prefix: str | None = None,
29
+ swagger_url: str | None = None,
30
+ online_repo_url: str | None = None,
31
+ initial_page_policy: str = "first",
32
+ ga_id: str | None = None,
33
+ er_diagram: Any = None,
34
+ enable_pydantic_resolve_meta: bool = False,
35
+ ):
36
+ self.ctx = VoyagerContext(
37
+ target_app=target_app,
38
+ module_color=module_color,
39
+ module_prefix=module_prefix,
40
+ swagger_url=swagger_url,
41
+ online_repo_url=online_repo_url,
42
+ initial_page_policy=initial_page_policy,
43
+ ga_id=ga_id,
44
+ er_diagram=er_diagram,
45
+ enable_pydantic_resolve_meta=enable_pydantic_resolve_meta,
46
+ framework_name="Litestar",
47
+ )
48
+ self.gzip_minimum_size = gzip_minimum_size
49
+
50
+ def create_app(self) -> Litestar:
51
+ """Create and return a Litestar application with voyager endpoints."""
52
+
53
+ @get("/er-diagram")
54
+ async def get_er_diagram(request: Request) -> str:
55
+ payload = await request.json()
56
+ return self.ctx.get_er_diagram_dot(payload)
57
+
58
+ @get("/dot")
59
+ async def get_dot(request: Request) -> dict:
60
+ data = self.ctx.get_option_param()
61
+ # Convert tags and schemas to dicts for JSON serialization
62
+ return {
63
+ "tags": [self._tag_to_dict(t) for t in data["tags"]],
64
+ "schemas": [self._schema_to_dict(s) for s in data["schemas"]],
65
+ "dot": data["dot"],
66
+ "enable_brief_mode": data["enable_brief_mode"],
67
+ "version": data["version"],
68
+ "initial_page_policy": data["initial_page_policy"],
69
+ "swagger_url": data["swagger_url"],
70
+ "has_er_diagram": data["has_er_diagram"],
71
+ "enable_pydantic_resolve_meta": data["enable_pydantic_resolve_meta"],
72
+ "framework_name": data["framework_name"],
73
+ }
74
+
75
+ @post("/dot-search")
76
+ async def get_search_dot(request: Request) -> dict:
77
+ payload = await request.json()
78
+ tags = self.ctx.get_search_dot(payload)
79
+ return {"tags": [self._tag_to_dict(t) for t in tags]}
80
+
81
+ @post("/dot")
82
+ async def get_filtered_dot(request: Request) -> str:
83
+ payload = await request.json()
84
+ return self.ctx.get_filtered_dot(payload)
85
+
86
+ @post("/dot-core-data")
87
+ async def get_filtered_dot_core_data(request: Request) -> CoreData:
88
+ payload = await request.json()
89
+ return self.ctx.get_core_data(payload)
90
+
91
+ @post("/dot-render-core-data")
92
+ async def render_dot_from_core_data(request: Request) -> str:
93
+ payload = await request.json()
94
+ core_data = CoreData(**payload)
95
+ return self.ctx.render_dot_from_core_data(core_data)
96
+
97
+ @get("/", media_type=MediaType.HTML)
98
+ async def index() -> str:
99
+ return self.ctx.get_index_html()
100
+
101
+ @post("/source")
102
+ async def get_object_by_module_name(request: Request) -> dict:
103
+ payload = await request.json()
104
+ result = self.ctx.get_source_code(payload.get("schema_name", ""))
105
+ status_code = 200 if "error" not in result else 400
106
+ if "error" in result and "not found" in result["error"]:
107
+ status_code = 404
108
+ return Response(
109
+ content=result,
110
+ status_code=status_code,
111
+ media_type=MediaType.JSON,
112
+ )
113
+
114
+ @post("/vscode-link")
115
+ async def get_vscode_link_by_module_name(request: Request) -> dict:
116
+ payload = await request.json()
117
+ result = self.ctx.get_vscode_link(payload.get("schema_name", ""))
118
+ status_code = 200 if "error" not in result else 400
119
+ if "error" in result and "not found" in result["error"]:
120
+ status_code = 404
121
+ return Response(
122
+ content=result,
123
+ status_code=status_code,
124
+ media_type=MediaType.JSON,
125
+ )
126
+
127
+ # Create static files router using the new API (replaces deprecated StaticFilesConfig)
128
+ static_files_router = create_static_files_router(
129
+ path=STATIC_FILES_PATH,
130
+ directories=[str(WEB_DIR)],
131
+ )
132
+
133
+ # Create Litestar app
134
+ app = Litestar(
135
+ route_handlers=[
136
+ get_er_diagram,
137
+ get_dot,
138
+ get_search_dot,
139
+ get_filtered_dot,
140
+ get_filtered_dot_core_data,
141
+ render_dot_from_core_data,
142
+ index,
143
+ get_object_by_module_name,
144
+ get_vscode_link_by_module_name,
145
+ static_files_router,
146
+ ],
147
+ )
148
+
149
+ return app
150
+
151
+ def _tag_to_dict(self, tag: Tag) -> dict:
152
+ """Convert Tag object to dict."""
153
+ return {
154
+ "id": tag.id,
155
+ "name": tag.name,
156
+ "routes": [
157
+ {
158
+ "id": r.id,
159
+ "name": r.name,
160
+ "module": r.module,
161
+ "unique_id": r.unique_id,
162
+ "response_schema": r.response_schema,
163
+ "is_primitive": r.is_primitive,
164
+ }
165
+ for r in tag.routes
166
+ ],
167
+ }
168
+
169
+ def _schema_to_dict(self, schema: SchemaNode) -> dict:
170
+ """Convert SchemaNode to dict."""
171
+ return {
172
+ "id": schema.id,
173
+ "module": schema.module,
174
+ "name": schema.name,
175
+ "fields": [
176
+ {
177
+ "name": f.name,
178
+ "type_name": f.type_name,
179
+ "is_object": f.is_object,
180
+ "is_exclude": f.is_exclude,
181
+ }
182
+ for f in schema.fields
183
+ ],
184
+ }
185
+
186
+ def get_mount_path(self) -> str:
187
+ """Get the recommended mount path for voyager."""
188
+ return "/voyager"
@@ -1,25 +1,26 @@
1
1
  from __future__ import annotations
2
2
 
3
- from fastapi_voyager.type_helper import (
4
- update_forward_refs,
5
- full_class_name,
6
- get_core_types,
7
- get_type_name
8
- )
3
+ from logging import getLogger
4
+
5
+ from pydantic import BaseModel
6
+ from pydantic_resolve import Entity, ErDiagram, MultipleRelationship, Relationship
7
+
8
+ from fastapi_voyager.render import Renderer
9
+ from fastapi_voyager.render_style import RenderConfig
9
10
  from fastapi_voyager.type import (
10
- FieldInfo,
11
11
  PK,
12
+ FieldInfo,
12
13
  FieldType,
13
- LinkType,
14
14
  Link,
15
- ModuleNode,
15
+ LinkType,
16
16
  SchemaNode,
17
17
  )
18
- from fastapi_voyager.render import Renderer
19
- from fastapi_voyager.render_style import RenderConfig
20
- from pydantic import BaseModel
21
- from pydantic_resolve import ErDiagram, Entity, Relationship, MultipleRelationship
22
- from logging import getLogger
18
+ from fastapi_voyager.type_helper import (
19
+ full_class_name,
20
+ get_core_types,
21
+ get_type_name,
22
+ update_forward_refs,
23
+ )
23
24
 
24
25
  logger = getLogger(__name__)
25
26
 
@@ -0,0 +1,34 @@
1
+ """
2
+ Introspectors for different web frameworks.
3
+
4
+ This package contains built-in introspector implementations for various frameworks.
5
+ """
6
+ from .base import AppIntrospector, RouteInfo
7
+ from .detector import FrameworkType, detect_framework, get_introspector
8
+
9
+ # Try to import each introspector, but don't fail if the framework isn't installed
10
+ try:
11
+ from .fastapi import FastAPIIntrospector
12
+ except ImportError:
13
+ FastAPIIntrospector = None # type: ignore
14
+
15
+ try:
16
+ from .django_ninja import DjangoNinjaIntrospector
17
+ except ImportError:
18
+ DjangoNinjaIntrospector = None # type: ignore
19
+
20
+ try:
21
+ from .litestar import LitestarIntrospector
22
+ except ImportError:
23
+ LitestarIntrospector = None # type: ignore
24
+
25
+ __all__ = [
26
+ "AppIntrospector",
27
+ "RouteInfo",
28
+ "FastAPIIntrospector",
29
+ "DjangoNinjaIntrospector",
30
+ "LitestarIntrospector",
31
+ "FrameworkType",
32
+ "detect_framework",
33
+ "get_introspector",
34
+ ]
@@ -0,0 +1,81 @@
1
+ """
2
+ Introspection abstraction layer for framework-agnostic route analysis.
3
+
4
+ This module provides the abstraction that allows fastapi-voyager to work with
5
+ different web frameworks that support OpenAPI and Pydantic, such as:
6
+ - FastAPI
7
+ - Django Ninja
8
+ - Litestar
9
+ - Flask-OpenAPI
10
+ """
11
+ from abc import ABC, abstractmethod
12
+ from collections.abc import Callable, Iterator
13
+ from dataclasses import dataclass
14
+ from typing import Any
15
+
16
+
17
+ @dataclass
18
+ class RouteInfo:
19
+ """
20
+ Standardized route information that works across different frameworks.
21
+
22
+ This data class encapsulates the essential information needed by voyager
23
+ to analyze and visualize routes, independent of the underlying framework.
24
+ """
25
+ # Unique identifier for the route (function path)
26
+ id: str
27
+
28
+ # Human-readable name (function name)
29
+ name: str
30
+
31
+ # Module where the route handler is defined
32
+ module: str
33
+
34
+ # Operation ID from OpenAPI spec
35
+ operation_id: str | None
36
+
37
+ # List of tags associated with this route
38
+ tags: list[str]
39
+
40
+ # The route handler function/endpoint
41
+ endpoint: Callable
42
+
43
+ # Response model (should be a Pydantic BaseModel)
44
+ response_model: type[Any]
45
+
46
+ # Any additional framework-specific data
47
+ extra: dict[str, Any] | None = None
48
+
49
+
50
+ class AppIntrospector(ABC):
51
+ """
52
+ Abstract base class for app introspection.
53
+
54
+ Implement this class to add support for different web frameworks.
55
+ The introspector is responsible for extracting route information
56
+ from the framework's internal structure.
57
+ """
58
+
59
+ @abstractmethod
60
+ def get_routes(self) -> Iterator[RouteInfo]:
61
+ """
62
+ Iterate over all available routes in the application.
63
+
64
+ Yields:
65
+ RouteInfo: Standardized route information
66
+
67
+ Example:
68
+ >>> for route in introspector.get_routes():
69
+ ... print(f"{route.id}: {route.tags}")
70
+ """
71
+ pass
72
+
73
+ @abstractmethod
74
+ def get_swagger_url(self) -> str | None:
75
+ """
76
+ Get the URL to the Swagger/OpenAPI documentation.
77
+
78
+ Returns:
79
+ The URL path or None if not available
80
+ """
81
+ pass
@@ -0,0 +1,123 @@
1
+ """
2
+ Framework detection utility for fastapi-voyager.
3
+
4
+ This module provides a centralized framework detection mechanism that is used
5
+ by both introspectors and adapters to avoid code duplication.
6
+ """
7
+ from enum import Enum
8
+ from typing import Any
9
+
10
+ from fastapi_voyager.introspectors.base import AppIntrospector
11
+
12
+
13
+ class FrameworkType(Enum):
14
+ """Supported framework types."""
15
+ FASTAPI = "fastapi"
16
+ DJANGO_NINJA = "django_ninja"
17
+ LITESTAR = "litestar"
18
+ UNKNOWN = "unknown"
19
+
20
+
21
+ def detect_framework(app: Any) -> FrameworkType:
22
+ """
23
+ Detect the framework type of the given application.
24
+
25
+ This function uses the same detection logic as the introspector system,
26
+ ensuring consistency across the codebase.
27
+
28
+ Args:
29
+ app: A web application instance
30
+
31
+ Returns:
32
+ FrameworkType: The detected framework type
33
+
34
+ Note:
35
+ The detection order matters: Litestar is checked before Django Ninja
36
+ to avoid Django import issues.
37
+ """
38
+ # If it's already an introspector, try to determine framework from it
39
+ if isinstance(app, AppIntrospector):
40
+ app_class_name = type(app).__name__
41
+ if "FastAPI" in app_class_name:
42
+ return FrameworkType.FASTAPI
43
+ elif "DjangoNinja" in app_class_name or "Ninja" in app_class_name:
44
+ return FrameworkType.DJANGO_NINJA
45
+ elif "Litestar" in app_class_name:
46
+ return FrameworkType.LITESTAR
47
+ return FrameworkType.UNKNOWN
48
+
49
+ # Get the class name for type checking
50
+ app_class_name = type(app).__name__
51
+
52
+ # Try FastAPI
53
+ try:
54
+ from fastapi import FastAPI
55
+ if isinstance(app, FastAPI):
56
+ return FrameworkType.FASTAPI
57
+ except ImportError:
58
+ pass
59
+
60
+ # Try Litestar (check before Django Ninja to avoid Django import issues)
61
+ try:
62
+ from litestar import Litestar
63
+ if isinstance(app, Litestar):
64
+ return FrameworkType.LITESTAR
65
+ except ImportError:
66
+ pass
67
+
68
+ # Try Django Ninja (check by class name first to avoid import if not needed)
69
+ try:
70
+ if app_class_name == "NinjaAPI":
71
+ from ninja import NinjaAPI
72
+ if isinstance(app, NinjaAPI):
73
+ return FrameworkType.DJANGO_NINJA
74
+ except ImportError:
75
+ pass
76
+
77
+ return FrameworkType.UNKNOWN
78
+
79
+
80
+ def get_introspector(app: Any) -> AppIntrospector | None:
81
+ """
82
+ Get the appropriate introspector for the given app.
83
+
84
+ This is a centralized function that uses the framework detection logic
85
+ to return the correct introspector instance.
86
+
87
+ Args:
88
+ app: A web application instance or AppIntrospector
89
+
90
+ Returns:
91
+ An AppIntrospector instance, or None if framework not supported
92
+
93
+ Raises:
94
+ TypeError: If the app type is not supported
95
+ """
96
+ # If it's already an introspector, return it
97
+ if isinstance(app, AppIntrospector):
98
+ return app
99
+
100
+ framework = detect_framework(app)
101
+
102
+ if framework == FrameworkType.FASTAPI:
103
+ from fastapi_voyager.introspectors import FastAPIIntrospector
104
+ if FastAPIIntrospector:
105
+ return FastAPIIntrospector(app)
106
+
107
+ elif framework == FrameworkType.LITESTAR:
108
+ from fastapi_voyager.introspectors import LitestarIntrospector
109
+ if LitestarIntrospector:
110
+ return LitestarIntrospector(app)
111
+
112
+ elif framework == FrameworkType.DJANGO_NINJA:
113
+ from fastapi_voyager.introspectors import DjangoNinjaIntrospector
114
+ if DjangoNinjaIntrospector:
115
+ return DjangoNinjaIntrospector(app)
116
+
117
+ # If we get here, the app type is not supported
118
+ raise TypeError(
119
+ f"Unsupported app type: {type(app).__name__}. "
120
+ f"Supported types: FastAPI, Django Ninja API, Litestar, or any AppIntrospector implementation. "
121
+ f"If you're using a different framework, please implement AppIntrospector for that framework. "
122
+ f"See ADAPTER_EXAMPLE.md for instructions."
123
+ )