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 ADDED
@@ -0,0 +1,150 @@
1
+ """
2
+ StarSpring - Spring Boot-inspired Python Web Framework
3
+
4
+ A Python web framework built on Starlette that provides Spring Boot-inspired
5
+ syntax and patterns, including dependency injection, decorator-based routing,
6
+ repository patterns, ORM, template engine, and comprehensive middleware support.
7
+ """
8
+
9
+ from starspring.application import StarSpringApplication, create_application
10
+
11
+ # Core components
12
+ from starspring.core.context import ApplicationContext, BeanScope
13
+ from starspring.core.controller import BaseController
14
+ from starspring.core.response import ResponseEntity, ApiResponse
15
+ from starspring.core.exceptions import (
16
+ StarSpringException,
17
+ NotFoundException,
18
+ BadRequestException,
19
+ UnauthorizedException,
20
+ ForbiddenException,
21
+ ValidationException,
22
+ InternalServerException
23
+ )
24
+
25
+ # Decorators
26
+ from starspring.decorators.components import (
27
+ Controller,
28
+ TemplateController,
29
+ Service,
30
+ Component,
31
+ Repository,
32
+ Autowired
33
+ )
34
+ from starspring.decorators.routing import (
35
+ GetMapping,
36
+ PostMapping,
37
+ PutMapping,
38
+ DeleteMapping,
39
+ PatchMapping,
40
+ RequestMapping
41
+ )
42
+ from starspring.decorators.configuration import (
43
+ Configuration,
44
+ Bean,
45
+ Value
46
+ )
47
+
48
+ # Data layer
49
+ from starspring.data.repository import Repository as RepositoryBase, CrudRepository, StarRepository
50
+ from starspring.data.entity import (
51
+ Entity,
52
+ Column,
53
+ Id,
54
+ GeneratedValue,
55
+ ManyToOne,
56
+ OneToMany,
57
+ ManyToMany,
58
+ BaseEntity,
59
+ GenerationType
60
+ )
61
+ from starspring.data.transaction import Transactional
62
+
63
+ # Template engine
64
+ from starspring.template import ModelAndView, TemplateEngine, render_template
65
+
66
+ # Middleware
67
+ from starspring.middleware.cors import enable_cors, CORSConfig
68
+
69
+ # Configuration
70
+ from starspring.config.properties import ApplicationProperties
71
+ from starspring.config.environment import Environment
72
+
73
+ # Client
74
+ from starspring.client.rest_client import RestTemplate
75
+
76
+ __version__ = "0.1.0"
77
+
78
+ __all__ = [
79
+ # Application
80
+ 'StarSpringApplication',
81
+ 'create_application',
82
+
83
+ # Core
84
+ 'ApplicationContext',
85
+ 'BeanScope',
86
+ 'BaseController',
87
+ 'ResponseEntity',
88
+ 'ApiResponse',
89
+
90
+ # Exceptions
91
+ 'StarSpringException',
92
+ 'NotFoundException',
93
+ 'BadRequestException',
94
+ 'UnauthorizedException',
95
+ 'ForbiddenException',
96
+ 'ValidationException',
97
+ 'InternalServerException',
98
+
99
+ # Component decorators
100
+ 'Controller',
101
+ 'TemplateController',
102
+ 'Service',
103
+ 'Component',
104
+ 'Repository',
105
+ 'Autowired',
106
+
107
+ # Routing decorators
108
+ 'GetMapping',
109
+ 'PostMapping',
110
+ 'PutMapping',
111
+ 'DeleteMapping',
112
+ 'PatchMapping',
113
+ 'RequestMapping',
114
+
115
+ # Configuration decorators
116
+ 'Configuration',
117
+ 'Bean',
118
+ 'Value',
119
+
120
+ # Data layer
121
+ 'RepositoryBase',
122
+ 'CrudRepository',
123
+ 'StarRepository',
124
+ 'Entity',
125
+ 'Column',
126
+ 'Id',
127
+ 'GeneratedValue',
128
+ 'ManyToOne',
129
+ 'OneToMany',
130
+ 'ManyToMany',
131
+ 'BaseEntity',
132
+ 'GenerationType',
133
+ 'Transactional',
134
+
135
+ # Template engine
136
+ 'ModelAndView',
137
+ 'TemplateEngine',
138
+ 'render_template',
139
+
140
+ # Middleware
141
+ 'enable_cors',
142
+ 'CORSConfig',
143
+
144
+ # Configuration
145
+ 'ApplicationProperties',
146
+ 'Environment',
147
+
148
+ # Client
149
+ 'RestTemplate',
150
+ ]
@@ -0,0 +1,421 @@
1
+ """
2
+ StarSpring Application
3
+
4
+ Main application class that bootstraps the framework.
5
+ """
6
+
7
+ import inspect
8
+ import logging
9
+ from typing import List, Type, Optional, Callable
10
+ from pathlib import Path
11
+
12
+ from starlette.applications import Starlette
13
+ from starlette.routing import Route, Mount
14
+ from starlette.staticfiles import StaticFiles
15
+ from starlette.middleware import Middleware
16
+
17
+ from starspring.core.context import ApplicationContext, get_application_context, set_application_context
18
+ from starspring.config.properties import ApplicationProperties, set_properties
19
+ from starspring.config.environment import Environment, set_environment
20
+ from starspring.middleware.exception import ExceptionHandlerMiddleware
21
+ from starspring.middleware.cors import CORSConfig, create_cors_middleware
22
+ from starspring.middleware.logging import LoggingMiddleware
23
+ from starspring.decorators.routing import create_route_handler
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+
29
+ class StarSpringApplication:
30
+ """
31
+ Main application class
32
+
33
+ Bootstraps the StarSpring framework and creates a Starlette ASGI application.
34
+ Similar to Spring Boot's SpringApplication.
35
+
36
+ Example:
37
+ app = StarSpringApplication()
38
+ app.scan_components("myapp.controllers")
39
+ app.add_cors(enable_cors())
40
+
41
+ if __name__ == "__main__":
42
+ app.run()
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ config_path: Optional[str] = None,
48
+ debug: bool = None,
49
+ title: str = "StarSpring Application",
50
+ version: str = "1.0.0"
51
+ ):
52
+ """
53
+ Initialize the application
54
+
55
+ Args:
56
+ config_path: Path to configuration file
57
+ debug: Debug mode (auto-detected from environment if None)
58
+ title: Application title
59
+ version: Application version
60
+ """
61
+ # Initialize environment
62
+ self.environment = Environment()
63
+ set_environment(self.environment)
64
+
65
+ # Set debug mode
66
+ if debug is None:
67
+ debug = self.environment.is_development()
68
+ self.debug = debug
69
+
70
+ # Initialize application context (reuse existing if present)
71
+ existing_context = None
72
+ try:
73
+ existing_context = get_application_context()
74
+ except:
75
+ pass
76
+
77
+ if existing_context:
78
+ self.context = existing_context
79
+ else:
80
+ self.context = ApplicationContext()
81
+ set_application_context(self.context)
82
+
83
+ # Initialize properties
84
+ self.properties = ApplicationProperties()
85
+ if config_path:
86
+ self.properties.load(config_path)
87
+ set_properties(self.properties)
88
+
89
+ # Initialize database if configured
90
+ self._init_database()
91
+
92
+ # Application metadata
93
+ self.title = title
94
+ self.version = version
95
+
96
+ # Middleware stack
97
+ self._middleware: List[Middleware] = []
98
+
99
+ # Routes
100
+ self._routes: List[Route] = []
101
+
102
+ # Static files
103
+ self._static_mounts: List[Mount] = []
104
+
105
+ # Lifecycle hooks
106
+ self._startup_hooks: List[Callable] = []
107
+ self._shutdown_hooks: List[Callable] = []
108
+
109
+ # Add default middleware
110
+ self._add_default_middleware()
111
+
112
+ # Starlette app (created on demand)
113
+ self._app: Optional[Starlette] = None
114
+
115
+ def _add_default_middleware(self):
116
+ """Add default middleware"""
117
+ # Exception handler (should be first)
118
+ self._middleware.append(
119
+ Middleware(ExceptionHandlerMiddleware, debug=self.debug)
120
+ )
121
+
122
+ # Logging middleware
123
+ self._middleware.append(
124
+ Middleware(LoggingMiddleware)
125
+ )
126
+
127
+ def _init_database(self):
128
+ """Initialize database connection if configured"""
129
+ try:
130
+ # Check if database URL is configured
131
+ db_url = self.properties.get("database.url")
132
+ if not db_url:
133
+ logger.info("No database configured (database.url not found)")
134
+ return
135
+
136
+ # Import database modules
137
+ from starspring.data.database_config import DatabaseConfig
138
+ from starspring.data.orm_gateway import SQLAlchemyGateway, set_orm_gateway
139
+
140
+ # Initialize database configuration
141
+ db_config = DatabaseConfig()
142
+
143
+ # Create and set ORM gateway
144
+ gateway = SQLAlchemyGateway(db_config.SessionFactory)
145
+ set_orm_gateway(gateway)
146
+
147
+ logger.info("Database initialized successfully")
148
+
149
+ except ImportError as e:
150
+ logger.warning(f"Database dependencies not installed: {e}")
151
+ logger.warning("Install with: pip install sqlalchemy")
152
+ except Exception as e:
153
+ logger.error(f"Failed to initialize database: {e}")
154
+ if self.debug:
155
+ raise
156
+
157
+ def _auto_create_tables_if_enabled(self):
158
+ """
159
+ Automatically create database tables from entity metadata
160
+
161
+ Similar to Spring Boot's ddl-auto feature.
162
+ Called after component scanning when entity classes are available.
163
+ """
164
+ try:
165
+ from starspring.data.orm_gateway import get_orm_gateway
166
+ from starspring.data.entity import mapper_registry
167
+
168
+ # Check if ddl-auto is enabled
169
+ ddl_auto = self.properties.get("database.ddl-auto", "create-if-not-exists")
170
+ if ddl_auto not in ["create", "create-if-not-exists", "update"]:
171
+ return
172
+
173
+ # Get ORM gateway
174
+ try:
175
+ gateway = get_orm_gateway()
176
+ except RuntimeError:
177
+ # No database configured
178
+ return
179
+
180
+ # Use SQLAlchemy's metadata.create_all to create tables
181
+ # This uses the imperative mappings we set up in the @Entity decorator
182
+ # We get the engine from the gateway's session
183
+ engine = gateway.session.get_bind()
184
+ mapper_registry.metadata.create_all(engine)
185
+
186
+ logger.info("Auto-created tables using SQLAlchemy metadata")
187
+
188
+ except Exception as e:
189
+ logger.error(f"Failed to auto-create tables: {e}")
190
+ if self.debug:
191
+ raise
192
+
193
+ def add_cors(self, config: CORSConfig) -> 'StarSpringApplication':
194
+ """
195
+ Add CORS middleware
196
+
197
+ Args:
198
+ config: CORS configuration
199
+
200
+ Returns:
201
+ Self for chaining
202
+ """
203
+ # CORS should be added early in the middleware stack
204
+ from starlette.middleware.cors import CORSMiddleware
205
+ self._middleware.insert(1, Middleware(
206
+ CORSMiddleware,
207
+ **config.to_middleware_kwargs()
208
+ ))
209
+ return self
210
+
211
+ def add_middleware(self, middleware_class, **options) -> 'StarSpringApplication':
212
+ """
213
+ Add custom middleware
214
+
215
+ Args:
216
+ middleware_class: Middleware class
217
+ **options: Middleware options
218
+
219
+ Returns:
220
+ Self for chaining
221
+ """
222
+ self._middleware.append(Middleware(middleware_class, **options))
223
+ return self
224
+
225
+ def scan_components(self, *module_paths: str) -> 'StarSpringApplication':
226
+ """
227
+ Scan and register components from modules
228
+
229
+ Args:
230
+ *module_paths: Module paths to scan (e.g., "myapp.controllers")
231
+
232
+ Returns:
233
+ Self for chaining
234
+ """
235
+ import importlib
236
+ import pkgutil
237
+
238
+ # Initialize scanned modules list if not exists
239
+ if not hasattr(self, '_scanned_modules'):
240
+ self._scanned_modules = []
241
+
242
+ for module_path in module_paths:
243
+ try:
244
+ # Import the module
245
+ module = importlib.import_module(module_path)
246
+ self._scanned_modules.append(module) # Track module
247
+ print(f"DEBUG: Scanning module {module_path}")
248
+ print(f"DEBUG: Current beans: {list(self.context._beans.keys())}")
249
+
250
+ # Scan for controllers and register routes
251
+ for name, obj in inspect.getmembers(module, inspect.isclass):
252
+ if hasattr(obj, '_is_controller'):
253
+ print(f"DEBUG: Found controller: {name}")
254
+ self._register_controller(obj)
255
+
256
+ # If it's a package, scan submodules
257
+ if hasattr(module, '__path__'):
258
+ for importer, modname, ispkg in pkgutil.walk_packages(
259
+ path=module.__path__,
260
+ prefix=module.__name__ + '.',
261
+ ):
262
+ submodule = importlib.import_module(modname)
263
+ self._scanned_modules.append(submodule) # Track submodule
264
+ for name, obj in inspect.getmembers(submodule, inspect.isclass):
265
+ if hasattr(obj, '_is_controller'):
266
+ self._register_controller(obj)
267
+
268
+ except ImportError as e:
269
+ print(f"Warning: Could not import module {module_path}: {e}")
270
+
271
+ # Auto-create database tables from entities
272
+ self._auto_create_tables_if_enabled()
273
+
274
+ return self
275
+
276
+ def _register_controller(self, controller_class: Type):
277
+ """Register a controller and its routes"""
278
+ # Get controller instance from context
279
+ controller = self.context.get_bean(controller_class)
280
+
281
+ # Get controller prefix
282
+ prefix = getattr(controller_class, '_controller_prefix', '')
283
+
284
+ # Scan for route methods
285
+ for name, method in inspect.getmembers(controller_class, inspect.isfunction):
286
+ if hasattr(method, '_route_path'):
287
+ path = method._route_path
288
+ methods = method._route_methods
289
+
290
+ # Combine prefix and path
291
+ full_path = f"{prefix}{path}".replace('//', '/')
292
+
293
+ # Get the bound method and wrap it with request handling
294
+ bound_method = getattr(controller, name)
295
+ handler = create_route_handler(bound_method)
296
+
297
+ # Create route
298
+ route = Route(
299
+ full_path,
300
+ endpoint=handler,
301
+ methods=methods
302
+ )
303
+ self._routes.append(route)
304
+ print(f"DEBUG: Registered route: {full_path} -> {methods}")
305
+
306
+ def add_static_files(
307
+ self,
308
+ path: str,
309
+ directory: str,
310
+ name: str = "static"
311
+ ) -> 'StarSpringApplication':
312
+ """
313
+ Add static file serving
314
+
315
+ Args:
316
+ path: URL path prefix
317
+ directory: Directory to serve files from
318
+ name: Mount name
319
+
320
+ Returns:
321
+ Self for chaining
322
+ """
323
+ self._static_mounts.append(
324
+ Mount(path, StaticFiles(directory=directory), name=name)
325
+ )
326
+ return self
327
+
328
+ def on_startup(self, func: Callable) -> 'StarSpringApplication':
329
+ """
330
+ Register a startup hook
331
+
332
+ Args:
333
+ func: Function to call on startup
334
+
335
+ Returns:
336
+ Self for chaining
337
+ """
338
+ self._startup_hooks.append(func)
339
+ return self
340
+
341
+ def on_shutdown(self, func: Callable) -> 'StarSpringApplication':
342
+ """
343
+ Register a shutdown hook
344
+
345
+ Args:
346
+ func: Function to call on shutdown
347
+
348
+ Returns:
349
+ Self for chaining
350
+ """
351
+ self._shutdown_hooks.append(func)
352
+ return self
353
+
354
+ def build(self) -> Starlette:
355
+ """
356
+ Build the Starlette application
357
+
358
+ Returns:
359
+ Configured Starlette application
360
+ """
361
+ if self._app is None:
362
+ # Combine routes and static mounts
363
+ all_routes = self._routes + self._static_mounts
364
+
365
+ # Create Starlette app
366
+ self._app = Starlette(
367
+ debug=self.debug,
368
+ routes=all_routes,
369
+ middleware=self._middleware,
370
+ on_startup=self._startup_hooks,
371
+ on_shutdown=self._shutdown_hooks,
372
+ )
373
+
374
+ return self._app
375
+
376
+ def run(
377
+ self,
378
+ host: str = "0.0.0.0",
379
+ port: int = 8000,
380
+ **uvicorn_options
381
+ ):
382
+ """
383
+ Run the application with Uvicorn
384
+
385
+ Args:
386
+ host: Host to bind to
387
+ port: Port to bind to
388
+ **uvicorn_options: Additional Uvicorn options
389
+ """
390
+ import uvicorn
391
+
392
+ app = self.build()
393
+
394
+ uvicorn.run(
395
+ app,
396
+ host=host,
397
+ port=port,
398
+ **uvicorn_options
399
+ )
400
+
401
+ @property
402
+ def app(self) -> Starlette:
403
+ """Get the Starlette application"""
404
+ return self.build()
405
+
406
+
407
+ def create_application(
408
+ config_path: Optional[str] = None,
409
+ **kwargs
410
+ ) -> StarSpringApplication:
411
+ """
412
+ Factory function to create a StarSpring application
413
+
414
+ Args:
415
+ config_path: Path to configuration file
416
+ **kwargs: Additional application options
417
+
418
+ Returns:
419
+ StarSpring application instance
420
+ """
421
+ return StarSpringApplication(config_path=config_path, **kwargs)
@@ -0,0 +1 @@
1
+ """API client components"""