fastapi-voyager 0.15.6__py3-none-any.whl → 0.16.0a2__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 (30) 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 +167 -0
  7. fastapi_voyager/adapters/litestar_adapter.py +188 -0
  8. fastapi_voyager/cli.py +11 -5
  9. fastapi_voyager/er_diagram.py +15 -14
  10. fastapi_voyager/introspectors/__init__.py +34 -0
  11. fastapi_voyager/introspectors/base.py +81 -0
  12. fastapi_voyager/introspectors/detector.py +123 -0
  13. fastapi_voyager/introspectors/django_ninja.py +114 -0
  14. fastapi_voyager/introspectors/fastapi.py +94 -0
  15. fastapi_voyager/introspectors/litestar.py +166 -0
  16. fastapi_voyager/pydantic_resolve_util.py +4 -2
  17. fastapi_voyager/render.py +2 -2
  18. fastapi_voyager/render_style.py +0 -1
  19. fastapi_voyager/server.py +174 -295
  20. fastapi_voyager/type_helper.py +2 -2
  21. fastapi_voyager/version.py +1 -1
  22. fastapi_voyager/voyager.py +75 -47
  23. fastapi_voyager/web/index.html +11 -14
  24. fastapi_voyager/web/store.js +2 -0
  25. fastapi_voyager/web/vue-main.js +4 -0
  26. {fastapi_voyager-0.15.6.dist-info → fastapi_voyager-0.16.0a2.dist-info}/METADATA +152 -8
  27. {fastapi_voyager-0.15.6.dist-info → fastapi_voyager-0.16.0a2.dist-info}/RECORD +30 -18
  28. {fastapi_voyager-0.15.6.dist-info → fastapi_voyager-0.16.0a2.dist-info}/WHEEL +0 -0
  29. {fastapi_voyager-0.15.6.dist-info → fastapi_voyager-0.16.0a2.dist-info}/entry_points.txt +0 -0
  30. {fastapi_voyager-0.15.6.dist-info → fastapi_voyager-0.16.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,167 @@
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 pydantic import BaseModel
9
+
10
+ from fastapi_voyager.adapters.base import VoyagerAdapter
11
+ from fastapi_voyager.adapters.common import STATIC_FILES_PATH, VoyagerContext
12
+ from fastapi_voyager.type import CoreData, SchemaNode, Tag
13
+
14
+
15
+ class OptionParam(BaseModel):
16
+ tags: list[Tag]
17
+ schemas: list[SchemaNode]
18
+ dot: str
19
+ enable_brief_mode: bool
20
+ version: str
21
+ initial_page_policy: Literal["first", "full", "empty"]
22
+ swagger_url: str | None = None
23
+ has_er_diagram: bool = False
24
+ enable_pydantic_resolve_meta: bool = False
25
+ framework_name: str = "API"
26
+
27
+
28
+ class Payload(BaseModel):
29
+ tags: list[str] | None = None
30
+ schema_name: str | None = None
31
+ schema_field: str | None = None
32
+ route_name: str | None = None
33
+ show_fields: str = "object"
34
+ brief: bool = False
35
+ hide_primitive_route: bool = False
36
+ show_module: bool = True
37
+ show_pydantic_resolve_meta: bool = False
38
+
39
+
40
+ class SearchResultOptionParam(BaseModel):
41
+ tags: list[Tag]
42
+
43
+
44
+ class SchemaSearchPayload(BaseModel):
45
+ schema_name: str | None = None
46
+ schema_field: str | None = None
47
+ show_fields: str = "object"
48
+ brief: bool = False
49
+ hide_primitive_route: bool = False
50
+ show_module: bool = True
51
+ show_pydantic_resolve_meta: bool = False
52
+
53
+
54
+ class ErDiagramPayload(BaseModel):
55
+ show_fields: str = "object"
56
+ show_module: bool = True
57
+
58
+
59
+ class SourcePayload(BaseModel):
60
+ schema_name: str
61
+
62
+
63
+ class FastAPIAdapter(VoyagerAdapter):
64
+ """
65
+ FastAPI-specific implementation of VoyagerAdapter.
66
+
67
+ Creates a FastAPI application with voyager endpoints.
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ target_app: Any,
73
+ module_color: dict[str, str] | None = None,
74
+ gzip_minimum_size: int | None = 500,
75
+ module_prefix: str | None = None,
76
+ swagger_url: str | None = None,
77
+ online_repo_url: str | None = None,
78
+ initial_page_policy: str = "first",
79
+ ga_id: str | None = None,
80
+ er_diagram: Any = None,
81
+ enable_pydantic_resolve_meta: bool = False,
82
+ ):
83
+ self.ctx = VoyagerContext(
84
+ target_app=target_app,
85
+ module_color=module_color,
86
+ module_prefix=module_prefix,
87
+ swagger_url=swagger_url,
88
+ online_repo_url=online_repo_url,
89
+ initial_page_policy=initial_page_policy,
90
+ ga_id=ga_id,
91
+ er_diagram=er_diagram,
92
+ enable_pydantic_resolve_meta=enable_pydantic_resolve_meta,
93
+ framework_name="FastAPI",
94
+ )
95
+ self.gzip_minimum_size = gzip_minimum_size
96
+
97
+ def create_app(self) -> Any:
98
+ """Create and return a FastAPI application with voyager endpoints."""
99
+ # Lazy import FastAPI to avoid import errors when framework is not installed
100
+ from fastapi import APIRouter, FastAPI
101
+ from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
102
+ from fastapi.staticfiles import StaticFiles
103
+ from starlette.middleware.gzip import GZipMiddleware
104
+
105
+ router = APIRouter(tags=["fastapi-voyager"])
106
+
107
+ @router.post("/er-diagram", response_class=PlainTextResponse)
108
+ def get_er_diagram(payload: ErDiagramPayload) -> str:
109
+ return self.ctx.get_er_diagram_dot(payload.model_dump())
110
+
111
+ @router.get("/dot", response_model=OptionParam)
112
+ def get_dot() -> OptionParam:
113
+ data = self.ctx.get_option_param()
114
+ return OptionParam(**data)
115
+
116
+ @router.post("/dot-search", response_model=SearchResultOptionParam)
117
+ def get_search_dot(payload: SchemaSearchPayload) -> SearchResultOptionParam:
118
+ tags = self.ctx.get_search_dot(payload.model_dump())
119
+ return SearchResultOptionParam(tags=tags)
120
+
121
+ @router.post("/dot", response_class=PlainTextResponse)
122
+ def get_filtered_dot(payload: Payload) -> str:
123
+ return self.ctx.get_filtered_dot(payload.model_dump())
124
+
125
+ @router.post("/dot-core-data", response_model=CoreData)
126
+ def get_filtered_dot_core_data(payload: Payload) -> CoreData:
127
+ return self.ctx.get_core_data(payload.model_dump())
128
+
129
+ @router.post("/dot-render-core-data", response_class=PlainTextResponse)
130
+ def render_dot_from_core_data(core_data: CoreData) -> str:
131
+ return self.ctx.render_dot_from_core_data(core_data)
132
+
133
+ @router.get("/", response_class=HTMLResponse)
134
+ def index() -> str:
135
+ return self.ctx.get_index_html()
136
+
137
+ @router.post("/source")
138
+ def get_object_by_module_name(payload: SourcePayload) -> JSONResponse:
139
+ result = self.ctx.get_source_code(payload.schema_name)
140
+ status_code = 200 if "error" not in result else 400
141
+ if "error" in result and "not found" in result["error"]:
142
+ status_code = 404
143
+ return JSONResponse(content=result, status_code=status_code)
144
+
145
+ @router.post("/vscode-link")
146
+ def get_vscode_link_by_module_name(payload: SourcePayload) -> JSONResponse:
147
+ result = self.ctx.get_vscode_link(payload.schema_name)
148
+ status_code = 200 if "error" not in result else 400
149
+ if "error" in result and "not found" in result["error"]:
150
+ status_code = 404
151
+ return JSONResponse(content=result, status_code=status_code)
152
+
153
+ app = FastAPI(title="fastapi-voyager demo server")
154
+
155
+ if self.gzip_minimum_size is not None and self.gzip_minimum_size >= 0:
156
+ app.add_middleware(GZipMiddleware, minimum_size=self.gzip_minimum_size)
157
+
158
+ from fastapi_voyager.adapters.common import WEB_DIR
159
+
160
+ app.mount(STATIC_FILES_PATH, StaticFiles(directory=str(WEB_DIR)), name="static")
161
+ app.include_router(router)
162
+
163
+ return app
164
+
165
+ def get_mount_path(self) -> str:
166
+ """Get the recommended mount path for voyager."""
167
+ 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 fastapi_voyager.adapters.base import VoyagerAdapter
9
+ from fastapi_voyager.adapters.common import STATIC_FILES_PATH, WEB_DIR, VoyagerContext
10
+ from fastapi_voyager.type import CoreData, SchemaNode, Tag
11
+
12
+
13
+ class LitestarAdapter(VoyagerAdapter):
14
+ """
15
+ Litestar-specific implementation of VoyagerAdapter.
16
+
17
+ Creates a Litestar application with voyager endpoints.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ target_app: Any,
23
+ module_color: dict[str, str] | None = None,
24
+ gzip_minimum_size: int | None = 500,
25
+ module_prefix: str | None = None,
26
+ swagger_url: str | None = None,
27
+ online_repo_url: str | None = None,
28
+ initial_page_policy: str = "first",
29
+ ga_id: str | None = None,
30
+ er_diagram: Any = None,
31
+ enable_pydantic_resolve_meta: bool = False,
32
+ ):
33
+ self.ctx = VoyagerContext(
34
+ target_app=target_app,
35
+ module_color=module_color,
36
+ module_prefix=module_prefix,
37
+ swagger_url=swagger_url,
38
+ online_repo_url=online_repo_url,
39
+ initial_page_policy=initial_page_policy,
40
+ ga_id=ga_id,
41
+ er_diagram=er_diagram,
42
+ enable_pydantic_resolve_meta=enable_pydantic_resolve_meta,
43
+ framework_name="Litestar",
44
+ )
45
+ self.gzip_minimum_size = gzip_minimum_size
46
+
47
+ def create_app(self) -> Any:
48
+ """Create and return a Litestar application with voyager endpoints."""
49
+ # Lazy import Litestar to avoid import errors when framework is not installed
50
+ from litestar import Litestar, MediaType, Request, Response, get, post
51
+ from litestar.static_files import create_static_files_router
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"
fastapi_voyager/cli.py CHANGED
@@ -5,17 +5,19 @@ import importlib.util
5
5
  import logging
