1515
1616from __future__ import annotations
1717
18+ import contextlib
1819from typing import TYPE_CHECKING
1920
2021from fastapi import FastAPI
2122from starlette .middleware import Middleware
23+ from starlette .routing import Route
2224
2325from pyfly .container .ordering import get_order
2426from pyfly .web .adapters .fastapi .controller import FastAPIControllerRegistrar
@@ -43,13 +45,16 @@ def create_app(
4345 debug : bool = False ,
4446 context : ApplicationContext | None = None ,
4547 docs_enabled : bool = True ,
48+ extra_routes : list [Route ] | None = None ,
49+ actuator_enabled : bool = False ,
4650 cors : CORSConfig | None = None ,
4751 lifespan : object | None = None ,
4852) -> FastAPI :
4953 """Create a FastAPI application with PyFly enterprise middleware.
5054
5155 When ``context`` is provided, auto-discovers all ``@rest_controller`` beans
52- and mounts their routes. Also auto-discovers user ``WebFilter`` beans.
56+ and mounts their routes. Also auto-discovers user ``WebFilter``,
57+ ``ActuatorEndpoint``, ``@websocket_mapping``, and ``@sse_mapping`` beans.
5358
5459 FastAPI provides built-in OpenAPI docs (Swagger UI at ``/docs``, ReDoc at
5560 ``/redoc``), so no custom OpenAPI generator is needed.
@@ -58,7 +63,11 @@ def create_app(
5863 - WebFilter chain (transaction ID, request logging, security headers, + user filters)
5964 - Global exception handler (RFC 7807 style)
6065 - Built-in Swagger UI and ReDoc (when docs_enabled)
66+ - Actuator endpoints (when actuator_enabled)
67+ - Admin dashboard (when pyfly.admin.enabled config is set)
6168 - CORS support (when cors is provided)
69+ - WebSocket routes (auto-discovered from @websocket_mapping)
70+ - SSE routes (auto-discovered from @sse_mapping)
6271 """
6372 # --- Build the WebFilter chain ---
6473 filters : list [WebFilter ] = [
@@ -124,6 +133,169 @@ def create_app(
124133 registrar = FastAPIControllerRegistrar ()
125134 registrar .register_controllers (app , context )
126135
136+ # Auto-discover WebSocket routes from ApplicationContext
137+ if context is not None :
138+ from pyfly .websocket .adapters .starlette import WebSocketRegistrar
139+
140+ ws_registrar = WebSocketRegistrar ()
141+ app .routes .extend (ws_registrar .collect_routes (context ))
142+
143+ # Auto-discover SSE routes from ApplicationContext
144+ if context is not None :
145+ from pyfly .web .sse .adapters .starlette import SSERegistrar
146+
147+ sse_registrar = SSERegistrar ()
148+ app .routes .extend (sse_registrar .collect_routes (context ))
149+
150+ # Mount OAuth2 login routes when an OAuth2LoginHandler bean exists
151+ if context is not None :
152+ from pyfly .security .oauth2 .login import OAuth2LoginHandler
153+
154+ for _cls , reg in context .container ._registrations .items ():
155+ if reg .instance is not None and isinstance (reg .instance , OAuth2LoginHandler ):
156+ app .routes .extend (reg .instance .routes ())
157+ break
158+
159+ # Append caller-supplied routes (e.g. test helpers)
160+ if extra_routes :
161+ app .routes .extend (extra_routes )
162+
163+ # Mount actuator endpoints when enabled
164+ agg = None
165+ if actuator_enabled :
166+ from pyfly .actuator .adapters .starlette import make_starlette_actuator_routes
167+ from pyfly .actuator .endpoints .beans_endpoint import BeansEndpoint
168+ from pyfly .actuator .endpoints .env_endpoint import EnvEndpoint
169+ from pyfly .actuator .endpoints .health_endpoint import HealthEndpoint
170+ from pyfly .actuator .endpoints .info_endpoint import InfoEndpoint
171+ from pyfly .actuator .endpoints .loggers_endpoint import LoggersEndpoint
172+ from pyfly .actuator .endpoints .metrics_endpoint import MetricsEndpoint
173+ from pyfly .actuator .health import HealthAggregator , HealthIndicator
174+ from pyfly .actuator .registry import ActuatorRegistry
175+
176+ agg = HealthAggregator ()
177+
178+ # Auto-discover HealthIndicator beans from context
179+ if context is not None :
180+ for cls , reg in context .container ._registrations .items ():
181+ if reg .instance is not None and isinstance (reg .instance , HealthIndicator ):
182+ indicator_name = reg .name or cls .__name__
183+ agg .add_indicator (indicator_name , reg .instance )
184+
185+ config = context .config if context is not None else None
186+ registry = ActuatorRegistry (config = config )
187+
188+ # Register built-in endpoints
189+ registry .register (HealthEndpoint (agg ))
190+ if context is not None :
191+ registry .register (BeansEndpoint (context ))
192+ registry .register (EnvEndpoint (context ))
193+ registry .register (InfoEndpoint (context ))
194+ registry .register (LoggersEndpoint ())
195+ registry .register (MetricsEndpoint ())
196+
197+ # Auto-discover custom ActuatorEndpoint beans from context
198+ if context is not None :
199+ registry .discover_from_context (context )
200+
201+ app .routes .extend (make_starlette_actuator_routes (registry ))
202+
203+ # Mount admin dashboard when enabled
204+ admin_enabled = False
205+ if context is not None :
206+ admin_enabled = str (context .config .get ("pyfly.admin.enabled" , "false" )).lower () in (
207+ "true" ,
208+ "1" ,
209+ "yes" ,
210+ )
211+
212+ if admin_enabled and context is not None :
213+ from pyfly .admin .adapters .starlette import AdminRouteBuilder
214+ from pyfly .admin .config import AdminProperties
215+ from pyfly .admin .middleware .trace_collector import TraceCollectorFilter
216+ from pyfly .admin .providers .beans_provider import BeansProvider
217+ from pyfly .admin .providers .cache_provider import CacheProvider
218+ from pyfly .admin .providers .config_provider import ConfigProvider
219+ from pyfly .admin .providers .cqrs_provider import CqrsProvider
220+ from pyfly .admin .providers .env_provider import EnvProvider
221+ from pyfly .admin .providers .health_provider import HealthProvider
222+ from pyfly .admin .providers .logfile_provider import LogfileProvider
223+ from pyfly .admin .providers .loggers_provider import LoggersProvider
224+ from pyfly .admin .providers .mappings_provider import MappingsProvider
225+ from pyfly .admin .providers .metrics_provider import MetricsProvider
226+ from pyfly .admin .providers .overview_provider import OverviewProvider
227+ from pyfly .admin .providers .runtime_provider import RuntimeProvider
228+ from pyfly .admin .providers .scheduled_provider import ScheduledProvider
229+ from pyfly .admin .providers .server_provider import ServerProvider
230+ from pyfly .admin .providers .traces_provider import TracesProvider
231+ from pyfly .admin .providers .transactions_provider import TransactionsProvider
232+ from pyfly .admin .registry import AdminViewRegistry
233+
234+ admin_props = AdminProperties ()
235+ with contextlib .suppress (Exception ):
236+ admin_props = context .config .bind (AdminProperties )
237+
238+ # Find trace collector from context (registered by auto-config)
239+ trace_collector = None
240+ for _cls , reg in context .container ._registrations .items ():
241+ if reg .instance is not None and isinstance (reg .instance , TraceCollectorFilter ):
242+ trace_collector = reg .instance
243+ break
244+
245+ # Find view registry from context
246+ view_registry = AdminViewRegistry ()
247+ for _cls , reg in context .container ._registrations .items ():
248+ if reg .instance is not None and isinstance (reg .instance , AdminViewRegistry ):
249+ view_registry = reg .instance
250+ view_registry .discover_from_context (context )
251+ break
252+
253+ # Reuse health aggregator from actuator, or create one for admin
254+ health_agg = agg
255+ if health_agg is None :
256+ from pyfly .actuator .health import HealthAggregator , HealthIndicator
257+
258+ health_agg = HealthAggregator ()
259+ for cls , reg in context .container ._registrations .items ():
260+ if reg .instance is not None and isinstance (reg .instance , HealthIndicator ):
261+ indicator_name = reg .name or cls .__name__
262+ health_agg .add_indicator (indicator_name , reg .instance )
263+
264+ # Find server adapter from context for admin dashboard
265+ server_adapter = None
266+ try :
267+ from pyfly .server .ports .outbound import ApplicationServerPort
268+
269+ for _cls , reg in context .container ._registrations .items ():
270+ if reg .instance is not None and isinstance (reg .instance , ApplicationServerPort ):
271+ server_adapter = reg .instance
272+ break
273+ except ImportError :
274+ pass
275+
276+ admin_builder = AdminRouteBuilder (
277+ properties = admin_props ,
278+ overview = OverviewProvider (context , health_agg ),
279+ beans = BeansProvider (context ),
280+ health = HealthProvider (health_agg ),
281+ env = EnvProvider (context ),
282+ config = ConfigProvider (context ),
283+ loggers = LoggersProvider (),
284+ metrics = MetricsProvider (),
285+ scheduled = ScheduledProvider (context ),
286+ mappings = MappingsProvider (context ),
287+ caches = CacheProvider (context ),
288+ cqrs = CqrsProvider (context ),
289+ transactions = TransactionsProvider (context ),
290+ traces = TracesProvider (trace_collector ),
291+ view_registry = view_registry ,
292+ trace_collector = trace_collector ,
293+ logfile = LogfileProvider (context ),
294+ runtime = RuntimeProvider (),
295+ server = ServerProvider (server_adapter ),
296+ )
297+ app .routes .extend (admin_builder .build_routes ())
298+
127299 # Register global exception handler
128300 register_exception_handlers (app )
129301
0 commit comments