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,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
REST client utilities
|
|
3
|
+
|
|
4
|
+
Provides Spring Boot-style RestTemplate for HTTP requests.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Type, TypeVar, Dict, Any
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
T = TypeVar('T')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RestTemplate:
|
|
16
|
+
"""
|
|
17
|
+
REST client template
|
|
18
|
+
|
|
19
|
+
Similar to Spring Boot's RestTemplate.
|
|
20
|
+
Provides convenient methods for making HTTP requests with automatic
|
|
21
|
+
JSON serialization/deserialization and Pydantic model support.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
rest = RestTemplate(base_url="https://api.example.com")
|
|
25
|
+
user = rest.get("/users/1", response_model=User)
|
|
26
|
+
new_user = rest.post("/users", body=user_data, response_model=User)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
base_url: str = "",
|
|
32
|
+
timeout: float = 30.0,
|
|
33
|
+
headers: Optional[Dict[str, str]] = None
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
Initialize REST template
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
base_url: Base URL for all requests
|
|
40
|
+
timeout: Request timeout in seconds
|
|
41
|
+
headers: Default headers for all requests
|
|
42
|
+
"""
|
|
43
|
+
self.base_url = base_url.rstrip('/')
|
|
44
|
+
self.timeout = timeout
|
|
45
|
+
self.default_headers = headers or {}
|
|
46
|
+
|
|
47
|
+
def _build_url(self, path: str) -> str:
|
|
48
|
+
"""Build full URL from path"""
|
|
49
|
+
if path.startswith('http://') or path.startswith('https://'):
|
|
50
|
+
return path
|
|
51
|
+
return f"{self.base_url}{path}"
|
|
52
|
+
|
|
53
|
+
def _merge_headers(self, headers: Optional[Dict[str, str]]) -> Dict[str, str]:
|
|
54
|
+
"""Merge default headers with request headers"""
|
|
55
|
+
merged = self.default_headers.copy()
|
|
56
|
+
if headers:
|
|
57
|
+
merged.update(headers)
|
|
58
|
+
return merged
|
|
59
|
+
|
|
60
|
+
def _serialize_body(self, body: Any) -> Dict[str, Any]:
|
|
61
|
+
"""Serialize request body"""
|
|
62
|
+
if isinstance(body, BaseModel):
|
|
63
|
+
return body.model_dump()
|
|
64
|
+
elif hasattr(body, 'dict'):
|
|
65
|
+
return body.dict()
|
|
66
|
+
return body
|
|
67
|
+
|
|
68
|
+
def _deserialize_response(
|
|
69
|
+
self,
|
|
70
|
+
response: httpx.Response,
|
|
71
|
+
response_model: Optional[Type[T]] = None
|
|
72
|
+
) -> T:
|
|
73
|
+
"""Deserialize response"""
|
|
74
|
+
if response_model:
|
|
75
|
+
data = response.json()
|
|
76
|
+
if isinstance(data, list):
|
|
77
|
+
return [response_model(**item) for item in data] # type: ignore
|
|
78
|
+
return response_model(**data)
|
|
79
|
+
return response.json() # type: ignore
|
|
80
|
+
|
|
81
|
+
async def get(
|
|
82
|
+
self,
|
|
83
|
+
path: str,
|
|
84
|
+
params: Optional[Dict[str, Any]] = None,
|
|
85
|
+
headers: Optional[Dict[str, str]] = None,
|
|
86
|
+
response_model: Optional[Type[T]] = None
|
|
87
|
+
) -> T:
|
|
88
|
+
"""
|
|
89
|
+
Make a GET request
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
path: Request path
|
|
93
|
+
params: Query parameters
|
|
94
|
+
headers: Request headers
|
|
95
|
+
response_model: Pydantic model for response deserialization
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Response data
|
|
99
|
+
"""
|
|
100
|
+
url = self._build_url(path)
|
|
101
|
+
merged_headers = self._merge_headers(headers)
|
|
102
|
+
|
|
103
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
104
|
+
response = await client.get(url, params=params, headers=merged_headers)
|
|
105
|
+
response.raise_for_status()
|
|
106
|
+
return self._deserialize_response(response, response_model)
|
|
107
|
+
|
|
108
|
+
async def post(
|
|
109
|
+
self,
|
|
110
|
+
path: str,
|
|
111
|
+
body: Any = None,
|
|
112
|
+
headers: Optional[Dict[str, str]] = None,
|
|
113
|
+
response_model: Optional[Type[T]] = None
|
|
114
|
+
) -> T:
|
|
115
|
+
"""
|
|
116
|
+
Make a POST request
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
path: Request path
|
|
120
|
+
body: Request body
|
|
121
|
+
headers: Request headers
|
|
122
|
+
response_model: Pydantic model for response deserialization
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Response data
|
|
126
|
+
"""
|
|
127
|
+
url = self._build_url(path)
|
|
128
|
+
merged_headers = self._merge_headers(headers)
|
|
129
|
+
serialized_body = self._serialize_body(body) if body else None
|
|
130
|
+
|
|
131
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
132
|
+
response = await client.post(url, json=serialized_body, headers=merged_headers)
|
|
133
|
+
response.raise_for_status()
|
|
134
|
+
return self._deserialize_response(response, response_model)
|
|
135
|
+
|
|
136
|
+
async def put(
|
|
137
|
+
self,
|
|
138
|
+
path: str,
|
|
139
|
+
body: Any = None,
|
|
140
|
+
headers: Optional[Dict[str, str]] = None,
|
|
141
|
+
response_model: Optional[Type[T]] = None
|
|
142
|
+
) -> T:
|
|
143
|
+
"""
|
|
144
|
+
Make a PUT request
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
path: Request path
|
|
148
|
+
body: Request body
|
|
149
|
+
headers: Request headers
|
|
150
|
+
response_model: Pydantic model for response deserialization
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Response data
|
|
154
|
+
"""
|
|
155
|
+
url = self._build_url(path)
|
|
156
|
+
merged_headers = self._merge_headers(headers)
|
|
157
|
+
serialized_body = self._serialize_body(body) if body else None
|
|
158
|
+
|
|
159
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
160
|
+
response = await client.put(url, json=serialized_body, headers=merged_headers)
|
|
161
|
+
response.raise_for_status()
|
|
162
|
+
return self._deserialize_response(response, response_model)
|
|
163
|
+
|
|
164
|
+
async def delete(
|
|
165
|
+
self,
|
|
166
|
+
path: str,
|
|
167
|
+
headers: Optional[Dict[str, str]] = None,
|
|
168
|
+
response_model: Optional[Type[T]] = None
|
|
169
|
+
) -> Optional[T]:
|
|
170
|
+
"""
|
|
171
|
+
Make a DELETE request
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
path: Request path
|
|
175
|
+
headers: Request headers
|
|
176
|
+
response_model: Pydantic model for response deserialization
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Response data (if any)
|
|
180
|
+
"""
|
|
181
|
+
url = self._build_url(path)
|
|
182
|
+
merged_headers = self._merge_headers(headers)
|
|
183
|
+
|
|
184
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
185
|
+
response = await client.delete(url, headers=merged_headers)
|
|
186
|
+
response.raise_for_status()
|
|
187
|
+
|
|
188
|
+
# DELETE might not return content
|
|
189
|
+
if response.status_code == 204 or not response.content:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
return self._deserialize_response(response, response_model)
|
|
193
|
+
|
|
194
|
+
async def patch(
|
|
195
|
+
self,
|
|
196
|
+
path: str,
|
|
197
|
+
body: Any = None,
|
|
198
|
+
headers: Optional[Dict[str, str]] = None,
|
|
199
|
+
response_model: Optional[Type[T]] = None
|
|
200
|
+
) -> T:
|
|
201
|
+
"""
|
|
202
|
+
Make a PATCH request
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
path: Request path
|
|
206
|
+
body: Request body
|
|
207
|
+
headers: Request headers
|
|
208
|
+
response_model: Pydantic model for response deserialization
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Response data
|
|
212
|
+
"""
|
|
213
|
+
url = self._build_url(path)
|
|
214
|
+
merged_headers = self._merge_headers(headers)
|
|
215
|
+
serialized_body = self._serialize_body(body) if body else None
|
|
216
|
+
|
|
217
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
218
|
+
response = await client.patch(url, json=serialized_body, headers=merged_headers)
|
|
219
|
+
response.raise_for_status()
|
|
220
|
+
return self._deserialize_response(response, response_model)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Configuration management components"""
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Environment configuration
|
|
3
|
+
|
|
4
|
+
Provides environment-based configuration support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Profile(Enum):
|
|
13
|
+
"""Application profiles"""
|
|
14
|
+
DEVELOPMENT = "dev"
|
|
15
|
+
PRODUCTION = "prod"
|
|
16
|
+
TEST = "test"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Environment:
|
|
20
|
+
"""
|
|
21
|
+
Environment configuration manager
|
|
22
|
+
|
|
23
|
+
Manages application profiles and environment-specific settings.
|
|
24
|
+
Similar to Spring Boot's @Profile and Environment.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, active_profile: Optional[Profile] = None):
|
|
28
|
+
self._active_profile = active_profile or self._detect_profile()
|
|
29
|
+
|
|
30
|
+
def _detect_profile(self) -> Profile:
|
|
31
|
+
"""Detect active profile from environment"""
|
|
32
|
+
profile_str = os.environ.get('STARSPRING_PROFILE', 'dev').lower()
|
|
33
|
+
|
|
34
|
+
for profile in Profile:
|
|
35
|
+
if profile.value == profile_str:
|
|
36
|
+
return profile
|
|
37
|
+
|
|
38
|
+
return Profile.DEVELOPMENT
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def active_profile(self) -> Profile:
|
|
42
|
+
"""Get the active profile"""
|
|
43
|
+
return self._active_profile
|
|
44
|
+
|
|
45
|
+
def is_development(self) -> bool:
|
|
46
|
+
"""Check if running in development mode"""
|
|
47
|
+
return self._active_profile == Profile.DEVELOPMENT
|
|
48
|
+
|
|
49
|
+
def is_production(self) -> bool:
|
|
50
|
+
"""Check if running in production mode"""
|
|
51
|
+
return self._active_profile == Profile.PRODUCTION
|
|
52
|
+
|
|
53
|
+
def is_test(self) -> bool:
|
|
54
|
+
"""Check if running in test mode"""
|
|
55
|
+
return self._active_profile == Profile.TEST
|
|
56
|
+
|
|
57
|
+
def get_env(self, key: str, default: Optional[str] = None) -> Optional[str]:
|
|
58
|
+
"""Get environment variable"""
|
|
59
|
+
return os.environ.get(key, default)
|
|
60
|
+
|
|
61
|
+
def set_profile(self, profile: Profile) -> None:
|
|
62
|
+
"""Set the active profile"""
|
|
63
|
+
self._active_profile = profile
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Global environment instance
|
|
67
|
+
_environment: Optional[Environment] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_environment() -> Environment:
|
|
71
|
+
"""Get the global environment instance"""
|
|
72
|
+
global _environment
|
|
73
|
+
if _environment is None:
|
|
74
|
+
_environment = Environment()
|
|
75
|
+
return _environment
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def set_environment(env: Environment) -> None:
|
|
79
|
+
"""Set the global environment instance"""
|
|
80
|
+
global _environment
|
|
81
|
+
_environment = env
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Application properties management
|
|
3
|
+
|
|
4
|
+
Provides configuration loading from YAML and properties files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import yaml
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApplicationProperties:
|
|
14
|
+
"""
|
|
15
|
+
Application properties manager
|
|
16
|
+
|
|
17
|
+
Loads configuration from application.yaml or application.properties files.
|
|
18
|
+
Similar to Spring Boot's application properties.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, config_path: Optional[str] = None):
|
|
22
|
+
self._properties: Dict[str, Any] = {}
|
|
23
|
+
self._config_path = config_path
|
|
24
|
+
|
|
25
|
+
if config_path:
|
|
26
|
+
self.load(config_path)
|
|
27
|
+
|
|
28
|
+
def load(self, config_path: str) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Load configuration from a file
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
config_path: Path to configuration file (YAML or properties)
|
|
34
|
+
"""
|
|
35
|
+
path = Path(config_path)
|
|
36
|
+
|
|
37
|
+
if not path.exists():
|
|
38
|
+
raise FileNotFoundError(f"Configuration file not found: {config_path}")
|
|
39
|
+
|
|
40
|
+
if path.suffix in ['.yaml', '.yml']:
|
|
41
|
+
self._load_yaml(path)
|
|
42
|
+
elif path.suffix == '.properties':
|
|
43
|
+
self._load_properties(path)
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError(f"Unsupported configuration file format: {path.suffix}")
|
|
46
|
+
|
|
47
|
+
def _load_yaml(self, path: Path) -> None:
|
|
48
|
+
"""Load YAML configuration"""
|
|
49
|
+
with open(path, 'r') as f:
|
|
50
|
+
data = yaml.safe_load(f)
|
|
51
|
+
if data:
|
|
52
|
+
self._properties = self._flatten_dict(data)
|
|
53
|
+
|
|
54
|
+
def _load_properties(self, path: Path) -> None:
|
|
55
|
+
"""Load properties file"""
|
|
56
|
+
with open(path, 'r') as f:
|
|
57
|
+
for line in f:
|
|
58
|
+
line = line.strip()
|
|
59
|
+
if line and not line.startswith('#'):
|
|
60
|
+
if '=' in line:
|
|
61
|
+
key, value = line.split('=', 1)
|
|
62
|
+
self._properties[key.strip()] = value.strip()
|
|
63
|
+
|
|
64
|
+
def _flatten_dict(self, d: Dict, parent_key: str = '', sep: str = '.') -> Dict:
|
|
65
|
+
"""Flatten nested dictionary with dot notation"""
|
|
66
|
+
items = []
|
|
67
|
+
for k, v in d.items():
|
|
68
|
+
new_key = f"{parent_key}{sep}{k}" if parent_key else k
|
|
69
|
+
if isinstance(v, dict):
|
|
70
|
+
items.extend(self._flatten_dict(v, new_key, sep=sep).items())
|
|
71
|
+
else:
|
|
72
|
+
items.append((new_key, v))
|
|
73
|
+
return dict(items)
|
|
74
|
+
|
|
75
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
76
|
+
"""
|
|
77
|
+
Get a property value
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
key: Property key (supports dot notation)
|
|
81
|
+
default: Default value if key not found
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Property value or default
|
|
85
|
+
"""
|
|
86
|
+
# Check environment variable override
|
|
87
|
+
env_key = key.upper().replace('.', '_')
|
|
88
|
+
env_value = os.environ.get(env_key)
|
|
89
|
+
if env_value is not None:
|
|
90
|
+
return env_value
|
|
91
|
+
|
|
92
|
+
return self._properties.get(key, default)
|
|
93
|
+
|
|
94
|
+
def get_int(self, key: str, default: int = 0) -> int:
|
|
95
|
+
"""Get property as integer"""
|
|
96
|
+
value = self.get(key, default)
|
|
97
|
+
return int(value) if value is not None else default
|
|
98
|
+
|
|
99
|
+
def get_bool(self, key: str, default: bool = False) -> bool:
|
|
100
|
+
"""Get property as boolean"""
|
|
101
|
+
value = self.get(key, default)
|
|
102
|
+
if isinstance(value, bool):
|
|
103
|
+
return value
|
|
104
|
+
if isinstance(value, str):
|
|
105
|
+
return value.lower() in ('true', 'yes', '1', 'on')
|
|
106
|
+
return bool(value)
|
|
107
|
+
|
|
108
|
+
def get_list(self, key: str, default: list = None) -> list:
|
|
109
|
+
"""Get property as list"""
|
|
110
|
+
value = self.get(key, default or [])
|
|
111
|
+
if isinstance(value, list):
|
|
112
|
+
return value
|
|
113
|
+
if isinstance(value, str):
|
|
114
|
+
return [item.strip() for item in value.split(',')]
|
|
115
|
+
return default or []
|
|
116
|
+
|
|
117
|
+
def set(self, key: str, value: Any) -> None:
|
|
118
|
+
"""Set a property value"""
|
|
119
|
+
self._properties[key] = value
|
|
120
|
+
|
|
121
|
+
def get_all(self) -> Dict[str, Any]:
|
|
122
|
+
"""Get all properties"""
|
|
123
|
+
return self._properties.copy()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Global properties instance
|
|
127
|
+
_app_properties: Optional[ApplicationProperties] = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_properties() -> ApplicationProperties:
|
|
131
|
+
"""Get the global application properties instance"""
|
|
132
|
+
global _app_properties
|
|
133
|
+
if _app_properties is None:
|
|
134
|
+
_app_properties = ApplicationProperties()
|
|
135
|
+
return _app_properties
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def set_properties(properties: ApplicationProperties) -> None:
|
|
139
|
+
"""Set the global application properties instance"""
|
|
140
|
+
global _app_properties
|
|
141
|
+
_app_properties = properties
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_property(key: str, default: Any = None) -> Any:
|
|
145
|
+
"""Convenience function to get a property"""
|
|
146
|
+
return get_properties().get(key, default)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core framework components"""
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Application context and dependency injection container
|
|
3
|
+
|
|
4
|
+
Provides Spring Boot-style dependency injection with automatic component scanning
|
|
5
|
+
and dependency resolution.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Any, Type, TypeVar, Optional, Callable, get_type_hints
|
|
9
|
+
import inspect
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
T = TypeVar('T')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BeanScope(Enum):
|
|
17
|
+
"""Bean scope types"""
|
|
18
|
+
SINGLETON = "singleton"
|
|
19
|
+
PROTOTYPE = "prototype"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BeanDefinition:
|
|
23
|
+
"""Metadata for a registered bean"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
bean_class: Type,
|
|
28
|
+
scope: BeanScope = BeanScope.SINGLETON,
|
|
29
|
+
factory: Optional[Callable] = None,
|
|
30
|
+
name: Optional[str] = None
|
|
31
|
+
):
|
|
32
|
+
self.bean_class = bean_class
|
|
33
|
+
self.scope = scope
|
|
34
|
+
self.factory = factory
|
|
35
|
+
self.name = name or bean_class.__name__
|
|
36
|
+
self.instance: Optional[Any] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ApplicationContext:
|
|
40
|
+
"""
|
|
41
|
+
Application context for managing beans and dependency injection
|
|
42
|
+
|
|
43
|
+
Similar to Spring's ApplicationContext, this class manages the lifecycle
|
|
44
|
+
of components and handles dependency injection.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self):
|
|
48
|
+
self._beans: Dict[str, BeanDefinition] = {}
|
|
49
|
+
self._instances: Dict[str, Any] = {}
|
|
50
|
+
self._type_to_name: Dict[Type, str] = {}
|
|
51
|
+
|
|
52
|
+
def register_bean(
|
|
53
|
+
self,
|
|
54
|
+
bean_class: Type[T],
|
|
55
|
+
scope: BeanScope = BeanScope.SINGLETON,
|
|
56
|
+
factory: Optional[Callable] = None,
|
|
57
|
+
name: Optional[str] = None
|
|
58
|
+
) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Register a bean in the application context
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
bean_class: The class to register
|
|
64
|
+
scope: Bean scope (singleton or prototype)
|
|
65
|
+
factory: Optional factory function to create the bean
|
|
66
|
+
name: Optional custom name for the bean
|
|
67
|
+
"""
|
|
68
|
+
bean_name = name or bean_class.__name__
|
|
69
|
+
bean_def = BeanDefinition(bean_class, scope, factory, bean_name)
|
|
70
|
+
self._beans[bean_name] = bean_def
|
|
71
|
+
self._type_to_name[bean_class] = bean_name
|
|
72
|
+
|
|
73
|
+
def get_bean(self, bean_class: Type[T], name: Optional[str] = None) -> T:
|
|
74
|
+
"""
|
|
75
|
+
Get a bean instance from the context
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
bean_class: The class type to retrieve
|
|
79
|
+
name: Optional bean name
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Instance of the requested bean
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
ValueError: If bean is not registered
|
|
86
|
+
"""
|
|
87
|
+
bean_name = name or self._type_to_name.get(bean_class)
|
|
88
|
+
|
|
89
|
+
if not bean_name or bean_name not in self._beans:
|
|
90
|
+
raise ValueError(f"No bean registered for {bean_class.__name__}")
|
|
91
|
+
|
|
92
|
+
bean_def = self._beans[bean_name]
|
|
93
|
+
|
|
94
|
+
# For singleton scope, return cached instance
|
|
95
|
+
if bean_def.scope == BeanScope.SINGLETON:
|
|
96
|
+
if bean_def.instance is None:
|
|
97
|
+
bean_def.instance = self._create_bean(bean_def)
|
|
98
|
+
return bean_def.instance
|
|
99
|
+
|
|
100
|
+
# For prototype scope, create new instance each time
|
|
101
|
+
return self._create_bean(bean_def)
|
|
102
|
+
|
|
103
|
+
def _create_bean(self, bean_def: BeanDefinition) -> Any:
|
|
104
|
+
"""
|
|
105
|
+
Create a bean instance with dependency injection
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
bean_def: Bean definition
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
New instance of the bean with dependencies injected
|
|
112
|
+
"""
|
|
113
|
+
# Use factory if provided
|
|
114
|
+
if bean_def.factory:
|
|
115
|
+
return bean_def.factory()
|
|
116
|
+
|
|
117
|
+
# Get constructor parameters
|
|
118
|
+
sig = inspect.signature(bean_def.bean_class.__init__)
|
|
119
|
+
params = {}
|
|
120
|
+
|
|
121
|
+
# Get type hints for constructor
|
|
122
|
+
try:
|
|
123
|
+
type_hints = get_type_hints(bean_def.bean_class.__init__)
|
|
124
|
+
except Exception:
|
|
125
|
+
type_hints = {}
|
|
126
|
+
|
|
127
|
+
# Inject dependencies
|
|
128
|
+
for param_name, param in sig.parameters.items():
|
|
129
|
+
if param_name == 'self':
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# Get the type annotation
|
|
133
|
+
param_type = type_hints.get(param_name)
|
|
134
|
+
|
|
135
|
+
if param_type and param_type in self._type_to_name:
|
|
136
|
+
# Recursively get the dependency
|
|
137
|
+
params[param_name] = self.get_bean(param_type)
|
|
138
|
+
elif param.default != inspect.Parameter.empty:
|
|
139
|
+
# Use default value if available
|
|
140
|
+
params[param_name] = param.default
|
|
141
|
+
|
|
142
|
+
# Create instance with injected dependencies
|
|
143
|
+
return bean_def.bean_class(**params)
|
|
144
|
+
|
|
145
|
+
def has_bean(self, bean_class: Type, name: Optional[str] = None) -> bool:
|
|
146
|
+
"""Check if a bean is registered"""
|
|
147
|
+
bean_name = name or self._type_to_name.get(bean_class)
|
|
148
|
+
return bean_name is not None and bean_name in self._beans
|
|
149
|
+
|
|
150
|
+
def get_all_beans(self) -> Dict[str, Any]:
|
|
151
|
+
"""Get all singleton bean instances"""
|
|
152
|
+
result = {}
|
|
153
|
+
for name, bean_def in self._beans.items():
|
|
154
|
+
if bean_def.scope == BeanScope.SINGLETON:
|
|
155
|
+
result[name] = self.get_bean(bean_def.bean_class, name)
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
def clear(self) -> None:
|
|
159
|
+
"""Clear all beans from the context"""
|
|
160
|
+
self._beans.clear()
|
|
161
|
+
self._instances.clear()
|
|
162
|
+
self._type_to_name.clear()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Global application context instance
|
|
166
|
+
_app_context: Optional[ApplicationContext] = None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_application_context() -> ApplicationContext:
|
|
170
|
+
"""Get the global application context"""
|
|
171
|
+
global _app_context
|
|
172
|
+
if _app_context is None:
|
|
173
|
+
_app_context = ApplicationContext()
|
|
174
|
+
return _app_context
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def set_application_context(context: ApplicationContext) -> None:
|
|
178
|
+
"""Set the global application context"""
|
|
179
|
+
global _app_context
|
|
180
|
+
_app_context = context
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base controller class
|
|
3
|
+
|
|
4
|
+
Provides common functionality for all controllers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from starlette.requests import Request
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseController:
|
|
12
|
+
"""
|
|
13
|
+
Base class for all controllers
|
|
14
|
+
|
|
15
|
+
Provides access to common utilities and request context.
|
|
16
|
+
Similar to Spring Boot's base controller pattern.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self._request: Optional[Request] = None
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def request(self) -> Optional[Request]:
|
|
24
|
+
"""Get the current request object"""
|
|
25
|
+
return self._request
|
|
26
|
+
|
|
27
|
+
def set_request(self, request: Request) -> None:
|
|
28
|
+
"""Set the current request object (called by framework)"""
|
|
29
|
+
self._request = request
|
|
30
|
+
|
|
31
|
+
def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]:
|
|
32
|
+
"""Get a request header value"""
|
|
33
|
+
if self._request:
|
|
34
|
+
return self._request.headers.get(name, default)
|
|
35
|
+
return default
|
|
36
|
+
|
|
37
|
+
def get_query_param(self, name: str, default: Optional[str] = None) -> Optional[str]:
|
|
38
|
+
"""Get a query parameter value"""
|
|
39
|
+
if self._request:
|
|
40
|
+
return self._request.query_params.get(name, default)
|
|
41
|
+
return default
|
|
42
|
+
|
|
43
|
+
def get_path_param(self, name: str) -> Optional[str]:
|
|
44
|
+
"""Get a path parameter value"""
|
|
45
|
+
if self._request:
|
|
46
|
+
return self._request.path_params.get(name)
|
|
47
|
+
return None
|