6
6
  import os
7
7
  import sys
8
-
9
- from fastapi import FastAPI
8
+ from typing import TYPE_CHECKING
10
9
 
11
10
  from fastapi_voyager import server as viz_server
12
11
  from fastapi_voyager.version import __version__
13
12
  from fastapi_voyager.voyager import Voyager
14
13
 
14
+ if TYPE_CHECKING:
15
+ from fastapi import FastAPI
16
+
15
17
  logger = logging.getLogger(__name__)
16
18
 
17
19
 
18
- def load_fastapi_app_from_file(module_path: str, app_name: str = "app") -> FastAPI | None:
20
+ def load_fastapi_app_from_file(module_path: str, app_name: str = "app") -> "FastAPI | None":
19
21
  """Load FastAPI app from a Python module file."""
20
22
  try:
21
23
  # Convert relative path to absolute path
@@ -35,6 +37,8 @@ def load_fastapi_app_from_file(module_path: str, app_name: str = "app") -> FastA
35
37
  # Get the FastAPI app instance
36
38
  if hasattr(module, app_name):
37
39
  app = getattr(module, app_name)
40
+ # Lazy import to avoid import errors when FastAPI is not installed
41
+ from fastapi import FastAPI
38
42
  if isinstance(app, FastAPI):
39
43
  return app
