starspring 0.1.0__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.
- starspring/__init__.py +150 -0
- starspring/application.py +421 -0
- starspring/client/__init__.py +1 -0
- starspring/client/rest_client.py +220 -0
- starspring/config/__init__.py +1 -0
- starspring/config/environment.py +81 -0
- starspring/config/properties.py +146 -0
- starspring/core/__init__.py +1 -0
- starspring/core/context.py +180 -0
- starspring/core/controller.py +47 -0
- starspring/core/exceptions.py +82 -0
- starspring/core/response.py +147 -0
- starspring/data/__init__.py +47 -0
- starspring/data/database_config.py +113 -0
- starspring/data/entity.py +365 -0
- starspring/data/orm_gateway.py +256 -0
- starspring/data/query_builder.py +345 -0
- starspring/data/repository.py +324 -0
- starspring/data/schema_generator.py +151 -0
- starspring/data/transaction.py +58 -0
- starspring/decorators/__init__.py +1 -0
- starspring/decorators/components.py +179 -0
- starspring/decorators/configuration.py +102 -0
- starspring/decorators/routing.py +306 -0
- starspring/decorators/validation.py +30 -0
- starspring/middleware/__init__.py +1 -0
- starspring/middleware/cors.py +90 -0
- starspring/middleware/exception.py +83 -0
- starspring/middleware/logging.py +60 -0
- starspring/template/__init__.py +19 -0
- starspring/template/engine.py +168 -0
- starspring/template/model_and_view.py +69 -0
- starspring-0.1.0.dist-info/METADATA +284 -0
- starspring-0.1.0.dist-info/RECORD +36 -0
- starspring-0.1.0.dist-info/WHEEL +5 -0
- starspring-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Routing decorators for HTTP methods
|
|
3
|
+
|
|
4
|
+
Provides Spring Boot-style routing annotations like @GetMapping, @PostMapping, etc.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Callable, List, Optional, Any, get_type_hints
|
|
8
|
+
from functools import wraps
|
|
9
|
+
import inspect
|
|
10
|
+
from pydantic import BaseModel, ValidationError
|
|
11
|
+
from starlette.requests import Request
|
|
12
|
+
from starlette.responses import JSONResponse
|
|
13
|
+
|
|
14
|
+
from starspring.core.response import ResponseEntity
|
|
15
|
+
from starspring.core.exceptions import StarSpringException, ValidationException
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RouteInfo:
|
|
19
|
+
"""Metadata for a route"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
path: str,
|
|
24
|
+
methods: List[str],
|
|
25
|
+
handler: Callable,
|
|
26
|
+
handler_name: str
|
|
27
|
+
):
|
|
28
|
+
self.path = path
|
|
29
|
+
self.methods = methods
|
|
30
|
+
self.handler = handler
|
|
31
|
+
self.handler_name = handler_name
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _create_route_decorator(path: str, methods: List[str]) -> Callable:
|
|
35
|
+
"""
|
|
36
|
+
Internal function to create route decorators
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
path: URL path for the route
|
|
40
|
+
methods: HTTP methods for the route
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Decorator function
|
|
44
|
+
"""
|
|
45
|
+
def decorator(func: Callable) -> Callable:
|
|
46
|
+
# Store route metadata on the function
|
|
47
|
+
func._route_path = path # type: ignore
|
|
48
|
+
func._route_methods = methods # type: ignore
|
|
49
|
+
# Don't wrap here - wrapping happens when route is registered
|
|
50
|
+
return func
|
|
51
|
+
|
|
52
|
+
return decorator
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_route_handler(bound_method: Callable) -> Callable:
|
|
56
|
+
"""
|
|
57
|
+
Create a route handler from a bound method
|
|
58
|
+
|
|
59
|
+
This wraps the bound method with request handling logic.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
bound_method: Bound controller method
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Async handler function for Starlette
|
|
66
|
+
"""
|
|
67
|
+
# Get the original unbound function to inspect its signature
|
|
68
|
+
func = bound_method.__func__ if hasattr(bound_method, '__func__') else bound_method
|
|
69
|
+
|
|
70
|
+
async def handler(request: Request, **path_params):
|
|
71
|
+
# Get function signature and type hints
|
|
72
|
+
sig = inspect.signature(func)
|
|
73
|
+
try:
|
|
74
|
+
type_hints = get_type_hints(func)
|
|
75
|
+
except Exception:
|
|
76
|
+
type_hints = {}
|
|
77
|
+
|
|
78
|
+
# Prepare arguments for the handler
|
|
79
|
+
kwargs = {}
|
|
80
|
+
request_injected = False
|
|
81
|
+
|
|
82
|
+
# Process each parameter (skip 'self' as method is already bound)
|
|
83
|
+
for param_name, param in sig.parameters.items():
|
|
84
|
+
if param_name == 'self':
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
param_type = type_hints.get(param_name)
|
|
88
|
+
|
|
89
|
+
# Check if parameter is in path parameters
|
|
90
|
+
# Priority: 1. kwargs passed to handler, 2. request.path_params (from Starlette)
|
|
91
|
+
if param_name in path_params:
|
|
92
|
+
value = path_params[param_name]
|
|
93
|
+
# Type conversion for path parameters
|
|
94
|
+
if param_type and param_type != str:
|
|
95
|
+
try:
|
|
96
|
+
value = param_type(value)
|
|
97
|
+
except (ValueError, TypeError):
|
|
98
|
+
pass
|
|
99
|
+
kwargs[param_name] = value
|
|
100
|
+
elif request.path_params and param_name in request.path_params:
|
|
101
|
+
value = request.path_params[param_name]
|
|
102
|
+
# Type conversion for path parameters
|
|
103
|
+
if param_type and param_type != str:
|
|
104
|
+
try:
|
|
105
|
+
value = param_type(value)
|
|
106
|
+
except (ValueError, TypeError):
|
|
107
|
+
pass
|
|
108
|
+
kwargs[param_name] = value
|
|
109
|
+
|
|
110
|
+
# Check if parameter is a Pydantic model (request body)
|
|
111
|
+
elif param_type and (
|
|
112
|
+
(inspect.isclass(param_type) and issubclass(param_type, BaseModel))
|
|
113
|
+
):
|
|
114
|
+
try:
|
|
115
|
+
body = await request.json()
|
|
116
|
+
kwargs[param_name] = param_type(**body)
|
|
117
|
+
except ValidationError as e:
|
|
118
|
+
raise ValidationException(
|
|
119
|
+
message="Validation failed",
|
|
120
|
+
details={"errors": e.errors()}
|
|
121
|
+
)
|
|
122
|
+
except Exception:
|
|
123
|
+
# If body parsing fails, skip this parameter
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
# Check if parameter is Request type or named 'request' (legacy/convenience)
|
|
127
|
+
elif param_type == Request or param_name in ('request', 'req'):
|
|
128
|
+
kwargs[param_name] = request
|
|
129
|
+
request_injected = True
|
|
130
|
+
|
|
131
|
+
# Check query parameters
|
|
132
|
+
elif param_name in request.query_params:
|
|
133
|
+
value = request.query_params[param_name]
|
|
134
|
+
# Type conversion for query parameters
|
|
135
|
+
if param_type and param_type != str:
|
|
136
|
+
try:
|
|
137
|
+
value = param_type(value)
|
|
138
|
+
except (ValueError, TypeError):
|
|
139
|
+
pass
|
|
140
|
+
kwargs[param_name] = value
|
|
141
|
+
|
|
142
|
+
# Use default value if available
|
|
143
|
+
elif param.default != inspect.Parameter.empty:
|
|
144
|
+
kwargs[param_name] = param.default
|
|
145
|
+
|
|
146
|
+
# Fallback: Inject Request object if it's a required parameter that hasn't been resolved yet
|
|
147
|
+
# AND it's not a primitive type (unless we have no type hint, in which case we guess it might be request)
|
|
148
|
+
elif not request_injected:
|
|
149
|
+
# Don't inject into typed primitives (int, str, bool, float)
|
|
150
|
+
is_primitive = param_type in (int, str, bool, float)
|
|
151
|
+
if not is_primitive:
|
|
152
|
+
kwargs[param_name] = request
|
|
153
|
+
request_injected = True
|
|
154
|
+
|
|
155
|
+
# Call the bound method
|
|
156
|
+
result = bound_method(**kwargs)
|
|
157
|
+
|
|
158
|
+
# Handle async functions
|
|
159
|
+
if inspect.iscoroutine(result):
|
|
160
|
+
result = await result
|
|
161
|
+
|
|
162
|
+
# Handle ModelAndView (template responses)
|
|
163
|
+
from starspring.template import ModelAndView, render_template
|
|
164
|
+
from starlette.responses import HTMLResponse
|
|
165
|
+
if isinstance(result, ModelAndView):
|
|
166
|
+
html_content = render_template(result.get_view_name(), result.get_model())
|
|
167
|
+
return HTMLResponse(content=html_content)
|
|
168
|
+
|
|
169
|
+
# Convert ResponseEntity to Starlette response
|
|
170
|
+
if isinstance(result, ResponseEntity):
|
|
171
|
+
return result.to_starlette_response()
|
|
172
|
+
|
|
173
|
+
# Handle Pydantic models
|
|
174
|
+
if hasattr(result, 'model_dump'):
|
|
175
|
+
return JSONResponse(content=result.model_dump())
|
|
176
|
+
elif hasattr(result, 'dict'):
|
|
177
|
+
return JSONResponse(content=result.dict())
|
|
178
|
+
elif hasattr(result, 'to_dict'):
|
|
179
|
+
return JSONResponse(content=result.to_dict())
|
|
180
|
+
|
|
181
|
+
# Handle lists (including empty lists and lists of Pydantic/Entity models)
|
|
182
|
+
if isinstance(result, list):
|
|
183
|
+
if len(result) == 0:
|
|
184
|
+
return JSONResponse(content=[])
|
|
185
|
+
elif hasattr(result[0], 'model_dump'):
|
|
186
|
+
return JSONResponse(content=[item.model_dump() for item in result])
|
|
187
|
+
elif hasattr(result[0], 'dict'):
|
|
188
|
+
return JSONResponse(content=[item.dict() for item in result])
|
|
189
|
+
elif hasattr(result[0], 'to_dict'):
|
|
190
|
+
return JSONResponse(content=[item.to_dict() for item in result])
|
|
191
|
+
else:
|
|
192
|
+
return JSONResponse(content=result)
|
|
193
|
+
|
|
194
|
+
# Handle dicts
|
|
195
|
+
if isinstance(result, dict):
|
|
196
|
+
return JSONResponse(content=result)
|
|
197
|
+
|
|
198
|
+
# Return as-is for other types (Starlette will handle it if it's a Response, otherwise it might fail)
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
return handler
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def GetMapping(path: str) -> Callable:
|
|
205
|
+
"""
|
|
206
|
+
Map HTTP GET requests to a handler method
|
|
207
|
+
|
|
208
|
+
Similar to Spring Boot's @GetMapping annotation.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
path: URL path for the route
|
|
212
|
+
|
|
213
|
+
Example:
|
|
214
|
+
@GetMapping("/users/{id}")
|
|
215
|
+
def get_user(self, id: int):
|
|
216
|
+
return user
|
|
217
|
+
"""
|
|
218
|
+
return _create_route_decorator(path, ["GET"])
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def PostMapping(path: str) -> Callable:
|
|
222
|
+
"""
|
|
223
|
+
Map HTTP POST requests to a handler method
|
|
224
|
+
|
|
225
|
+
Similar to Spring Boot's @PostMapping annotation.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
path: URL path for the route
|
|
229
|
+
|
|
230
|
+
Example:
|
|
231
|
+
@PostMapping("/users")
|
|
232
|
+
def create_user(self, user: UserCreateRequest):
|
|
233
|
+
return created_user
|
|
234
|
+
"""
|
|
235
|
+
return _create_route_decorator(path, ["POST"])
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def PutMapping(path: str) -> Callable:
|
|
239
|
+
"""
|
|
240
|
+
Map HTTP PUT requests to a handler method
|
|
241
|
+
|
|
242
|
+
Similar to Spring Boot's @PutMapping annotation.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
path: URL path for the route
|
|
246
|
+
|
|
247
|
+
Example:
|
|
248
|
+
@PutMapping("/users/{id}")
|
|
249
|
+
def update_user(self, id: int, user: UserUpdateRequest):
|
|
250
|
+
return updated_user
|
|
251
|
+
"""
|
|
252
|
+
return _create_route_decorator(path, ["PUT"])
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def DeleteMapping(path: str) -> Callable:
|
|
256
|
+
"""
|
|
257
|
+
Map HTTP DELETE requests to a handler method
|
|
258
|
+
|
|
259
|
+
Similar to Spring Boot's @DeleteMapping annotation.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
path: URL path for the route
|
|
263
|
+
|
|
264
|
+
Example:
|
|
265
|
+
@DeleteMapping("/users/{id}")
|
|
266
|
+
def delete_user(self, id: int):
|
|
267
|
+
return ResponseEntity.no_content()
|
|
268
|
+
"""
|
|
269
|
+
return _create_route_decorator(path, ["DELETE"])
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def PatchMapping(path: str) -> Callable:
|
|
273
|
+
"""
|
|
274
|
+
Map HTTP PATCH requests to a handler method
|
|
275
|
+
|
|
276
|
+
Similar to Spring Boot's @PatchMapping annotation.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
path: URL path for the route
|
|
280
|
+
|
|
281
|
+
Example:
|
|
282
|
+
@PatchMapping("/users/{id}")
|
|
283
|
+
def patch_user(self, id: int, updates: dict):
|
|
284
|
+
return patched_user
|
|
285
|
+
"""
|
|
286
|
+
return _create_route_decorator(path, ["PATCH"])
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def RequestMapping(path: str, methods: Optional[List[str]] = None) -> Callable:
|
|
290
|
+
"""
|
|
291
|
+
Map HTTP requests to a handler method
|
|
292
|
+
|
|
293
|
+
Similar to Spring Boot's @RequestMapping annotation.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
path: URL path for the route
|
|
297
|
+
methods: List of HTTP methods (defaults to GET)
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
@RequestMapping("/users", methods=["GET", "POST"])
|
|
301
|
+
def handle_users(self):
|
|
302
|
+
return users
|
|
303
|
+
"""
|
|
304
|
+
if methods is None:
|
|
305
|
+
methods = ["GET"]
|
|
306
|
+
return _create_route_decorator(path, methods)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation decorators
|
|
3
|
+
|
|
4
|
+
Provides validation support for controllers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Callable, Type
|
|
8
|
+
from functools import wraps
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def Validated(cls: Type) -> Type:
|
|
12
|
+
"""
|
|
13
|
+
Enable validation on a controller
|
|
14
|
+
|
|
15
|
+
Similar to Spring Boot's @Validated annotation.
|
|
16
|
+
When used, all Pydantic models in request handlers will be validated.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
@Controller("/api/users")
|
|
20
|
+
@Validated
|
|
21
|
+
class UserController:
|
|
22
|
+
@PostMapping("/")
|
|
23
|
+
def create_user(self, user: UserCreateRequest):
|
|
24
|
+
return user
|
|
25
|
+
|
|
26
|
+
Note: Validation is enabled by default in StarSpring, so this decorator
|
|
27
|
+
is mainly for documentation purposes and future extensibility.
|
|
28
|
+
"""
|
|
29
|
+
cls._is_validated = True # type: ignore
|
|
30
|
+
return cls
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Middleware components"""
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CORS middleware configuration
|
|
3
|
+
|
|
4
|
+
Provides easy CORS setup for the application.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
from starlette.middleware.cors import CORSMiddleware as StarletteCORSMiddleware
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CORSConfig:
|
|
12
|
+
"""
|
|
13
|
+
CORS configuration
|
|
14
|
+
|
|
15
|
+
Provides Spring Boot-style CORS configuration.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
allow_origins: List[str] = None,
|
|
21
|
+
allow_methods: List[str] = None,
|
|
22
|
+
allow_headers: List[str] = None,
|
|
23
|
+
allow_credentials: bool = False,
|
|
24
|
+
allow_origin_regex: Optional[str] = None,
|
|
25
|
+
expose_headers: List[str] = None,
|
|
26
|
+
max_age: int = 600
|
|
27
|
+
):
|
|
28
|
+
self.allow_origins = allow_origins or ["*"]
|
|
29
|
+
self.allow_methods = allow_methods or ["*"]
|
|
30
|
+
self.allow_headers = allow_headers or ["*"]
|
|
31
|
+
self.allow_credentials = allow_credentials
|
|
32
|
+
self.allow_origin_regex = allow_origin_regex
|
|
33
|
+
self.expose_headers = expose_headers or []
|
|
34
|
+
self.max_age = max_age
|
|
35
|
+
|
|
36
|
+
def to_middleware_kwargs(self) -> dict:
|
|
37
|
+
"""Convert to Starlette CORSMiddleware kwargs"""
|
|
38
|
+
return {
|
|
39
|
+
"allow_origins": self.allow_origins,
|
|
40
|
+
"allow_methods": self.allow_methods,
|
|
41
|
+
"allow_headers": self.allow_headers,
|
|
42
|
+
"allow_credentials": self.allow_credentials,
|
|
43
|
+
"allow_origin_regex": self.allow_origin_regex,
|
|
44
|
+
"expose_headers": self.expose_headers,
|
|
45
|
+
"max_age": self.max_age,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def create_cors_middleware(config: CORSConfig):
|
|
50
|
+
"""
|
|
51
|
+
Create CORS middleware from configuration
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
config: CORS configuration
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Configured CORS middleware
|
|
58
|
+
"""
|
|
59
|
+
def middleware(app):
|
|
60
|
+
return StarletteCORSMiddleware(app, **config.to_middleware_kwargs())
|
|
61
|
+
|
|
62
|
+
return middleware
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Convenience function for common CORS setup
|
|
66
|
+
def enable_cors(
|
|
67
|
+
allow_origins: List[str] = None,
|
|
68
|
+
allow_credentials: bool = False
|
|
69
|
+
) -> CORSConfig:
|
|
70
|
+
"""
|
|
71
|
+
Enable CORS with common settings
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
allow_origins: List of allowed origins (default: ["*"])
|
|
75
|
+
allow_credentials: Whether to allow credentials
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
CORS configuration
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
app = StarSpringApplication()
|
|
82
|
+
app.add_cors(enable_cors(
|
|
83
|
+
allow_origins=["http://localhost:3000"],
|
|
84
|
+
allow_credentials=True
|
|
85
|
+
))
|
|
86
|
+
"""
|
|
87
|
+
return CORSConfig(
|
|
88
|
+
allow_origins=allow_origins or ["*"],
|
|
89
|
+
allow_credentials=allow_credentials
|
|
90
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception handling middleware
|
|
3
|
+
|
|
4
|
+
Provides global exception handling for the application.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Callable
|
|
8
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
9
|
+
from starlette.requests import Request
|
|
10
|
+
from starlette.responses import JSONResponse
|
|
11
|
+
from pydantic import ValidationError
|
|
12
|
+
|
|
13
|
+
from starspring.core.exceptions import StarSpringException
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ExceptionHandlerMiddleware(BaseHTTPMiddleware):
|
|
17
|
+
"""
|
|
18
|
+
Global exception handler middleware
|
|
19
|
+
|
|
20
|
+
Catches all exceptions and converts them to proper HTTP responses.
|
|
21
|
+
Similar to Spring Boot's @ControllerAdvice.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, app, debug: bool = False):
|
|
25
|
+
super().__init__(app)
|
|
26
|
+
self.debug = debug
|
|
27
|
+
|
|
28
|
+
async def dispatch(self, request: Request, call_next: Callable):
|
|
29
|
+
try:
|
|
30
|
+
response = await call_next(request)
|
|
31
|
+
return response
|
|
32
|
+
except StarSpringException as e:
|
|
33
|
+
# Handle framework exceptions
|
|
34
|
+
return self._create_error_response(
|
|
35
|
+
status_code=e.status_code,
|
|
36
|
+
error=e.__class__.__name__,
|
|
37
|
+
message=e.message,
|
|
38
|
+
details=e.details
|
|
39
|
+
)
|
|
40
|
+
except ValidationError as e:
|
|
41
|
+
# Handle Pydantic validation errors
|
|
42
|
+
return self._create_error_response(
|
|
43
|
+
status_code=422,
|
|
44
|
+
error="ValidationError",
|
|
45
|
+
message="Validation failed",
|
|
46
|
+
details={"errors": e.errors()}
|
|
47
|
+
)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
# Handle unexpected exceptions
|
|
50
|
+
error_details = {"type": e.__class__.__name__}
|
|
51
|
+
if self.debug:
|
|
52
|
+
import traceback
|
|
53
|
+
error_details["traceback"] = traceback.format_exc()
|
|
54
|
+
|
|
55
|
+
return self._create_error_response(
|
|
56
|
+
status_code=500,
|
|
57
|
+
error="InternalServerError",
|
|
58
|
+
message=str(e) if self.debug else "An internal error occurred",
|
|
59
|
+
details=error_details if self.debug else None
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def _create_error_response(
|
|
63
|
+
self,
|
|
64
|
+
status_code: int,
|
|
65
|
+
error: str,
|
|
66
|
+
message: str,
|
|
67
|
+
details: dict = None
|
|
68
|
+
) -> JSONResponse:
|
|
69
|
+
"""Create a standardized error response"""
|
|
70
|
+
content = {
|
|
71
|
+
"success": False,
|
|
72
|
+
"error": error,
|
|
73
|
+
"message": message,
|
|
74
|
+
"status": status_code
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if details:
|
|
78
|
+
content["details"] = details
|
|
79
|
+
|
|
80
|
+
return JSONResponse(
|
|
81
|
+
status_code=status_code,
|
|
82
|
+
content=content
|
|
83
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging middleware
|
|
3
|
+
|
|
4
|
+
Provides request/response logging.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Callable
|
|
10
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
11
|
+
from starlette.requests import Request
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Configure logger
|
|
15
|
+
logger = logging.getLogger("starspring")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LoggingMiddleware(BaseHTTPMiddleware):
|
|
19
|
+
"""
|
|
20
|
+
Request/response logging middleware
|
|
21
|
+
|
|
22
|
+
Logs all incoming requests and their response times.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, app, log_level: int = logging.INFO):
|
|
26
|
+
super().__init__(app)
|
|
27
|
+
self.log_level = log_level
|
|
28
|
+
|
|
29
|
+
# Configure logger if not already configured
|
|
30
|
+
if not logger.handlers:
|
|
31
|
+
handler = logging.StreamHandler()
|
|
32
|
+
formatter = logging.Formatter(
|
|
33
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
34
|
+
)
|
|
35
|
+
handler.setFormatter(formatter)
|
|
36
|
+
logger.addHandler(handler)
|
|
37
|
+
logger.setLevel(log_level)
|
|
38
|
+
|
|
39
|
+
async def dispatch(self, request: Request, call_next: Callable):
|
|
40
|
+
# Log request
|
|
41
|
+
start_time = time.time()
|
|
42
|
+
|
|
43
|
+
logger.log(
|
|
44
|
+
self.log_level,
|
|
45
|
+
f"→ {request.method} {request.url.path}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Process request
|
|
49
|
+
response = await call_next(request)
|
|
50
|
+
|
|
51
|
+
# Log response
|
|
52
|
+
duration = (time.time() - start_time) * 1000 # Convert to ms
|
|
53
|
+
|
|
54
|
+
logger.log(
|
|
55
|
+
self.log_level,
|
|
56
|
+
f"← {request.method} {request.url.path} "
|
|
57
|
+
f"[{response.status_code}] {duration:.2f}ms"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return response
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Template package initialization
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from starspring.template.model_and_view import ModelAndView
|
|
6
|
+
from starspring.template.engine import (
|
|
7
|
+
TemplateEngine,
|
|
8
|
+
get_template_engine,
|
|
9
|
+
set_template_engine,
|
|
10
|
+
render_template
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'ModelAndView',
|
|
15
|
+
'TemplateEngine',
|
|
16
|
+
'get_template_engine',
|
|
17
|
+
'set_template_engine',
|
|
18
|
+
'render_template',
|
|
19
|
+
]
|