api-foundry-query-engine 0.8.39__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.
- api_foundry_query_engine/.pre-commit-config.yaml +22 -0
- api_foundry_query_engine/__init__.py +1 -0
- api_foundry_query_engine/adapters/adapter.py +73 -0
- api_foundry_query_engine/adapters/case_change_adapter.py +79 -0
- api_foundry_query_engine/adapters/gateway_adapter.py +191 -0
- api_foundry_query_engine/adapters/security_adapter.py +106 -0
- api_foundry_query_engine/connectors/connection.py +32 -0
- api_foundry_query_engine/connectors/connection_factory.py +115 -0
- api_foundry_query_engine/connectors/oracle_connector.py +29 -0
- api_foundry_query_engine/connectors/postgres_connection.py +142 -0
- api_foundry_query_engine/dao/batch_operation_handler.py +295 -0
- api_foundry_query_engine/dao/dao.py +23 -0
- api_foundry_query_engine/dao/operation_dao.py +186 -0
- api_foundry_query_engine/dao/sql_custom_query_handler.py +68 -0
- api_foundry_query_engine/dao/sql_delete_query_handler.py +146 -0
- api_foundry_query_engine/dao/sql_insert_query_handler.py +187 -0
- api_foundry_query_engine/dao/sql_query_handler.py +713 -0
- api_foundry_query_engine/dao/sql_restore_query_handler.py +195 -0
- api_foundry_query_engine/dao/sql_select_query_handler.py +433 -0
- api_foundry_query_engine/dao/sql_subselect_query_handler.py +66 -0
- api_foundry_query_engine/dao/sql_update_query_handler.py +198 -0
- api_foundry_query_engine/lambda_handler.py +63 -0
- api_foundry_query_engine/operation.py +104 -0
- api_foundry_query_engine/services/service.py +75 -0
- api_foundry_query_engine/services/transactional_service.py +52 -0
- api_foundry_query_engine/utils/api_model.py +380 -0
- api_foundry_query_engine/utils/app_exception.py +22 -0
- api_foundry_query_engine/utils/claims_check.py +471 -0
- api_foundry_query_engine/utils/dependency_resolver.py +157 -0
- api_foundry_query_engine/utils/gateway_operation.py +279 -0
- api_foundry_query_engine/utils/logger.py +60 -0
- api_foundry_query_engine/utils/reference_resolver.py +222 -0
- api_foundry_query_engine/utils/token_decoder.py +624 -0
- api_foundry_query_engine-0.8.39.dist-info/METADATA +21 -0
- api_foundry_query_engine-0.8.39.dist-info/RECORD +37 -0
- api_foundry_query_engine-0.8.39.dist-info/WHEEL +4 -0
- api_foundry_query_engine-0.8.39.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import yaml
|
|
3
|
+
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Dict, List, Mapping, Optional
|
|
6
|
+
|
|
7
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
8
|
+
|
|
9
|
+
log = logger(__name__)
|
|
10
|
+
|
|
11
|
+
api_model = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_schema_object(name: str) -> Optional["SchemaObject"]:
|
|
15
|
+
global api_model
|
|
16
|
+
if api_model is None:
|
|
17
|
+
return None
|
|
18
|
+
return api_model.schema_objects.get(name)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_path_operation(path: str, method: str) -> Optional["PathOperation"]:
|
|
22
|
+
global api_model
|
|
23
|
+
if api_model is None:
|
|
24
|
+
return None
|
|
25
|
+
return api_model.path_operations.get(f"{path}_{method}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SchemaObjectProperty:
|
|
29
|
+
"""Represents a property of a schema object."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, data: Dict[str, Any]):
|
|
32
|
+
self.api_name = data.get("api_name")
|
|
33
|
+
self.column_name = data.get("column_name")
|
|
34
|
+
self.type = data.get("type")
|
|
35
|
+
self.api_type = data.get("api_type")
|
|
36
|
+
self.column_type = data.get("column_type")
|
|
37
|
+
self.required = data.get("required", False)
|
|
38
|
+
self.min_length = data.get("min_length")
|
|
39
|
+
self.max_length = data.get("max_length")
|
|
40
|
+
self.pattern = data.get("pattern")
|
|
41
|
+
self.default = data.get("default")
|
|
42
|
+
self.key_type = data.get("key_type")
|
|
43
|
+
self.sequence_name = data.get("sequence_name")
|
|
44
|
+
self.concurrency_control = data.get("concurrency_control")
|
|
45
|
+
self.inject_value = data.get("inject_value")
|
|
46
|
+
self.inject_on = data.get("inject_on", [])
|
|
47
|
+
self.soft_delete = data.get("x-af-soft-delete") or data.get("soft_delete")
|
|
48
|
+
|
|
49
|
+
def __repr__(self):
|
|
50
|
+
return f"SchemaObjectProperty(api_name={self.api_name}, column_name={self.column_name}, type={self.type})"
|
|
51
|
+
|
|
52
|
+
def convert_to_db_value(self, value) -> Optional[Any]:
|
|
53
|
+
if value is None:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
column_type = self.column_type if self.column_type is not None else "string"
|
|
57
|
+
|
|
58
|
+
# Handle string types
|
|
59
|
+
if column_type in ["string", "varchar", "char", "text", "uuid"]:
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
# Handle numeric types - float/double/numeric variations
|
|
63
|
+
elif column_type in ["number", "float", "double", "numeric", "decimal", "real"]:
|
|
64
|
+
return float(value)
|
|
65
|
+
|
|
66
|
+
# Handle boolean types - can map to boolean or integer columns
|
|
67
|
+
elif column_type == "boolean":
|
|
68
|
+
if isinstance(value, bool):
|
|
69
|
+
return value
|
|
70
|
+
return str(value).lower() == "true"
|
|
71
|
+
elif (
|
|
72
|
+
column_type in ["int", "integer", "smallint", "bigint"]
|
|
73
|
+
and hasattr(self, "api_type")
|
|
74
|
+
and self.api_type == "boolean"
|
|
75
|
+
):
|
|
76
|
+
# Boolean API type mapping to integer column type
|
|
77
|
+
if isinstance(value, bool):
|
|
78
|
+
return 1 if value else 0
|
|
79
|
+
return 1 if str(value).lower() == "true" else 0
|
|
80
|
+
|
|
81
|
+
# Handle integer types (after boolean check to avoid conflicts)
|
|
82
|
+
elif column_type in [
|
|
83
|
+
"integer",
|
|
84
|
+
"int",
|
|
85
|
+
"bigint",
|
|
86
|
+
"smallint",
|
|
87
|
+
"serial",
|
|
88
|
+
"bigserial",
|
|
89
|
+
]:
|
|
90
|
+
return int(value)
|
|
91
|
+
|
|
92
|
+
# Handle date types
|
|
93
|
+
elif column_type == "date":
|
|
94
|
+
return datetime.strptime(value, "%Y-%m-%d").date() if value else None
|
|
95
|
+
|
|
96
|
+
# Handle datetime types - various column type names
|
|
97
|
+
elif column_type in ["date-time", "datetime", "timestamp", "timestamptz"]:
|
|
98
|
+
return datetime.fromisoformat(value) if value else None
|
|
99
|
+
|
|
100
|
+
# Handle time types
|
|
101
|
+
elif column_type in ["time", "timetz"]:
|
|
102
|
+
return datetime.strptime(value, "%H:%M:%S").time() if value else None
|
|
103
|
+
|
|
104
|
+
# Default to string conversion for unknown types
|
|
105
|
+
else:
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
def is_soft_delete_field(self) -> bool:
|
|
109
|
+
"""Check if this property is configured for soft delete."""
|
|
110
|
+
return self.soft_delete is not None
|
|
111
|
+
|
|
112
|
+
def get_soft_delete_strategy(self) -> str:
|
|
113
|
+
"""Get the soft delete strategy for this property."""
|
|
114
|
+
if not self.soft_delete:
|
|
115
|
+
return "none"
|
|
116
|
+
return self.soft_delete.get("strategy", "none")
|
|
117
|
+
|
|
118
|
+
def get_soft_delete_config(self) -> Dict[str, Any]:
|
|
119
|
+
"""Get the complete soft delete configuration."""
|
|
120
|
+
return self.soft_delete or {}
|
|
121
|
+
|
|
122
|
+
def convert_to_api_value(self, value) -> Optional[Any]:
|
|
123
|
+
if value is None:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
api_type = self.api_type if self.api_type is not None else "string"
|
|
127
|
+
|
|
128
|
+
# Handle string types (including UUID which is represented as string in API)
|
|
129
|
+
if api_type in ["string", "uuid"]:
|
|
130
|
+
return str(value) if value is not None else None
|
|
131
|
+
|
|
132
|
+
# Handle integer type
|
|
133
|
+
elif api_type == "integer":
|
|
134
|
+
return int(value) if value is not None else None
|
|
135
|
+
|
|
136
|
+
# Handle number and float types
|
|
137
|
+
elif api_type in ["number", "float"]:
|
|
138
|
+
return float(value) if value is not None else None
|
|
139
|
+
|
|
140
|
+
# Handle boolean type - convert any value to string representation
|
|
141
|
+
elif api_type == "boolean":
|
|
142
|
+
if isinstance(value, bool):
|
|
143
|
+
return str(value)
|
|
144
|
+
elif isinstance(value, int):
|
|
145
|
+
# Handle boolean stored as integer (0/1)
|
|
146
|
+
return "true" if value != 0 else "false"
|
|
147
|
+
elif isinstance(value, str):
|
|
148
|
+
return value.lower() in ["true", "1", "yes", "on"]
|
|
149
|
+
else:
|
|
150
|
+
return str(bool(value))
|
|
151
|
+
|
|
152
|
+
# Handle date type
|
|
153
|
+
elif api_type == "date":
|
|
154
|
+
if hasattr(value, "date"):
|
|
155
|
+
# If it's a datetime, extract the date part
|
|
156
|
+
return value.date().isoformat()
|
|
157
|
+
elif hasattr(value, "isoformat"):
|
|
158
|
+
# If it's already a date
|
|
159
|
+
return value.isoformat()
|
|
160
|
+
else:
|
|
161
|
+
return str(value)
|
|
162
|
+
|
|
163
|
+
# Handle datetime type
|
|
164
|
+
elif api_type == "date-time":
|
|
165
|
+
if hasattr(value, "isoformat"):
|
|
166
|
+
return value.isoformat()
|
|
167
|
+
else:
|
|
168
|
+
return str(value)
|
|
169
|
+
|
|
170
|
+
# Handle time type
|
|
171
|
+
elif api_type == "time":
|
|
172
|
+
if hasattr(value, "time"):
|
|
173
|
+
# If it's a datetime, extract the time part
|
|
174
|
+
return value.time().isoformat()
|
|
175
|
+
elif hasattr(value, "isoformat"):
|
|
176
|
+
# If it's already a time
|
|
177
|
+
return value.isoformat()
|
|
178
|
+
else:
|
|
179
|
+
return str(value)
|
|
180
|
+
|
|
181
|
+
# Default to string conversion for unknown types
|
|
182
|
+
else:
|
|
183
|
+
return str(value) if value is not None else None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class SchemaObjectAssociation:
|
|
187
|
+
"""Represents an association (relationship) between schema objects."""
|
|
188
|
+
|
|
189
|
+
def __init__(self, parent_schema: str, data: Dict[str, Any]):
|
|
190
|
+
self.parent_schema = parent_schema
|
|
191
|
+
self.schema_name = data.get("schema_name")
|
|
192
|
+
self.api_name = data.get("api_name")
|
|
193
|
+
self.type = data.get("type")
|
|
194
|
+
self._child_property = data.get("child_property")
|
|
195
|
+
self._parent_property = data.get("parent_property")
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def child_property(self) -> str:
|
|
199
|
+
if self._child_property:
|
|
200
|
+
return self._child_property
|
|
201
|
+
if not self.schema_name:
|
|
202
|
+
raise ValueError("schema_name is None in SchemaObjectAssociation")
|
|
203
|
+
child_schema = get_schema_object(self.schema_name)
|
|
204
|
+
if not child_schema:
|
|
205
|
+
raise ValueError(f"SchemaObject '{self.schema_name}' not found")
|
|
206
|
+
if not child_schema.primary_key:
|
|
207
|
+
raise ValueError(f"Primary key not defined for schema '{self.schema_name}'")
|
|
208
|
+
column_name = getattr(child_schema.primary_key, "column_name", None)
|
|
209
|
+
if column_name is None:
|
|
210
|
+
raise ValueError(
|
|
211
|
+
f"Primary key property does not have 'column_name' for schema '{self.schema_name}'"
|
|
212
|
+
)
|
|
213
|
+
return column_name
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def parent_property(self) -> str:
|
|
217
|
+
if self._parent_property:
|
|
218
|
+
return self._parent_property
|
|
219
|
+
parent_schema_obj = get_schema_object(self.parent_schema)
|
|
220
|
+
if not parent_schema_obj:
|
|
221
|
+
raise ValueError(f"SchemaObject '{self.parent_schema}' not found")
|
|
222
|
+
if not parent_schema_obj.primary_key:
|
|
223
|
+
raise ValueError(
|
|
224
|
+
f"Primary key not defined for schema '{self.parent_schema}'"
|
|
225
|
+
)
|
|
226
|
+
column_name = getattr(parent_schema_obj.primary_key, "column_name", None)
|
|
227
|
+
if column_name is None:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"Primary key property does not have 'column_name' for schema '{self.parent_schema}'"
|
|
230
|
+
)
|
|
231
|
+
return column_name
|
|
232
|
+
|
|
233
|
+
def __repr__(self):
|
|
234
|
+
return (
|
|
235
|
+
f"SchemaObjectAssociation(name={self.api_name}, "
|
|
236
|
+
+ f"child_property={self._child_property}, "
|
|
237
|
+
+ f"parent_property={self.parent_property})"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def child_schema_object(self) -> "SchemaObject":
|
|
242
|
+
if self.schema_name is None:
|
|
243
|
+
raise ValueError("schema_name is None in SchemaObjectAssociation")
|
|
244
|
+
schema_obj = get_schema_object(self.schema_name)
|
|
245
|
+
if schema_obj is None:
|
|
246
|
+
raise ValueError(f"SchemaObject '{self.schema_name}' not found")
|
|
247
|
+
return schema_obj
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class SchemaObject:
|
|
251
|
+
"""Represents a schema object in the API configuration."""
|
|
252
|
+
|
|
253
|
+
def __init__(self, data: Dict[str, Any]):
|
|
254
|
+
self.api_name: str = str(data.get("api_name"))
|
|
255
|
+
self.database: str = str(data.get("database"))
|
|
256
|
+
self.schema: Optional[str] = data.get("schema")
|
|
257
|
+
self.table_name: str = str(data.get("table_name"))
|
|
258
|
+
self.qualified_name: str = (
|
|
259
|
+
f"{self.schema}.{self.table_name}" if self.schema else self.table_name
|
|
260
|
+
)
|
|
261
|
+
self.properties: Dict[str, SchemaObjectProperty] = {
|
|
262
|
+
name: SchemaObjectProperty(prop_data)
|
|
263
|
+
for name, prop_data in data.get("properties", {}).items()
|
|
264
|
+
}
|
|
265
|
+
self.relations = {
|
|
266
|
+
name: SchemaObjectAssociation(
|
|
267
|
+
self.api_name if self.api_name is not None else "", assoc_data
|
|
268
|
+
)
|
|
269
|
+
for name, assoc_data in data.get("relations", {}).items()
|
|
270
|
+
}
|
|
271
|
+
self.concurrency_property = (
|
|
272
|
+
self.properties[str(data.get("concurrency_property"))]
|
|
273
|
+
if data.get("concurrency_property")
|
|
274
|
+
else None
|
|
275
|
+
)
|
|
276
|
+
self._primary_key: str = str(data.get("primary_key"))
|
|
277
|
+
self.permissions = data.get("permissions")
|
|
278
|
+
|
|
279
|
+
def __repr__(self):
|
|
280
|
+
return f"SchemaObject(table_name={self.table_name}, primary_key={self.primary_key})"
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def primary_key(self):
|
|
284
|
+
return self.properties.get(self._primary_key)
|
|
285
|
+
|
|
286
|
+
def has_soft_delete_support(self) -> bool:
|
|
287
|
+
"""Check if this schema object supports soft delete operations."""
|
|
288
|
+
return len(self.get_soft_delete_properties()) > 0
|
|
289
|
+
|
|
290
|
+
def get_soft_delete_properties(self) -> Dict[str, SchemaObjectProperty]:
|
|
291
|
+
"""Get all properties configured for soft delete filtering."""
|
|
292
|
+
return {
|
|
293
|
+
name: prop
|
|
294
|
+
for name, prop in self.properties.items()
|
|
295
|
+
if prop.is_soft_delete_field()
|
|
296
|
+
and prop.get_soft_delete_strategy()
|
|
297
|
+
in ["null_check", "boolean_flag", "exclude_values"]
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
def get_soft_delete_audit_properties(
|
|
301
|
+
self,
|
|
302
|
+
) -> Dict[str, SchemaObjectProperty]:
|
|
303
|
+
"""Get properties used for soft delete audit trails."""
|
|
304
|
+
return {
|
|
305
|
+
name: prop
|
|
306
|
+
for name, prop in self.properties.items()
|
|
307
|
+
if prop.is_soft_delete_field()
|
|
308
|
+
and prop.get_soft_delete_strategy() == "audit_field"
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
def get_soft_delete_strategies(self) -> List[str]:
|
|
312
|
+
"""Get list of all soft delete strategies used in this schema."""
|
|
313
|
+
strategies = set()
|
|
314
|
+
for prop in self.properties.values():
|
|
315
|
+
if prop.is_soft_delete_field():
|
|
316
|
+
strategies.add(prop.get_soft_delete_strategy())
|
|
317
|
+
return list(strategies)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class PathOperation:
|
|
321
|
+
"""Represents a path operation in the API configuration."""
|
|
322
|
+
|
|
323
|
+
def __init__(self, data: Dict[str, Any]):
|
|
324
|
+
self.entity: str = data["entity"]
|
|
325
|
+
self.action: str = data["action"]
|
|
326
|
+
self.sql: str = data["sql"]
|
|
327
|
+
self.database: str = data["database"]
|
|
328
|
+
self.inputs: Dict[str, SchemaObjectProperty] = {
|
|
329
|
+
name: SchemaObjectProperty(input_data)
|
|
330
|
+
for name, input_data in data.get("inputs", {}).items()
|
|
331
|
+
}
|
|
332
|
+
self.outputs: Dict[str, SchemaObjectProperty] = {
|
|
333
|
+
name: SchemaObjectProperty(output_data)
|
|
334
|
+
for name, output_data in data.get("outputs", {}).items()
|
|
335
|
+
}
|
|
336
|
+
self.permissions = data.get("security")
|
|
337
|
+
|
|
338
|
+
def __repr__(self):
|
|
339
|
+
return f"PathOperation(entity={self.entity}, action={self.action})"
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class APIModel:
|
|
343
|
+
"""Class to load and expose the API configuration as objects."""
|
|
344
|
+
|
|
345
|
+
def __init__(self, config: Dict[str, Any]):
|
|
346
|
+
log.info("building api_model")
|
|
347
|
+
self.schema_objects = {
|
|
348
|
+
name: SchemaObject(schema_data)
|
|
349
|
+
for name, schema_data in config.get("schema_objects", {}).items()
|
|
350
|
+
}
|
|
351
|
+
self.path_operations = {
|
|
352
|
+
name: PathOperation(path_data)
|
|
353
|
+
for name, path_data in config.get("path_operations", {}).items()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
def get_path_operation(self, path: str, method: str) -> Optional[PathOperation]:
|
|
357
|
+
"""Returns a path operation by name."""
|
|
358
|
+
if self.path_operations is None:
|
|
359
|
+
return None
|
|
360
|
+
return self.path_operations.get(f"{path}_{method}")
|
|
361
|
+
|
|
362
|
+
def __repr__(self):
|
|
363
|
+
return (
|
|
364
|
+
f"APIModel(schema_objects={list(self.schema_objects.keys())}, "
|
|
365
|
+
+ f"path_operations={list(self.path_operations.keys())})"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def set_api_model(engine_config: Mapping[str, str]):
|
|
370
|
+
global api_model
|
|
371
|
+
if api_model is None:
|
|
372
|
+
if engine_config.get("API_SPEC"):
|
|
373
|
+
api_model = APIModel(yaml.safe_load(engine_config["API_SPEC"]))
|
|
374
|
+
else:
|
|
375
|
+
log.info("Loading API model from file")
|
|
376
|
+
with open(
|
|
377
|
+
os.environ.get("API_SPEC", "/var/task/api_spec.yaml"), "r"
|
|
378
|
+
) as file:
|
|
379
|
+
api_model = APIModel(yaml.safe_load(file))
|
|
380
|
+
log.info("Loaded API model: %s", api_model)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class ApplicationException(Exception):
|
|
2
|
+
"""Custom exception class for application errors."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, status_code: int, message: str):
|
|
5
|
+
"""
|
|
6
|
+
Initialize the ApplicationException.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
- status_code (int): The HTTP status code associated with the
|
|
10
|
+
exception.
|
|
11
|
+
- message (str): The error message.
|
|
12
|
+
"""
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
self.status_code = status_code
|
|
15
|
+
self.message = message
|
|
16
|
+
|
|
17
|
+
def __str__(self):
|
|
18
|
+
"""Return a string representation of the exception."""
|
|
19
|
+
return (
|
|
20
|
+
f"ApplicationException(status_code={self.status_code}, "
|
|
21
|
+
+ f"message='{self.message}')"
|
|
22
|
+
)
|