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.
@@ -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
+ ]