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,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Schema generator for automatic table creation
|
|
3
|
+
|
|
4
|
+
Generates database schema from entity metadata, similar to Spring Boot's ddl-auto feature.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Type, List
|
|
8
|
+
from sqlalchemy import text
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SchemaGenerator:
|
|
15
|
+
"""
|
|
16
|
+
Generates database schema from entity metadata
|
|
17
|
+
|
|
18
|
+
Similar to Spring Boot's Hibernate ddl-auto feature.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, session):
|
|
22
|
+
"""
|
|
23
|
+
Initialize schema generator
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
session: SQLAlchemy session
|
|
27
|
+
"""
|
|
28
|
+
self.session = session
|
|
29
|
+
|
|
30
|
+
def create_tables_from_entities(self, entity_classes: List[Type]) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Create database tables from entity classes
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
entity_classes: List of entity classes with @Entity decorator
|
|
36
|
+
"""
|
|
37
|
+
logger.info("Creating database tables from entities...")
|
|
38
|
+
|
|
39
|
+
for entity_class in entity_classes:
|
|
40
|
+
if not hasattr(entity_class, '_entity_metadata'):
|
|
41
|
+
logger.warning(f"Skipping {entity_class.__name__} - not an entity")
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
self._create_table_for_entity(entity_class)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.error(f"Failed to create table for {entity_class.__name__}: {e}")
|
|
48
|
+
raise
|
|
49
|
+
|
|
50
|
+
self.session.commit()
|
|
51
|
+
logger.info(f"Successfully created {len(entity_classes)} tables")
|
|
52
|
+
|
|
53
|
+
def _create_table_for_entity(self, entity_class: Type) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Create a single table from entity metadata
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
entity_class: Entity class with metadata
|
|
59
|
+
"""
|
|
60
|
+
metadata = entity_class._entity_metadata
|
|
61
|
+
table_name = metadata.table_name
|
|
62
|
+
|
|
63
|
+
# Check if table already exists
|
|
64
|
+
check_sql = f"""
|
|
65
|
+
SELECT name FROM sqlite_master
|
|
66
|
+
WHERE type='table' AND name='{table_name}'
|
|
67
|
+
"""
|
|
68
|
+
result = self.session.execute(text(check_sql))
|
|
69
|
+
if result.fetchone():
|
|
70
|
+
logger.debug(f"Table '{table_name}' already exists, skipping")
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# Build CREATE TABLE statement
|
|
74
|
+
columns = []
|
|
75
|
+
|
|
76
|
+
for field_name, col_meta in metadata.columns.items():
|
|
77
|
+
col_def = self._build_column_definition(field_name, col_meta)
|
|
78
|
+
columns.append(col_def)
|
|
79
|
+
|
|
80
|
+
columns_sql = ",\n ".join(columns)
|
|
81
|
+
create_sql = f"""
|
|
82
|
+
CREATE TABLE IF NOT EXISTS {table_name} (
|
|
83
|
+
{columns_sql}
|
|
84
|
+
)
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
logger.info(f"Creating table: {table_name}")
|
|
88
|
+
self.session.execute(text(create_sql))
|
|
89
|
+
|
|
90
|
+
def _build_column_definition(self, field_name: str, col_meta) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Build column definition SQL
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
field_name: Field name
|
|
96
|
+
col_meta: Column metadata
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Column definition SQL
|
|
100
|
+
"""
|
|
101
|
+
from datetime import datetime
|
|
102
|
+
|
|
103
|
+
# Determine SQL type
|
|
104
|
+
col_type = col_meta.type
|
|
105
|
+
if col_type == int:
|
|
106
|
+
sql_type = "INTEGER"
|
|
107
|
+
elif col_type == str:
|
|
108
|
+
length = col_meta.length or 255
|
|
109
|
+
sql_type = f"TEXT" # SQLite uses TEXT for all strings
|
|
110
|
+
elif col_type == bool:
|
|
111
|
+
sql_type = "BOOLEAN"
|
|
112
|
+
elif col_type == datetime:
|
|
113
|
+
sql_type = "DATETIME"
|
|
114
|
+
elif col_type == float:
|
|
115
|
+
sql_type = "REAL"
|
|
116
|
+
else:
|
|
117
|
+
sql_type = "TEXT" # Default fallback
|
|
118
|
+
|
|
119
|
+
# Build column definition
|
|
120
|
+
parts = [field_name, sql_type]
|
|
121
|
+
|
|
122
|
+
# Primary key
|
|
123
|
+
if col_meta.primary_key:
|
|
124
|
+
parts.append("PRIMARY KEY")
|
|
125
|
+
|
|
126
|
+
# Auto increment
|
|
127
|
+
if col_meta.auto_increment:
|
|
128
|
+
parts.append("AUTOINCREMENT")
|
|
129
|
+
|
|
130
|
+
# Nullable
|
|
131
|
+
if not col_meta.nullable:
|
|
132
|
+
parts.append("NOT NULL")
|
|
133
|
+
|
|
134
|
+
# Unique
|
|
135
|
+
if col_meta.unique:
|
|
136
|
+
parts.append("UNIQUE")
|
|
137
|
+
|
|
138
|
+
# Default value
|
|
139
|
+
if col_meta.default is not None and not callable(col_meta.default):
|
|
140
|
+
if isinstance(col_meta.default, bool):
|
|
141
|
+
default_val = "1" if col_meta.default else "0"
|
|
142
|
+
elif isinstance(col_meta.default, str):
|
|
143
|
+
default_val = f"'{col_meta.default}'"
|
|
144
|
+
elif isinstance(col_meta.default, (int, float)):
|
|
145
|
+
default_val = str(col_meta.default)
|
|
146
|
+
else:
|
|
147
|
+
# Skip complex defaults
|
|
148
|
+
return " ".join(parts)
|
|
149
|
+
parts.append(f"DEFAULT {default_val}")
|
|
150
|
+
|
|
151
|
+
return " ".join(parts)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transaction management
|
|
3
|
+
|
|
4
|
+
Provides declarative transaction support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Callable
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from starspring.data.orm_gateway import get_orm_gateway
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def Transactional(func: Callable) -> Callable:
|
|
13
|
+
"""
|
|
14
|
+
Mark a method as transactional
|
|
15
|
+
|
|
16
|
+
Similar to Spring Boot's @Transactional annotation.
|
|
17
|
+
Automatically commits on success and rolls back on exception.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
@Service
|
|
21
|
+
class UserService:
|
|
22
|
+
@Transactional
|
|
23
|
+
async def create_user(self, user: User):
|
|
24
|
+
# Operations here are wrapped in a transaction
|
|
25
|
+
return await self.user_repository.save(user)
|
|
26
|
+
"""
|
|
27
|
+
@wraps(func)
|
|
28
|
+
async def async_wrapper(*args, **kwargs):
|
|
29
|
+
gateway = get_orm_gateway()
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
gateway.begin_transaction()
|
|
33
|
+
result = await func(*args, **kwargs)
|
|
34
|
+
gateway.commit()
|
|
35
|
+
return result
|
|
36
|
+
except Exception as e:
|
|
37
|
+
gateway.rollback()
|
|
38
|
+
raise e
|
|
39
|
+
|
|
40
|
+
@wraps(func)
|
|
41
|
+
def sync_wrapper(*args, **kwargs):
|
|
42
|
+
gateway = get_orm_gateway()
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
gateway.begin_transaction()
|
|
46
|
+
result = func(*args, **kwargs)
|
|
47
|
+
gateway.commit()
|
|
48
|
+
return result
|
|
49
|
+
except Exception as e:
|
|
50
|
+
gateway.rollback()
|
|
51
|
+
raise e
|
|
52
|
+
|
|
53
|
+
# Return appropriate wrapper based on function type
|
|
54
|
+
import inspect
|
|
55
|
+
if inspect.iscoroutinefunction(func):
|
|
56
|
+
return async_wrapper
|
|
57
|
+
else:
|
|
58
|
+
return sync_wrapper
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Decorator components for routing, DI, and configuration"""
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Component decorators for dependency injection
|
|
3
|
+
|
|
4
|
+
Provides Spring Boot-style component annotations like @Controller, @Service, etc.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Type, TypeVar, Callable
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from starspring.core.context import get_application_context, BeanScope
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
T = TypeVar('T')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def Controller(prefix: str = "") -> Callable[[Type[T]], Type[T]]:
|
|
16
|
+
"""
|
|
17
|
+
Mark a class as a REST controller
|
|
18
|
+
|
|
19
|
+
Similar to Spring Boot's @RestController annotation.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
prefix: URL prefix for all routes in this controller
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
@Controller("/api/users")
|
|
26
|
+
class UserController:
|
|
27
|
+
pass
|
|
28
|
+
"""
|
|
29
|
+
def decorator(cls: Type[T]) -> Type[T]:
|
|
30
|
+
# Store metadata on the class
|
|
31
|
+
cls._is_controller = True # type: ignore
|
|
32
|
+
cls._controller_prefix = prefix # type: ignore
|
|
33
|
+
cls._routes = [] # type: ignore
|
|
34
|
+
|
|
35
|
+
# Register in application context
|
|
36
|
+
context = get_application_context()
|
|
37
|
+
context.register_bean(cls, scope=BeanScope.SINGLETON)
|
|
38
|
+
|
|
39
|
+
return cls
|
|
40
|
+
|
|
41
|
+
return decorator
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def Service(cls: Optional[Type[T]] = None, *, name: Optional[str] = None) -> Type[T]:
|
|
45
|
+
"""
|
|
46
|
+
Mark a class as a service component
|
|
47
|
+
|
|
48
|
+
Similar to Spring Boot's @Service annotation.
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
@Service
|
|
52
|
+
class UserService:
|
|
53
|
+
pass
|
|
54
|
+
"""
|
|
55
|
+
def decorator(target_cls: Type[T]) -> Type[T]:
|
|
56
|
+
# Store metadata
|
|
57
|
+
target_cls._is_service = True # type: ignore
|
|
58
|
+
|
|
59
|
+
# Register in application context
|
|
60
|
+
context = get_application_context()
|
|
61
|
+
context.register_bean(target_cls, scope=BeanScope.SINGLETON, name=name)
|
|
62
|
+
|
|
63
|
+
return target_cls
|
|
64
|
+
|
|
65
|
+
# Handle both @Service and @Service() syntax
|
|
66
|
+
if cls is None:
|
|
67
|
+
return decorator # type: ignore
|
|
68
|
+
else:
|
|
69
|
+
return decorator(cls)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def Component(cls: Optional[Type[T]] = None, *, name: Optional[str] = None) -> Type[T]:
|
|
73
|
+
"""
|
|
74
|
+
Mark a class as a generic component
|
|
75
|
+
|
|
76
|
+
Similar to Spring Boot's @Component annotation.
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
@Component
|
|
80
|
+
class EmailSender:
|
|
81
|
+
pass
|
|
82
|
+
"""
|
|
83
|
+
def decorator(target_cls: Type[T]) -> Type[T]:
|
|
84
|
+
# Store metadata
|
|
85
|
+
target_cls._is_component = True # type: ignore
|
|
86
|
+
|
|
87
|
+
# Register in application context
|
|
88
|
+
context = get_application_context()
|
|
89
|
+
context.register_bean(target_cls, scope=BeanScope.SINGLETON, name=name)
|
|
90
|
+
|
|
91
|
+
return target_cls
|
|
92
|
+
|
|
93
|
+
# Handle both @Component and @Component() syntax
|
|
94
|
+
if cls is None:
|
|
95
|
+
return decorator # type: ignore
|
|
96
|
+
else:
|
|
97
|
+
return decorator(cls)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def Repository(cls: Optional[Type[T]] = None, *, name: Optional[str] = None) -> Type[T]:
|
|
101
|
+
"""
|
|
102
|
+
Mark a class as a repository component
|
|
103
|
+
|
|
104
|
+
Similar to Spring Boot's @Repository annotation.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
@Repository
|
|
108
|
+
class UserRepository:
|
|
109
|
+
pass
|
|
110
|
+
"""
|
|
111
|
+
def decorator(target_cls: Type[T]) -> Type[T]:
|
|
112
|
+
# Store metadata
|
|
113
|
+
target_cls._is_repository = True # type: ignore
|
|
114
|
+
|
|
115
|
+
# Register in application context
|
|
116
|
+
context = get_application_context()
|
|
117
|
+
context.register_bean(target_cls, scope=BeanScope.SINGLETON, name=name)
|
|
118
|
+
|
|
119
|
+
return target_cls
|
|
120
|
+
|
|
121
|
+
# Handle both @Repository and @Repository() syntax
|
|
122
|
+
if cls is None:
|
|
123
|
+
return decorator # type: ignore
|
|
124
|
+
else:
|
|
125
|
+
return decorator(cls)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def TemplateController(prefix: str = "") -> Callable[[Type[T]], Type[T]]:
|
|
129
|
+
"""
|
|
130
|
+
Mark a class as a template controller
|
|
131
|
+
|
|
132
|
+
Similar to Spring MVC's @Controller annotation.
|
|
133
|
+
Template controllers return ModelAndView objects for HTML responses.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
prefix: URL prefix for all routes in this controller
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
@TemplateController("/")
|
|
140
|
+
class HomeController:
|
|
141
|
+
@GetMapping("/")
|
|
142
|
+
def index(self) -> ModelAndView:
|
|
143
|
+
return ModelAndView("index.html", {"title": "Home"})
|
|
144
|
+
"""
|
|
145
|
+
def decorator(cls: Type[T]) -> Type[T]:
|
|
146
|
+
# Store metadata on the class
|
|
147
|
+
cls._is_controller = True # type: ignore
|
|
148
|
+
cls._is_template_controller = True # type: ignore
|
|
149
|
+
cls._controller_prefix = prefix # type: ignore
|
|
150
|
+
cls._routes = [] # type: ignore
|
|
151
|
+
|
|
152
|
+
# Register in application context
|
|
153
|
+
context = get_application_context()
|
|
154
|
+
context.register_bean(cls, scope=BeanScope.SINGLETON)
|
|
155
|
+
|
|
156
|
+
return cls
|
|
157
|
+
|
|
158
|
+
return decorator
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def Autowired(func: Callable) -> Callable:
|
|
162
|
+
"""
|
|
163
|
+
Mark a method for dependency injection
|
|
164
|
+
|
|
165
|
+
Similar to Spring Boot's @Autowired annotation.
|
|
166
|
+
This is mainly for documentation purposes as dependency injection
|
|
167
|
+
happens automatically in the constructor.
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
@Autowired
|
|
171
|
+
def __init__(self, user_service: UserService):
|
|
172
|
+
self.user_service = user_service
|
|
173
|
+
"""
|
|
174
|
+
@wraps(func)
|
|
175
|
+
def wrapper(*args, **kwargs):
|
|
176
|
+
return func(*args, **kwargs)
|
|
177
|
+
|
|
178
|
+
wrapper._is_autowired = True # type: ignore
|
|
179
|
+
return wrapper
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration decorators
|
|
3
|
+
|
|
4
|
+
Provides Spring Boot-style configuration annotations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Callable, Optional, Type, TypeVar, Any
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from starspring.core.context import get_application_context
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
T = TypeVar('T')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def Configuration(cls: Type[T]) -> Type[T]:
|
|
16
|
+
"""
|
|
17
|
+
Mark a class as a configuration class
|
|
18
|
+
|
|
19
|
+
Similar to Spring Boot's @Configuration annotation.
|
|
20
|
+
Configuration classes can contain @Bean methods.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
@Configuration
|
|
24
|
+
class AppConfig:
|
|
25
|
+
@Bean
|
|
26
|
+
def database(self):
|
|
27
|
+
return Database()
|
|
28
|
+
"""
|
|
29
|
+
cls._is_configuration = True # type: ignore
|
|
30
|
+
|
|
31
|
+
# Process @Bean methods
|
|
32
|
+
context = get_application_context()
|
|
33
|
+
instance = cls()
|
|
34
|
+
|
|
35
|
+
for attr_name in dir(cls):
|
|
36
|
+
attr = getattr(cls, attr_name)
|
|
37
|
+
if callable(attr) and hasattr(attr, '_is_bean'):
|
|
38
|
+
# Call the bean factory method
|
|
39
|
+
bean_instance = attr(instance)
|
|
40
|
+
bean_type = type(bean_instance)
|
|
41
|
+
bean_name = getattr(attr, '_bean_name', None)
|
|
42
|
+
|
|
43
|
+
# Register the bean
|
|
44
|
+
context.register_bean(
|
|
45
|
+
bean_type,
|
|
46
|
+
factory=lambda: attr(instance),
|
|
47
|
+
name=bean_name
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return cls
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def Bean(func: Optional[Callable] = None, *, name: Optional[str] = None) -> Callable:
|
|
54
|
+
"""
|
|
55
|
+
Mark a method as a bean factory method
|
|
56
|
+
|
|
57
|
+
Similar to Spring Boot's @Bean annotation.
|
|
58
|
+
Must be used within a @Configuration class.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
name: Optional custom name for the bean
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
@Configuration
|
|
65
|
+
class AppConfig:
|
|
66
|
+
@Bean
|
|
67
|
+
def user_service(self):
|
|
68
|
+
return UserService()
|
|
69
|
+
"""
|
|
70
|
+
def decorator(target_func: Callable) -> Callable:
|
|
71
|
+
target_func._is_bean = True # type: ignore
|
|
72
|
+
target_func._bean_name = name # type: ignore
|
|
73
|
+
return target_func
|
|
74
|
+
|
|
75
|
+
# Handle both @Bean and @Bean() syntax
|
|
76
|
+
if func is None:
|
|
77
|
+
return decorator
|
|
78
|
+
else:
|
|
79
|
+
return decorator(func)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def Value(key: str, default: Any = None) -> Any:
|
|
83
|
+
"""
|
|
84
|
+
Inject a configuration value
|
|
85
|
+
|
|
86
|
+
Similar to Spring Boot's @Value annotation.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
key: Configuration key to inject
|
|
90
|
+
default: Default value if key not found
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
class MyService:
|
|
94
|
+
def __init__(self):
|
|
95
|
+
self.api_key = Value("api.key", "default-key")
|
|
96
|
+
|
|
97
|
+
Note: This is a simplified implementation. In a real application,
|
|
98
|
+
this would integrate with the configuration system.
|
|
99
|
+
"""
|
|
100
|
+
# This is a placeholder - actual implementation would read from config
|
|
101
|
+
from starspring.config.properties import get_property
|
|
102
|
+
return get_property(key, default)
|