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,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