55in FastAPI applications, similar to Django's app discovery.
66"""
77
8- import importlib
9- import inspect
108from typing import Any
119
12- from chanx .asyncapi .generator import AsyncAPIGenerator
13- from chanx .core .registry import message_registry
14- from chanx .core .websocket import AsyncJsonWebsocketConsumer
15- from chanx .routing .discovery import RouteDiscovery , RouteInfo
16-
10+ from starlette .routing import Mount , WebSocketRoute
1711
18- def discover_consumers_in_module (
19- module_name : str ,
20- ) -> list [type [AsyncJsonWebsocketConsumer ]]:
21- """
22- Discover chanx consumers in a given module.
23-
24- Args:
25- module_name: The module name to scan for consumers (e.g., 'app.consumers')
26-
27- Returns:
28- List of discovered consumer classes
29- """
30- try :
31- module = importlib .import_module (module_name )
32- consumers = []
33-
34- for name in dir (module ):
35- obj = getattr (module , name )
36- if (
37- inspect .isclass (obj )
38- and issubclass (obj , AsyncJsonWebsocketConsumer )
39- and obj != AsyncJsonWebsocketConsumer
40- ):
41- consumers .append (obj )
42-
43- return consumers
44- except ImportError :
45- return []
46-
47-
48- def discover_consumers_in_packages (
49- package_names : list [str ],
50- ) -> list [type [AsyncJsonWebsocketConsumer ]]:
51- """
52- Discover chanx consumers in multiple packages.
53-
54- Args:
55- package_names: List of package names to scan
56-
57- Returns:
58- List of all discovered consumer classes
59- """
60- all_consumers = []
61-
62- for package in package_names :
63- # Try common consumer module names
64- for module_suffix in ["consumers" , "consumer" , "websockets" , "ws" ]:
65- module_name = f"{ package } .{ module_suffix } "
66- consumers = discover_consumers_in_module (module_name )
67- all_consumers .extend (consumers )
68-
69- return all_consumers
70-
71-
72- def register_consumers_with_message_registry (
73- consumers : list [type [AsyncJsonWebsocketConsumer ]],
74- ) -> None :
75- """
76- Register discovered consumers with the message registry.
77-
78- Args:
79- consumers: List of consumer classes to register
80- """
81- for consumer_class in consumers :
82- # Process message handlers
83- for attr_name in dir (consumer_class ):
84- if attr_name .startswith ("_" ):
85- continue
86-
87- try :
88- attr = getattr (consumer_class , attr_name )
89- if hasattr (attr , "_ws_handler_info" ):
90- handler_info = attr ._ws_handler_info
91- input_type = handler_info .get ("input_type" )
92- output_type = handler_info .get ("output_type" )
93-
94- if input_type :
95- message_registry .add (input_type , consumer_class .__name__ )
96- if output_type :
97- message_registry .add (output_type , consumer_class .__name__ )
98-
99- elif hasattr (attr , "_event_handler_info" ):
100- handler_info = attr ._event_handler_info
101- input_type = handler_info .get ("input_type" )
102- output_type = handler_info .get ("output_type" )
103-
104- if input_type :
105- message_registry .add (input_type , consumer_class .__name__ )
106- if output_type :
107- message_registry .add (output_type , consumer_class .__name__ )
108-
109- except (AttributeError , TypeError ):
110- continue
111-
112-
113- def auto_discover_and_register (package_names : list [str ] = None ) -> dict [str , Any ]:
114- """
115- Auto-discover consumers and register them with the message registry.
116-
117- Args:
118- package_names: List of package names to scan. If None, uses common patterns.
119-
120- Returns:
121- Dictionary with discovery results and statistics
122- """
123- if package_names is None :
124- # Try to auto-detect common package structures
125- import sys
126-
127- package_names = []
128-
129- # Look for common app structures
130- for module_name in sys .modules :
131- if (
132- "." not in module_name
133- and module_name not in ["chanx" , "fast_channels" , "channels" ]
134- and not module_name .startswith ("_" )
135- ):
136- package_names .append (module_name )
137-
138- consumers = discover_consumers_in_packages (package_names )
139- register_consumers_with_message_registry (consumers )
140-
141- return {
142- "discovered_consumers" : len (consumers ),
143- "consumer_classes" : [c .__name__ for c in consumers ],
144- "scanned_packages" : package_names ,
145- }
146-
147-
148- def get_consumer_info (
149- consumer_class : type [AsyncJsonWebsocketConsumer ],
150- ) -> dict [str , Any ]:
151- """
152- Get detailed information about a consumer class.
153-
154- Args:
155- consumer_class: The consumer class to analyze
156-
157- Returns:
158- Dictionary with consumer information
159- """
160- info = {
161- "name" : consumer_class .__name__ ,
162- "module" : consumer_class .__module__ ,
163- "doc" : consumer_class .__doc__ ,
164- "handlers" : [],
165- "channel_info" : getattr (consumer_class , "_channel_info" , None ),
166- }
167-
168- # Analyze handlers
169- for attr_name in dir (consumer_class ):
170- if attr_name .startswith ("_" ):
171- continue
172-
173- try :
174- attr = getattr (consumer_class , attr_name )
175-
176- if hasattr (attr , "_ws_handler_info" ):
177- handler_info = attr ._ws_handler_info .copy ()
178- handler_info ["type" ] = "websocket"
179- handler_info ["method" ] = attr_name
180- info ["handlers" ].append (handler_info )
181-
182- elif hasattr (attr , "_event_handler_info" ):
183- handler_info = attr ._event_handler_info .copy ()
184- handler_info ["type" ] = "event"
185- handler_info ["method" ] = attr_name
186- info ["handlers" ].append (handler_info )
187-
188- except (AttributeError , TypeError ):
189- continue
190-
191- return info
12+ from chanx .routing .discovery import RouteDiscovery , RouteInfo
19213
19314
19415class FastAPIRouteDiscovery (RouteDiscovery ):
@@ -229,7 +50,6 @@ def _walk_routes(
22950 base_url: Base WebSocket URL
23051 prefix: Current path prefix
23152 """
232- from starlette .routing import Mount , WebSocketRoute
23353
23454 # Skip if application doesn't have routes (e.g., StaticFiles)
23555 if not hasattr (application , "routes" ):
@@ -305,21 +125,6 @@ def _extract_path_params(self, path: str) -> dict[str, str] | None:
305125
306126 return params if params else None
307127
308- def _discover_from_consumers (self , routes : list [RouteInfo ], base_url : str ) -> None :
309- """Create routes from discovered consumers (fallback method)."""
310- discovery_result = auto_discover_and_register ()
311-
312- # Create basic routes for each consumer (simplified)
313- for consumer_name in discovery_result ["consumer_classes" ]:
314- # Create a basic route - in practice you'd want more sophisticated discovery
315- route = RouteInfo (
316- path = f"/ws/{ consumer_name .lower ().replace ('consumer' , '' )} " ,
317- handler = None , # Would be the actual consumer instance
318- base_url = base_url ,
319- consumer = None , # Would be the consumer class
320- )
321- routes .append (route )
322-
323128 def get_websocket_application (self ) -> Any :
324129 """Get the FastAPI WebSocket application."""
325130 return self .app
@@ -329,44 +134,3 @@ def extract_routes_from_router(
329134 ) -> None :
330135 """Extract routes from FastAPI router (using the walk method instead)."""
331136 self ._walk_routes (router , routes , base_url , prefix )
332-
333-
334- def generate_fastapi_asyncapi_spec (
335- consumers : list [type [AsyncJsonWebsocketConsumer ]] = None ,
336- app = None ,
337- title : str = "FastAPI WebSocket API" ,
338- version : str = "1.0.0" ,
339- description : str = "AsyncAPI specification for FastAPI application using chanx framework" ,
340- ) -> dict [str , Any ]:
341- """
342- Generate AsyncAPI specification for FastAPI application.
343-
344- Args:
345- consumers: List of consumer classes. If None, auto-discovers.
346- app: FastAPI application instance
347- title: API title
348- version: API version
349- description: API description
350-
351- Returns:
352- AsyncAPI specification dictionary
353- """
354- # Auto-discover consumers if not provided
355- if consumers is None :
356- auto_discover_and_register ()
357-
358- # Create route discovery
359- discovery = FastAPIRouteDiscovery (app )
360- routes = discovery .discover_routes ()
361-
362- # Generate AsyncAPI spec using the existing generator
363- generator = AsyncAPIGenerator (
364- routes = routes ,
365- title = title ,
366- version = version ,
367- description = description ,
368- server_url = "localhost:8000" ,
369- server_protocol = "ws" ,
370- )
371-
372- return generator .generate ()
0 commit comments