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,345 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Query method parser and SQL generator
|
|
3
|
+
|
|
4
|
+
Parses repository method names and generates SQL queries automatically.
|
|
5
|
+
Similar to Spring Data JPA's query derivation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import List, Tuple, Any, Optional, Type
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class QueryOperation(Enum):
|
|
14
|
+
"""Query operation types"""
|
|
15
|
+
FIND = "find"
|
|
16
|
+
COUNT = "count"
|
|
17
|
+
DELETE = "delete"
|
|
18
|
+
EXISTS = "exists"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class QueryCondition(Enum):
|
|
22
|
+
"""Query condition operators"""
|
|
23
|
+
EQUALS = ""
|
|
24
|
+
AND = "And"
|
|
25
|
+
OR = "Or"
|
|
26
|
+
GREATER_THAN = "GreaterThan"
|
|
27
|
+
LESS_THAN = "LessThan"
|
|
28
|
+
GREATER_THAN_EQUAL = "GreaterThanEqual"
|
|
29
|
+
LESS_THAN_EQUAL = "LessThanEqual"
|
|
30
|
+
LIKE = "Like"
|
|
31
|
+
CONTAINING = "Containing"
|
|
32
|
+
STARTING_WITH = "StartingWith"
|
|
33
|
+
ENDING_WITH = "EndingWith"
|
|
34
|
+
BETWEEN = "Between"
|
|
35
|
+
IN = "In"
|
|
36
|
+
NOT = "Not"
|
|
37
|
+
IS_NULL = "IsNull"
|
|
38
|
+
IS_NOT_NULL = "IsNotNull"
|
|
39
|
+
TRUE = "True"
|
|
40
|
+
FALSE = "False"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class QueryPart:
|
|
44
|
+
"""Represents a part of a query"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
field: str,
|
|
49
|
+
operator: QueryCondition,
|
|
50
|
+
connector: Optional[str] = None
|
|
51
|
+
):
|
|
52
|
+
self.field = field
|
|
53
|
+
self.operator = operator
|
|
54
|
+
self.connector = connector # AND or OR
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ParsedQuery:
|
|
58
|
+
"""Parsed query information"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
operation: QueryOperation,
|
|
63
|
+
parts: List[QueryPart],
|
|
64
|
+
order_by: Optional[List[Tuple[str, str]]] = None
|
|
65
|
+
):
|
|
66
|
+
self.operation = operation
|
|
67
|
+
self.parts = parts
|
|
68
|
+
self.order_by = order_by or []
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class QueryMethodParser:
|
|
72
|
+
"""
|
|
73
|
+
Parses repository method names into query components
|
|
74
|
+
|
|
75
|
+
Supports patterns like:
|
|
76
|
+
- findByName
|
|
77
|
+
- findByEmailAndActive
|
|
78
|
+
- findByAgeGreaterThan
|
|
79
|
+
- findByNameContaining
|
|
80
|
+
- countByActive
|
|
81
|
+
- deleteByEmail
|
|
82
|
+
- existsByUsername
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
# Regex patterns for parsing
|
|
86
|
+
OPERATION_PATTERN = r'^(find|count|delete|exists)By'
|
|
87
|
+
CONDITION_PATTERNS = {
|
|
88
|
+
'GreaterThanEqual': r'GreaterThanEqual',
|
|
89
|
+
'LessThanEqual': r'LessThanEqual',
|
|
90
|
+
'GreaterThan': r'GreaterThan',
|
|
91
|
+
'LessThan': r'LessThan',
|
|
92
|
+
'Containing': r'Containing',
|
|
93
|
+
'StartingWith': r'StartingWith',
|
|
94
|
+
'EndingWith': r'EndingWith',
|
|
95
|
+
'Between': r'Between',
|
|
96
|
+
'Like': r'Like',
|
|
97
|
+
'In': r'In',
|
|
98
|
+
'IsNull': r'IsNull',
|
|
99
|
+
'IsNotNull': r'IsNotNull',
|
|
100
|
+
'True': r'True',
|
|
101
|
+
'False': r'False',
|
|
102
|
+
'Not': r'Not',
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
def parse(self, method_name: str) -> ParsedQuery:
|
|
106
|
+
"""
|
|
107
|
+
Parse a repository method name into query components
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
method_name: Method name to parse
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
ParsedQuery object
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
ValueError: If method name cannot be parsed
|
|
117
|
+
"""
|
|
118
|
+
# Extract operation
|
|
119
|
+
operation_match = re.match(self.OPERATION_PATTERN, method_name)
|
|
120
|
+
if not operation_match:
|
|
121
|
+
raise ValueError(f"Invalid query method name: {method_name}")
|
|
122
|
+
|
|
123
|
+
operation_str = operation_match.group(1)
|
|
124
|
+
operation = QueryOperation(operation_str)
|
|
125
|
+
|
|
126
|
+
# Remove operation prefix
|
|
127
|
+
remainder = method_name[len(operation_match.group(0)):]
|
|
128
|
+
|
|
129
|
+
# Check for OrderBy clause
|
|
130
|
+
order_by = []
|
|
131
|
+
if 'OrderBy' in remainder:
|
|
132
|
+
parts = remainder.split('OrderBy')
|
|
133
|
+
remainder = parts[0]
|
|
134
|
+
order_clause = parts[1]
|
|
135
|
+
order_by = self._parse_order_by(order_clause)
|
|
136
|
+
|
|
137
|
+
# Parse conditions
|
|
138
|
+
query_parts = self._parse_conditions(remainder)
|
|
139
|
+
|
|
140
|
+
return ParsedQuery(operation, query_parts, order_by)
|
|
141
|
+
|
|
142
|
+
def _parse_conditions(self, condition_str: str) -> List[QueryPart]:
|
|
143
|
+
"""Parse condition string into query parts"""
|
|
144
|
+
parts = []
|
|
145
|
+
|
|
146
|
+
# Split by And/Or
|
|
147
|
+
tokens = re.split(r'(And|Or)', condition_str)
|
|
148
|
+
|
|
149
|
+
current_connector = None
|
|
150
|
+
i = 0
|
|
151
|
+
while i < len(tokens):
|
|
152
|
+
token = tokens[i]
|
|
153
|
+
|
|
154
|
+
if token in ['And', 'Or']:
|
|
155
|
+
current_connector = token.upper()
|
|
156
|
+
i += 1
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
if not token:
|
|
160
|
+
i += 1
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
# Parse this condition
|
|
164
|
+
field, operator = self._parse_single_condition(token)
|
|
165
|
+
parts.append(QueryPart(field, operator, current_connector))
|
|
166
|
+
|
|
167
|
+
i += 1
|
|
168
|
+
|
|
169
|
+
return parts
|
|
170
|
+
|
|
171
|
+
def _parse_single_condition(self, condition: str) -> Tuple[str, QueryCondition]:
|
|
172
|
+
"""Parse a single condition into field and operator"""
|
|
173
|
+
# Check for operators
|
|
174
|
+
for op_name, op_pattern in self.CONDITION_PATTERNS.items():
|
|
175
|
+
if condition.endswith(op_name):
|
|
176
|
+
field = condition[:-len(op_name)]
|
|
177
|
+
return self._camel_to_snake(field), QueryCondition[op_name.upper().replace('THAN', '_THAN')]
|
|
178
|
+
|
|
179
|
+
# No operator means equals
|
|
180
|
+
return self._camel_to_snake(condition), QueryCondition.EQUALS
|
|
181
|
+
|
|
182
|
+
def _parse_order_by(self, order_str: str) -> List[Tuple[str, str]]:
|
|
183
|
+
"""Parse OrderBy clause"""
|
|
184
|
+
orders = []
|
|
185
|
+
parts = re.split(r'(And)', order_str)
|
|
186
|
+
|
|
187
|
+
for part in parts:
|
|
188
|
+
if part == 'And' or not part:
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
# Check for Asc/Desc
|
|
192
|
+
if part.endswith('Desc'):
|
|
193
|
+
field = part[:-4]
|
|
194
|
+
direction = 'DESC'
|
|
195
|
+
elif part.endswith('Asc'):
|
|
196
|
+
field = part[:-3]
|
|
197
|
+
direction = 'ASC'
|
|
198
|
+
else:
|
|
199
|
+
field = part
|
|
200
|
+
direction = 'ASC'
|
|
201
|
+
|
|
202
|
+
orders.append((self._camel_to_snake(field), direction))
|
|
203
|
+
|
|
204
|
+
return orders
|
|
205
|
+
|
|
206
|
+
def _camel_to_snake(self, name: str) -> str:
|
|
207
|
+
"""Convert CamelCase to snake_case"""
|
|
208
|
+
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
|
209
|
+
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class SQLQueryGenerator:
|
|
213
|
+
"""
|
|
214
|
+
Generates SQL queries from parsed query information
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
def __init__(self, table_name: str):
|
|
218
|
+
self.table_name = table_name
|
|
219
|
+
|
|
220
|
+
def generate(self, parsed_query: ParsedQuery) -> Tuple[str, List[str]]:
|
|
221
|
+
"""
|
|
222
|
+
Generate SQL query from parsed query
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
parsed_query: Parsed query information
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Tuple of (SQL query string, list of parameter placeholders)
|
|
229
|
+
"""
|
|
230
|
+
if parsed_query.operation == QueryOperation.FIND:
|
|
231
|
+
return self._generate_select(parsed_query)
|
|
232
|
+
elif parsed_query.operation == QueryOperation.COUNT:
|
|
233
|
+
return self._generate_count(parsed_query)
|
|
234
|
+
elif parsed_query.operation == QueryOperation.DELETE:
|
|
235
|
+
return self._generate_delete(parsed_query)
|
|
236
|
+
elif parsed_query.operation == QueryOperation.EXISTS:
|
|
237
|
+
return self._generate_exists(parsed_query)
|
|
238
|
+
|
|
239
|
+
raise ValueError(f"Unsupported operation: {parsed_query.operation}")
|
|
240
|
+
|
|
241
|
+
def _generate_select(self, parsed_query: ParsedQuery) -> Tuple[str, List[str]]:
|
|
242
|
+
"""Generate SELECT query"""
|
|
243
|
+
sql = f"SELECT * FROM {self.table_name}"
|
|
244
|
+
params = []
|
|
245
|
+
|
|
246
|
+
if parsed_query.parts:
|
|
247
|
+
where_clause, params = self._build_where_clause(parsed_query.parts)
|
|
248
|
+
sql += f" WHERE {where_clause}"
|
|
249
|
+
|
|
250
|
+
if parsed_query.order_by:
|
|
251
|
+
order_parts = [f"{field} {direction}" for field, direction in parsed_query.order_by]
|
|
252
|
+
sql += f" ORDER BY {', '.join(order_parts)}"
|
|
253
|
+
|
|
254
|
+
return sql, params
|
|
255
|
+
|
|
256
|
+
def _generate_count(self, parsed_query: ParsedQuery) -> Tuple[str, List[str]]:
|
|
257
|
+
"""Generate COUNT query"""
|
|
258
|
+
sql = f"SELECT COUNT(*) FROM {self.table_name}"
|
|
259
|
+
params = []
|
|
260
|
+
|
|
261
|
+
if parsed_query.parts:
|
|
262
|
+
where_clause, params = self._build_where_clause(parsed_query.parts)
|
|
263
|
+
sql += f" WHERE {where_clause}"
|
|
264
|
+
|
|
265
|
+
return sql, params
|
|
266
|
+
|
|
267
|
+
def _generate_delete(self, parsed_query: ParsedQuery) -> Tuple[str, List[str]]:
|
|
268
|
+
"""Generate DELETE query"""
|
|
269
|
+
sql = f"DELETE FROM {self.table_name}"
|
|
270
|
+
params = []
|
|
271
|
+
|
|
272
|
+
if parsed_query.parts:
|
|
273
|
+
where_clause, params = self._build_where_clause(parsed_query.parts)
|
|
274
|
+
sql += f" WHERE {where_clause}"
|
|
275
|
+
|
|
276
|
+
return sql, params
|
|
277
|
+
|
|
278
|
+
def _generate_exists(self, parsed_query: ParsedQuery) -> Tuple[str, List[str]]:
|
|
279
|
+
"""Generate EXISTS query"""
|
|
280
|
+
sql = f"SELECT EXISTS(SELECT 1 FROM {self.table_name}"
|
|
281
|
+
params = []
|
|
282
|
+
|
|
283
|
+
if parsed_query.parts:
|
|
284
|
+
where_clause, params = self._build_where_clause(parsed_query.parts)
|
|
285
|
+
sql += f" WHERE {where_clause}"
|
|
286
|
+
|
|
287
|
+
sql += ")"
|
|
288
|
+
return sql, params
|
|
289
|
+
|
|
290
|
+
def _build_where_clause(self, parts: List[QueryPart]) -> Tuple[str, List[str]]:
|
|
291
|
+
"""Build WHERE clause from query parts"""
|
|
292
|
+
conditions = []
|
|
293
|
+
params = []
|
|
294
|
+
|
|
295
|
+
for part in parts:
|
|
296
|
+
condition, part_params = self._build_condition(part)
|
|
297
|
+
|
|
298
|
+
if part.connector and conditions:
|
|
299
|
+
conditions.append(f" {part.connector} {condition}")
|
|
300
|
+
else:
|
|
301
|
+
conditions.append(condition)
|
|
302
|
+
|
|
303
|
+
params.extend(part_params)
|
|
304
|
+
|
|
305
|
+
return ''.join(conditions), params
|
|
306
|
+
|
|
307
|
+
def _build_condition(self, part: QueryPart) -> Tuple[str, List[str]]:
|
|
308
|
+
"""Build a single condition"""
|
|
309
|
+
field = part.field
|
|
310
|
+
operator = part.operator
|
|
311
|
+
|
|
312
|
+
if operator == QueryCondition.EQUALS:
|
|
313
|
+
return f"{field} = ?", [field]
|
|
314
|
+
elif operator == QueryCondition.GREATER_THAN:
|
|
315
|
+
return f"{field} > ?", [field]
|
|
316
|
+
elif operator == QueryCondition.LESS_THAN:
|
|
317
|
+
return f"{field} < ?", [field]
|
|
318
|
+
elif operator == QueryCondition.GREATER_THAN_EQUAL:
|
|
319
|
+
return f"{field} >= ?", [field]
|
|
320
|
+
elif operator == QueryCondition.LESS_THAN_EQUAL:
|
|
321
|
+
return f"{field} <= ?", [field]
|
|
322
|
+
elif operator == QueryCondition.LIKE:
|
|
323
|
+
return f"{field} LIKE ?", [field]
|
|
324
|
+
elif operator == QueryCondition.CONTAINING:
|
|
325
|
+
return f"{field} LIKE ?", [field] # Will add % in executor
|
|
326
|
+
elif operator == QueryCondition.STARTING_WITH:
|
|
327
|
+
return f"{field} LIKE ?", [field] # Will add % in executor
|
|
328
|
+
elif operator == QueryCondition.ENDING_WITH:
|
|
329
|
+
return f"{field} LIKE ?", [field] # Will add % in executor
|
|
330
|
+
elif operator == QueryCondition.BETWEEN:
|
|
331
|
+
return f"{field} BETWEEN ? AND ?", [field, field]
|
|
332
|
+
elif operator == QueryCondition.IN:
|
|
333
|
+
return f"{field} IN (?)", [field]
|
|
334
|
+
elif operator == QueryCondition.IS_NULL:
|
|
335
|
+
return f"{field} IS NULL", []
|
|
336
|
+
elif operator == QueryCondition.IS_NOT_NULL:
|
|
337
|
+
return f"{field} IS NOT NULL", []
|
|
338
|
+
elif operator == QueryCondition.TRUE:
|
|
339
|
+
return f"{field} = TRUE", []
|
|
340
|
+
elif operator == QueryCondition.FALSE:
|
|
341
|
+
return f"{field} = FALSE", []
|
|
342
|
+
elif operator == QueryCondition.NOT:
|
|
343
|
+
return f"{field} != ?", [field]
|
|
344
|
+
|
|
345
|
+
raise ValueError(f"Unsupported operator: {operator}")
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Repository pattern implementation
|
|
3
|
+
|
|
4
|
+
Provides Spring Boot-style repository pattern for data access with automatic query generation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TypeVar, Generic, List, Optional, Any, Type
|
|
8
|
+
from starspring.data.orm_gateway import get_orm_gateway
|
|
9
|
+
import inspect
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
T = TypeVar('T')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Repository(Generic[T]):
|
|
18
|
+
"""
|
|
19
|
+
Generic repository base class
|
|
20
|
+
|
|
21
|
+
Provides CRUD operations for entities.
|
|
22
|
+
Similar to Spring Data JPA's Repository interface.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
@Repository
|
|
26
|
+
class UserRepository(Repository[User]):
|
|
27
|
+
def find_by_email(self, email: str) -> Optional[User]:
|
|
28
|
+
# Custom query method
|
|
29
|
+
pass
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, entity_class: Type[T]):
|
|
33
|
+
"""
|
|
34
|
+
Initialize repository
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
entity_class: The entity class this repository manages
|
|
38
|
+
"""
|
|
39
|
+
self.entity_class = entity_class
|
|
40
|
+
self._gateway_instance = None # Lazy initialization
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def _gateway(self):
|
|
44
|
+
"""Lazy-load the ORM gateway"""
|
|
45
|
+
if self._gateway_instance is None:
|
|
46
|
+
self._gateway_instance = get_orm_gateway()
|
|
47
|
+
return self._gateway_instance
|
|
48
|
+
|
|
49
|
+
async def save(self, entity: T) -> T:
|
|
50
|
+
"""
|
|
51
|
+
Save an entity
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
entity: Entity to save
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Saved entity
|
|
58
|
+
"""
|
|
59
|
+
return await self._gateway.save(entity)
|
|
60
|
+
|
|
61
|
+
async def find_by_id(self, id: Any) -> Optional[T]:
|
|
62
|
+
"""
|
|
63
|
+
Find an entity by ID
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
id: Entity ID
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Entity if found, None otherwise
|
|
70
|
+
"""
|
|
71
|
+
return await self._gateway.find_by_id(self.entity_class, id)
|
|
72
|
+
|
|
73
|
+
async def find_all(self) -> List[T]:
|
|
74
|
+
"""
|
|
75
|
+
Find all entities
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of all entities
|
|
79
|
+
"""
|
|
80
|
+
return await self._gateway.find_all(self.entity_class)
|
|
81
|
+
|
|
82
|
+
async def delete(self, entity: T) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Delete an entity
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
entity: Entity to delete
|
|
88
|
+
"""
|
|
89
|
+
await self._gateway.delete(entity)
|
|
90
|
+
|
|
91
|
+
async def delete_by_id(self, id: Any) -> bool:
|
|
92
|
+
"""
|
|
93
|
+
Delete an entity by ID
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
id: Entity ID
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
True if deleted, False if not found
|
|
100
|
+
"""
|
|
101
|
+
entity = await self.find_by_id(id)
|
|
102
|
+
if entity:
|
|
103
|
+
await self.delete(entity)
|
|
104
|
+
return True
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
async def update(self, entity: T) -> T:
|
|
108
|
+
"""
|
|
109
|
+
Update an entity
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
entity: Entity to update
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Updated entity
|
|
116
|
+
"""
|
|
117
|
+
return await self._gateway.update(entity)
|
|
118
|
+
|
|
119
|
+
async def exists(self, id: Any) -> bool:
|
|
120
|
+
"""
|
|
121
|
+
Check if an entity exists
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
id: Entity ID
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if exists, False otherwise
|
|
128
|
+
"""
|
|
129
|
+
return await self._gateway.exists(self.entity_class, id)
|
|
130
|
+
|
|
131
|
+
async def count(self) -> int:
|
|
132
|
+
"""
|
|
133
|
+
Count all entities
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Number of entities
|
|
137
|
+
"""
|
|
138
|
+
entities = await self.find_all()
|
|
139
|
+
return len(entities)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class CrudRepository(Repository[T]):
|
|
143
|
+
"""
|
|
144
|
+
Alias for Repository
|
|
145
|
+
|
|
146
|
+
Provides Spring Data JPA-style naming.
|
|
147
|
+
"""
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class StarRepository(Repository[T]):
|
|
152
|
+
"""
|
|
153
|
+
StarSpring repository with additional batch operations and automatic query generation
|
|
154
|
+
|
|
155
|
+
Extends the base Repository with:
|
|
156
|
+
- Batch operations for multiple entities
|
|
157
|
+
- Automatic query method execution (findBy*, countBy*, deleteBy*, existsBy*)
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
@Repository
|
|
161
|
+
class UserRepository(StarRepository[User]):
|
|
162
|
+
# These methods are auto-implemented!
|
|
163
|
+
async def findByEmail(self, email: str) -> Optional[User]:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
async def findByActive(self, active: bool) -> List[User]:
|
|
167
|
+
pass
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def __init__(self, entity_class: Type[T] = None):
|
|
171
|
+
"""
|
|
172
|
+
Initialize StarRepository
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
entity_class: Optional entity class. If not provided, will be extracted from generic type.
|
|
176
|
+
"""
|
|
177
|
+
# If entity_class not provided, try to extract from generic type
|
|
178
|
+
if entity_class is None:
|
|
179
|
+
entity_class = self._extract_entity_class()
|
|
180
|
+
|
|
181
|
+
# Call parent constructor
|
|
182
|
+
super().__init__(entity_class)
|
|
183
|
+
|
|
184
|
+
def _extract_entity_class(self) -> Type[T]:
|
|
185
|
+
"""
|
|
186
|
+
Extract entity class from generic type parameter
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Entity class type
|
|
190
|
+
"""
|
|
191
|
+
import typing
|
|
192
|
+
|
|
193
|
+
# Get the class's __orig_bases__ to find the generic parameter
|
|
194
|
+
for base in getattr(self.__class__, '__orig_bases__', []):
|
|
195
|
+
# Check if this is a generic type
|
|
196
|
+
if hasattr(base, '__origin__') and hasattr(base, '__args__'):
|
|
197
|
+
# Get the first type argument (the entity class)
|
|
198
|
+
if base.__args__:
|
|
199
|
+
return base.__args__[0]
|
|
200
|
+
|
|
201
|
+
# Fallback: raise error if we can't determine entity class
|
|
202
|
+
raise ValueError(
|
|
203
|
+
f"Could not determine entity class for {self.__class__.__name__}. "
|
|
204
|
+
f"Please specify it as a generic parameter: class MyRepo(StarRepository[MyEntity])"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def __getattribute__(self, name: str):
|
|
208
|
+
"""
|
|
209
|
+
Magic method to intercept attribute access
|
|
210
|
+
|
|
211
|
+
Automatically implements query methods based on method name.
|
|
212
|
+
"""
|
|
213
|
+
# Get the attribute normally first
|
|
214
|
+
try:
|
|
215
|
+
attr = object.__getattribute__(self, name)
|
|
216
|
+
|
|
217
|
+
# If it's a method and it's defined (not just 'pass'), return it
|
|
218
|
+
if callable(attr) and hasattr(attr, '__func__'):
|
|
219
|
+
# Check if method has actual implementation (more than just 'pass')
|
|
220
|
+
source = inspect.getsource(attr.__func__)
|
|
221
|
+
if 'pass' not in source or len(source.strip().split('\n')) > 3:
|
|
222
|
+
return attr
|
|
223
|
+
except AttributeError:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
# Check if this is a query method that should be auto-implemented
|
|
227
|
+
if name.startswith(('findBy', 'countBy', 'deleteBy', 'existsBy')):
|
|
228
|
+
logger.debug(f"Auto-implementing query method: {name}")
|
|
229
|
+
return self._create_query_method(name)
|
|
230
|
+
|
|
231
|
+
# Default behavior for everything else
|
|
232
|
+
return object.__getattribute__(self, name)
|
|
233
|
+
|
|
234
|
+
def _create_query_method(self, method_name: str):
|
|
235
|
+
"""
|
|
236
|
+
Create a query method dynamically
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
method_name: Name of the method (e.g., 'findByEmail')
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Async function that executes the query
|
|
243
|
+
"""
|
|
244
|
+
from starspring.data.query_builder import QueryMethodParser, SQLQueryGenerator, QueryOperation
|
|
245
|
+
|
|
246
|
+
# Parse the method name
|
|
247
|
+
try:
|
|
248
|
+
parser = QueryMethodParser()
|
|
249
|
+
parsed_query = parser.parse(method_name)
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.error(f"Failed to parse query method '{method_name}': {e}")
|
|
252
|
+
raise ValueError(f"Invalid query method name: {method_name}")
|
|
253
|
+
|
|
254
|
+
# Get table name from entity metadata
|
|
255
|
+
if hasattr(self.entity_class, '_entity_metadata'):
|
|
256
|
+
table_name = self.entity_class._entity_metadata.table_name
|
|
257
|
+
else:
|
|
258
|
+
# Fallback: convert class name to snake_case
|
|
259
|
+
import re
|
|
260
|
+
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', self.entity_class.__name__)
|
|
261
|
+
table_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
|
|
262
|
+
|
|
263
|
+
# Generate SQL
|
|
264
|
+
generator = SQLQueryGenerator(table_name)
|
|
265
|
+
sql, param_names = generator.generate(parsed_query)
|
|
266
|
+
|
|
267
|
+
# Create the async function
|
|
268
|
+
async def query_executor(*args, **kwargs):
|
|
269
|
+
"""Auto-generated query executor"""
|
|
270
|
+
# Build parameter dictionary
|
|
271
|
+
params = {}
|
|
272
|
+
for i, param_name in enumerate(param_names):
|
|
273
|
+
if i < len(args):
|
|
274
|
+
params[param_name] = args[i]
|
|
275
|
+
elif param_name in kwargs:
|
|
276
|
+
params[param_name] = kwargs[param_name]
|
|
277
|
+
|
|
278
|
+
# Execute the query through the gateway
|
|
279
|
+
result = await self._gateway.execute_query(
|
|
280
|
+
sql,
|
|
281
|
+
params,
|
|
282
|
+
self.entity_class,
|
|
283
|
+
parsed_query.operation
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return result
|
|
287
|
+
|
|
288
|
+
return query_executor
|
|
289
|
+
|
|
290
|
+
async def save_all(self, entities: List[T]) -> List[T]:
|
|
291
|
+
"""
|
|
292
|
+
Save multiple entities
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
entities: List of entities to save
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
List of saved entities
|
|
299
|
+
"""
|
|
300
|
+
result = []
|
|
301
|
+
for entity in entities:
|
|
302
|
+
saved = await self.save(entity)
|
|
303
|
+
result.append(saved)
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
async def delete_all(self, entities: List[T]) -> None:
|
|
307
|
+
"""
|
|
308
|
+
Delete multiple entities
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
entities: List of entities to delete
|
|
312
|
+
"""
|
|
313
|
+
for entity in entities:
|
|
314
|
+
await self.delete(entity)
|
|
315
|
+
|
|
316
|
+
async def delete_all_by_id(self, ids: List[Any]) -> None:
|
|
317
|
+
"""
|
|
318
|
+
Delete multiple entities by ID
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
ids: List of entity IDs
|
|
322
|
+
"""
|
|
323
|
+
for id in ids:
|
|
324
|
+
await self.delete_by_id(id)
|