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,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception hierarchy for StarSpring framework
|
|
3
|
+
|
|
4
|
+
Provides Spring Boot-style exception classes with automatic HTTP status mapping.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StarSpringException(Exception):
|
|
11
|
+
"""Base exception for all StarSpring framework exceptions"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
message: str,
|
|
16
|
+
status_code: int = 500,
|
|
17
|
+
details: Optional[Dict[str, Any]] = None
|
|
18
|
+
):
|
|
19
|
+
super().__init__(message)
|
|
20
|
+
self.message = message
|
|
21
|
+
self.status_code = status_code
|
|
22
|
+
self.details = details or {}
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
25
|
+
"""Convert exception to dictionary for JSON response"""
|
|
26
|
+
result = {
|
|
27
|
+
"error": self.__class__.__name__,
|
|
28
|
+
"message": self.message,
|
|
29
|
+
"status": self.status_code,
|
|
30
|
+
}
|
|
31
|
+
if self.details:
|
|
32
|
+
result["details"] = self.details
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BadRequestException(StarSpringException):
|
|
37
|
+
"""Exception for 400 Bad Request errors"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, message: str = "Bad Request", details: Optional[Dict[str, Any]] = None):
|
|
40
|
+
super().__init__(message, status_code=400, details=details)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UnauthorizedException(StarSpringException):
|
|
44
|
+
"""Exception for 401 Unauthorized errors"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, message: str = "Unauthorized", details: Optional[Dict[str, Any]] = None):
|
|
47
|
+
super().__init__(message, status_code=401, details=details)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ForbiddenException(StarSpringException):
|
|
51
|
+
"""Exception for 403 Forbidden errors"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, message: str = "Forbidden", details: Optional[Dict[str, Any]] = None):
|
|
54
|
+
super().__init__(message, status_code=403, details=details)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class NotFoundException(StarSpringException):
|
|
58
|
+
"""Exception for 404 Not Found errors"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, message: str = "Resource Not Found", details: Optional[Dict[str, Any]] = None):
|
|
61
|
+
super().__init__(message, status_code=404, details=details)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ConflictException(StarSpringException):
|
|
65
|
+
"""Exception for 409 Conflict errors"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, message: str = "Conflict", details: Optional[Dict[str, Any]] = None):
|
|
68
|
+
super().__init__(message, status_code=409, details=details)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ValidationException(StarSpringException):
|
|
72
|
+
"""Exception for validation errors"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, message: str = "Validation Failed", details: Optional[Dict[str, Any]] = None):
|
|
75
|
+
super().__init__(message, status_code=422, details=details)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class InternalServerException(StarSpringException):
|
|
79
|
+
"""Exception for 500 Internal Server errors"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, message: str = "Internal Server Error", details: Optional[Dict[str, Any]] = None):
|
|
82
|
+
super().__init__(message, status_code=500, details=details)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Response utilities similar to Spring Boot's ResponseEntity
|
|
3
|
+
|
|
4
|
+
Provides structured response classes for API endpoints.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Dict, Any, Generic, TypeVar
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
T = TypeVar('T')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HttpStatus(Enum):
|
|
16
|
+
"""HTTP status codes"""
|
|
17
|
+
OK = 200
|
|
18
|
+
CREATED = 201
|
|
19
|
+
ACCEPTED = 202
|
|
20
|
+
NO_CONTENT = 204
|
|
21
|
+
BAD_REQUEST = 400
|
|
22
|
+
UNAUTHORIZED = 401
|
|
23
|
+
FORBIDDEN = 403
|
|
24
|
+
NOT_FOUND = 404
|
|
25
|
+
CONFLICT = 409
|
|
26
|
+
UNPROCESSABLE_ENTITY = 422
|
|
27
|
+
INTERNAL_SERVER_ERROR = 500
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ApiResponse(BaseModel, Generic[T]):
|
|
31
|
+
"""
|
|
32
|
+
Generic API response wrapper
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
return ApiResponse(
|
|
36
|
+
success=True,
|
|
37
|
+
data=user,
|
|
38
|
+
message="User created successfully"
|
|
39
|
+
)
|
|
40
|
+
"""
|
|
41
|
+
success: bool
|
|
42
|
+
data: Optional[T] = None
|
|
43
|
+
message: Optional[str] = None
|
|
44
|
+
errors: Optional[Dict[str, Any]] = None
|
|
45
|
+
|
|
46
|
+
class Config:
|
|
47
|
+
arbitrary_types_allowed = True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ResponseEntity(Generic[T]):
|
|
51
|
+
"""
|
|
52
|
+
Response entity similar to Spring Boot's ResponseEntity
|
|
53
|
+
|
|
54
|
+
Provides fluent API for building HTTP responses with status codes and headers.
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
return ResponseEntity.ok(user)
|
|
58
|
+
return ResponseEntity.created(user).header("Location", "/api/users/1")
|
|
59
|
+
return ResponseEntity.not_found().body({"error": "User not found"})
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
body: Optional[T] = None,
|
|
65
|
+
status: int = 200,
|
|
66
|
+
headers: Optional[Dict[str, str]] = None
|
|
67
|
+
):
|
|
68
|
+
self.body = body
|
|
69
|
+
self.status = status
|
|
70
|
+
self.headers = headers or {}
|
|
71
|
+
|
|
72
|
+
def header(self, key: str, value: str) -> 'ResponseEntity[T]':
|
|
73
|
+
"""Add a header to the response"""
|
|
74
|
+
self.headers[key] = value
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def to_starlette_response(self):
|
|
78
|
+
"""Convert to Starlette JSONResponse"""
|
|
79
|
+
from starlette.responses import JSONResponse
|
|
80
|
+
|
|
81
|
+
# Handle Pydantic models
|
|
82
|
+
if hasattr(self.body, 'model_dump'):
|
|
83
|
+
content = self.body.model_dump()
|
|
84
|
+
elif hasattr(self.body, 'dict'):
|
|
85
|
+
content = self.body.dict()
|
|
86
|
+
elif isinstance(self.body, list):
|
|
87
|
+
content = [
|
|
88
|
+
item.model_dump() if hasattr(item, 'model_dump')
|
|
89
|
+
else item.dict() if hasattr(item, 'dict')
|
|
90
|
+
else item
|
|
91
|
+
for item in self.body
|
|
92
|
+
]
|
|
93
|
+
else:
|
|
94
|
+
content = self.body
|
|
95
|
+
|
|
96
|
+
return JSONResponse(
|
|
97
|
+
content=content,
|
|
98
|
+
status_code=self.status,
|
|
99
|
+
headers=self.headers
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Static factory methods for common responses
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def ok(body: Optional[T] = None) -> 'ResponseEntity[T]':
|
|
106
|
+
"""Create a 200 OK response"""
|
|
107
|
+
return ResponseEntity(body=body, status=HttpStatus.OK.value)
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def created(body: Optional[T] = None) -> 'ResponseEntity[T]':
|
|
111
|
+
"""Create a 201 Created response"""
|
|
112
|
+
return ResponseEntity(body=body, status=HttpStatus.CREATED.value)
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def accepted(body: Optional[T] = None) -> 'ResponseEntity[T]':
|
|
116
|
+
"""Create a 202 Accepted response"""
|
|
117
|
+
return ResponseEntity(body=body, status=HttpStatus.ACCEPTED.value)
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def no_content() -> 'ResponseEntity[None]':
|
|
121
|
+
"""Create a 204 No Content response"""
|
|
122
|
+
return ResponseEntity(body=None, status=HttpStatus.NO_CONTENT.value)
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def bad_request(body: Optional[T] = None) -> 'ResponseEntity[T]':
|
|
126
|
+
"""Create a 400 Bad Request response"""
|
|
127
|
+
return ResponseEntity(body=body, status=HttpStatus.BAD_REQUEST.value)
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def unauthorized(body: Optional[T] = None) -> 'ResponseEntity[T]':
|
|
131
|
+
"""Create a 401 Unauthorized response"""
|
|
132
|
+
return ResponseEntity(body=body, status=HttpStatus.UNAUTHORIZED.value)
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def forbidden(body: Optional[T] = None) -> 'ResponseEntity[T]':
|
|
136
|
+
"""Create a 403 Forbidden response"""
|
|
137
|
+
return ResponseEntity(body=body, status=HttpStatus.FORBIDDEN.value)
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def not_found(body: Optional[T] = None) -> 'ResponseEntity[T]':
|
|
141
|
+
"""Create a 404 Not Found response"""
|
|
142
|
+
return ResponseEntity(body=body, status=HttpStatus.NOT_FOUND.value)
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def status(status_code: int, body: Optional[T] = None) -> 'ResponseEntity[T]':
|
|
146
|
+
"""Create a response with custom status code"""
|
|
147
|
+
return ResponseEntity(body=body, status=status_code)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data package initialization
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from starspring.data.repository import Repository, CrudRepository, StarRepository
|
|
6
|
+
from starspring.data.entity import (
|
|
7
|
+
Entity,
|
|
8
|
+
Column,
|
|
9
|
+
Id,
|
|
10
|
+
GeneratedValue,
|
|
11
|
+
ManyToOne,
|
|
12
|
+
OneToMany,
|
|
13
|
+
ManyToMany,
|
|
14
|
+
BaseEntity,
|
|
15
|
+
GenerationType,
|
|
16
|
+
ColumnMetadata,
|
|
17
|
+
RelationshipMetadata,
|
|
18
|
+
EntityMetadata
|
|
19
|
+
)
|
|
20
|
+
from starspring.data.orm_gateway import ORMGateway, SQLAlchemyGateway, get_orm_gateway, set_orm_gateway
|
|
21
|
+
from starspring.data.transaction import Transactional
|
|
22
|
+
from starspring.data.query_builder import QueryMethodParser, SQLQueryGenerator
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
'Repository',
|
|
26
|
+
'CrudRepository',
|
|
27
|
+
'StarRepository',
|
|
28
|
+
'Entity',
|
|
29
|
+
'Column',
|
|
30
|
+
'Id',
|
|
31
|
+
'GeneratedValue',
|
|
32
|
+
'ManyToOne',
|
|
33
|
+
'OneToMany',
|
|
34
|
+
'ManyToMany',
|
|
35
|
+
'BaseEntity',
|
|
36
|
+
'GenerationType',
|
|
37
|
+
'ColumnMetadata',
|
|
38
|
+
'RelationshipMetadata',
|
|
39
|
+
'EntityMetadata',
|
|
40
|
+
'ORMGateway',
|
|
41
|
+
'SQLAlchemyGateway',
|
|
42
|
+
'get_orm_gateway',
|
|
43
|
+
'set_orm_gateway',
|
|
44
|
+
'Transactional',
|
|
45
|
+
'QueryMethodParser',
|
|
46
|
+
'SQLQueryGenerator',
|
|
47
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database configuration module
|
|
3
|
+
|
|
4
|
+
Provides database connection configuration for MySQL, PostgreSQL, and SQLite.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from starspring.config.properties import get_properties
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DatabaseConfig:
|
|
15
|
+
"""
|
|
16
|
+
Database configuration
|
|
17
|
+
|
|
18
|
+
Loads database settings from application properties and creates
|
|
19
|
+
SQLAlchemy engine and session factory.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
"""Initialize database configuration from properties"""
|
|
24
|
+
try:
|
|
25
|
+
from sqlalchemy import create_engine
|
|
26
|
+
from sqlalchemy.orm import sessionmaker
|
|
27
|
+
except ImportError:
|
|
28
|
+
raise ImportError(
|
|
29
|
+
"SQLAlchemy is required for database support. "
|
|
30
|
+
"Install it with: pip install sqlalchemy"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Load configuration
|
|
34
|
+
props = get_properties()
|
|
35
|
+
|
|
36
|
+
self.url = props.get("database.url")
|
|
37
|
+
if not self.url:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
"Database URL not configured. "
|
|
40
|
+
"Add 'database.url' to application.yaml"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.pool_size = props.get_int("database.pool_size", 10)
|
|
44
|
+
self.max_overflow = props.get_int("database.max_overflow", 20)
|
|
45
|
+
self.echo = props.get_bool("database.echo", False)
|
|
46
|
+
self.pool_pre_ping = props.get_bool("database.pool_pre_ping", True)
|
|
47
|
+
|
|
48
|
+
# Create engine
|
|
49
|
+
logger.info(f"Initializing database: {self._mask_password(self.url)}")
|
|
50
|
+
|
|
51
|
+
self.engine = create_engine(
|
|
52
|
+
self.url,
|
|
53
|
+
pool_size=self.pool_size,
|
|
54
|
+
max_overflow=self.max_overflow,
|
|
55
|
+
echo=self.echo,
|
|
56
|
+
pool_pre_ping=self.pool_pre_ping
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Create session factory
|
|
60
|
+
self.SessionFactory = sessionmaker(bind=self.engine)
|
|
61
|
+
|
|
62
|
+
logger.info("Database initialized successfully")
|
|
63
|
+
|
|
64
|
+
def _mask_password(self, url: str) -> str:
|
|
65
|
+
"""Mask password in database URL for logging"""
|
|
66
|
+
if '@' in url and '://' in url:
|
|
67
|
+
protocol, rest = url.split('://', 1)
|
|
68
|
+
if '@' in rest:
|
|
69
|
+
credentials, host = rest.split('@', 1)
|
|
70
|
+
if ':' in credentials:
|
|
71
|
+
user, _ = credentials.split(':', 1)
|
|
72
|
+
return f"{protocol}://{user}:****@{host}"
|
|
73
|
+
return url
|
|
74
|
+
|
|
75
|
+
def create_tables(self, base):
|
|
76
|
+
"""
|
|
77
|
+
Create all tables defined in the Base metadata
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
base: SQLAlchemy declarative base
|
|
81
|
+
"""
|
|
82
|
+
logger.info("Creating database tables...")
|
|
83
|
+
base.metadata.create_all(self.engine)
|
|
84
|
+
logger.info("Database tables created")
|
|
85
|
+
|
|
86
|
+
def drop_tables(self, base):
|
|
87
|
+
"""
|
|
88
|
+
Drop all tables defined in the Base metadata
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
base: SQLAlchemy declarative base
|
|
92
|
+
"""
|
|
93
|
+
logger.warning("Dropping all database tables...")
|
|
94
|
+
base.metadata.drop_all(self.engine)
|
|
95
|
+
logger.info("Database tables dropped")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Global database config instance
|
|
99
|
+
_database_config: Optional[DatabaseConfig] = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_database_config() -> DatabaseConfig:
|
|
103
|
+
"""Get the global database configuration instance"""
|
|
104
|
+
global _database_config
|
|
105
|
+
if _database_config is None:
|
|
106
|
+
_database_config = DatabaseConfig()
|
|
107
|
+
return _database_config
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def set_database_config(config: DatabaseConfig):
|
|
111
|
+
"""Set the global database configuration instance"""
|
|
112
|
+
global _database_config
|
|
113
|
+
_database_config = config
|