40
44
  logger.error(f"'{app_name}' is not a FastAPI instance")
@@ -47,7 +51,7 @@ def load_fastapi_app_from_file(module_path: str, app_name: str = "app") -> FastA
47
51
  return None
48
52
 
49
53
 
50
- def load_fastapi_app_from_module(module_name: str, app_name: str = "app") -> FastAPI | None:
54
+ def load_fastapi_app_from_module(module_name: str, app_name: str = "app") -> "FastAPI | None":
51
55
  """Load FastAPI app from a Python module name."""
52
56
  try:
53
57
  # Temporarily add the current working directory to sys.path
@@ -65,6 +69,8 @@ def load_fastapi_app_from_module(module_name: str, app_name: str = "app") -> Fas
65
69
  # Get the FastAPI app instance
66
70
  if hasattr(module, app_name):
67
71
  app = getattr(module, app_name)
72
+ # Lazy import to avoid import errors when FastAPI is not installed
73
+ from fastapi import FastAPI
68
74
  if isinstance(app, FastAPI):
69
75
  return app
70
76
  logger.error(f"'{app_name}' is not a FastAPI instance")
@@ -85,7 +91,7 @@ def load_fastapi_app_from_module(module_name: str, app_name: str = "app") -> Fas
85
91
 
86
92
 
87
93
  def generate_visualization(
88
- app: FastAPI,
94
+ app: "FastAPI",
89
95
  output_file: str = "router_viz.dot", tags: list[str] | None = None,
90
96
  schema: str | None = None,
91
97
  show_fields: bool = False,
@@ -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
+ )