-
Notifications
You must be signed in to change notification settings - Fork 640
Expand file tree
/
Copy pathbase_app.py
More file actions
214 lines (169 loc) · 7.4 KB
/
base_app.py
File metadata and controls
214 lines (169 loc) · 7.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
"""
Base application module
Contains business-agnostic FastAPI base configurations such as CORS, middleware, lifecycle management, etc.
"""
import os
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from core.observation.logger import get_logger
from core.middleware.database_session_middleware import DatabaseSessionMiddleware
from core.middleware.global_exception_handler import global_exception_handler
from core.middleware.profile_middleware import ProfileMiddleware
from core.di.utils import get_bean_by_type
from core.component.database_connection_provider import DatabaseConnectionProvider
from core.lifespan.lifespan_factory import LifespanFactory
# Recommended usage: obtain logger once at the module top, then use directly (high performance)
logger = get_logger(__name__)
def create_base_app(
cors_origins=None,
cors_allow_credentials=True,
cors_allow_methods=None,
cors_allow_headers=None,
lifespan_context=None,
):
"""
Create a base FastAPI application
Args:
cors_origins (list[str], optional): List of allowed CORS origins, default is ["*"]
cors_allow_credentials (bool): Whether to allow credentials, default is True
cors_allow_methods (list[str], optional): Allowed HTTP methods, default is ["*"]
cors_allow_headers (list[str], optional): Allowed HTTP headers, default is ["*"]
lifespan_context (callable, optional): Lifecycle context manager, default uses database lifecycle
Returns:
FastAPI: Configured FastAPI application instance
"""
# Use the provided lifespan context or default database lifecycle
if lifespan_context is None:
lifespan_factory = get_bean_by_type(LifespanFactory)
lifespan_context = lifespan_factory.create_lifespan_with_names(
["database_lifespan_provider"]
)
# Control docs display based on environment variable
# Only enable docs in development environment (ENV=dev)
env = os.environ.get('ENV', 'prod').upper()
enable_docs = env == 'DEV'
# Create FastAPI application
app = FastAPI(
lifespan=lifespan_context,
docs_url="/docs" if enable_docs else None,
redoc_url="/redoc" if enable_docs else None,
openapi_url="/openapi.json" if enable_docs else None,
)
if enable_docs:
logger.info("FastAPI documentation enabled (ENV=%s)", env)
else:
logger.info("FastAPI documentation disabled (ENV=%s)", env)
# Set default CORS values
if cors_origins is None:
cors_origins = ["*"]
if cors_allow_methods is None:
cors_allow_methods = ["*"]
if cors_allow_headers is None:
cors_allow_headers = ["*"]
# Add HTTP exception handler, otherwise HTTPException won't be handled by global_exception_handler
app.add_exception_handler(HTTPException, global_exception_handler)
# Convert FastAPI/Pydantic 422 validation errors to our ErrorApiResponse format
app.add_exception_handler(RequestValidationError, global_exception_handler)
# Add global exception handler
# Acts as a fallback outside middleware
app.add_exception_handler(Exception, global_exception_handler)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=cors_origins,
allow_credentials=cors_allow_credentials,
allow_methods=cors_allow_methods,
allow_headers=cors_allow_headers,
)
# Add basic middleware
# The order of middleware matters: the earlier added, the later executed
# app.add_middleware(DatabaseSessionMiddleware)
# Add performance profiling middleware (add last, executes first)
app.add_middleware(ProfileMiddleware)
# Mount lifespan management methods to app instance
_mount_lifespan_methods(app)
return app
def _mount_lifespan_methods(app: FastAPI):
"""
Mount lifespan management methods to the FastAPI application instance
After mounting, you can directly use:
- app.start_lifespan(): Start lifespan
- app.exit_lifespan(): Exit lifespan
Args:
app (FastAPI): FastAPI application instance
"""
# Store reference to lifespan manager
app.lifespan_manager = None
async def start_lifespan():
"""Start the application's lifespan context manager"""
if app.lifespan_manager is not None:
logger.warning("Lifespan already started, no need to start again")
return app.lifespan_manager
# Get lifespan context manager
lifespan_context = app.router.lifespan_context
if lifespan_context:
# Create context manager instance
lifespan_manager = lifespan_context(app)
# Manually enter context (equivalent to starting)
await lifespan_manager.__aenter__()
# Store manager reference
app.lifespan_manager = lifespan_manager
logger.info("Application Lifespan startup completed")
return lifespan_manager
else:
logger.warning("This application has no lifespan configured")
return None
async def exit_lifespan():
"""Exit the application's lifespan context manager"""
if app.lifespan_manager is None:
logger.warning("Lifespan not started or already exited")
return
try:
# Manually exit context
await app.lifespan_manager.__aexit__(None, None, None)
logger.info("Application Lifespan exit completed")
except (AttributeError, RuntimeError) as e:
logger.error("Error occurred when exiting Lifespan: %s", str(e))
finally:
# Clean up reference
app.lifespan_manager = None
# Mount methods to app instance
app.start_lifespan = start_lifespan
app.exit_lifespan = exit_lifespan
async def manually_start_lifespan(app: FastAPI):
"""
Manually start the lifespan context manager of a FastAPI application
Note: It is recommended to use the convenient methods mounted on the app instance:
- app.start_lifespan(): Start lifespan
- app.exit_lifespan(): Exit lifespan
This function is used to initialize the application lifecycle without starting an HTTP server,
including database connections, business graph structures, etc. Suitable for scripts, tests,
or other scenarios requiring application context but not HTTP services.
Args:
app (FastAPI): FastAPI application instance
Returns:
context_manager: Lifecycle context manager instance, can be used for manual exit
Example:
```python
from app import app
# Recommended way: directly use mounted methods
await app.start_lifespan()
# Perform operations requiring application context
# ...
await app.exit_lifespan()
# Or use traditional way
from base_app import manually_start_lifespan
lifespan_manager = await manually_start_lifespan(app)
# await lifespan_manager.__aexit__(None, None, None)
```
"""
# Directly call the mounted method
return await app.start_lifespan()
async def close_database_connection():
"""Close the database connection pool"""
try:
db_provider = get_bean_by_type(DatabaseConnectionProvider)
await db_provider.close()
except (AttributeError, RuntimeError) as e:
logger.error("Error occurred when closing database connection: %s", str(e))