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.
Files changed (37) hide show
  1. api_foundry_query_engine/.pre-commit-config.yaml +22 -0
  2. api_foundry_query_engine/__init__.py +1 -0
  3. api_foundry_query_engine/adapters/adapter.py +73 -0
  4. api_foundry_query_engine/adapters/case_change_adapter.py +79 -0
  5. api_foundry_query_engine/adapters/gateway_adapter.py +191 -0
  6. api_foundry_query_engine/adapters/security_adapter.py +106 -0
  7. api_foundry_query_engine/connectors/connection.py +32 -0
  8. api_foundry_query_engine/connectors/connection_factory.py +115 -0
  9. api_foundry_query_engine/connectors/oracle_connector.py +29 -0
  10. api_foundry_query_engine/connectors/postgres_connection.py +142 -0
  11. api_foundry_query_engine/dao/batch_operation_handler.py +295 -0
  12. api_foundry_query_engine/dao/dao.py +23 -0
  13. api_foundry_query_engine/dao/operation_dao.py +186 -0
  14. api_foundry_query_engine/dao/sql_custom_query_handler.py +68 -0
  15. api_foundry_query_engine/dao/sql_delete_query_handler.py +146 -0
  16. api_foundry_query_engine/dao/sql_insert_query_handler.py +187 -0
  17. api_foundry_query_engine/dao/sql_query_handler.py +713 -0
  18. api_foundry_query_engine/dao/sql_restore_query_handler.py +195 -0
  19. api_foundry_query_engine/dao/sql_select_query_handler.py +433 -0
  20. api_foundry_query_engine/dao/sql_subselect_query_handler.py +66 -0
  21. api_foundry_query_engine/dao/sql_update_query_handler.py +198 -0
  22. api_foundry_query_engine/lambda_handler.py +63 -0
  23. api_foundry_query_engine/operation.py +104 -0
  24. api_foundry_query_engine/services/service.py +75 -0
  25. api_foundry_query_engine/services/transactional_service.py +52 -0
  26. api_foundry_query_engine/utils/api_model.py +380 -0
  27. api_foundry_query_engine/utils/app_exception.py +22 -0
  28. api_foundry_query_engine/utils/claims_check.py +471 -0
  29. api_foundry_query_engine/utils/dependency_resolver.py +157 -0
  30. api_foundry_query_engine/utils/gateway_operation.py +279 -0
  31. api_foundry_query_engine/utils/logger.py +60 -0
  32. api_foundry_query_engine/utils/reference_resolver.py +222 -0
  33. api_foundry_query_engine/utils/token_decoder.py +624 -0
  34. api_foundry_query_engine-0.8.39.dist-info/METADATA +21 -0
  35. api_foundry_query_engine-0.8.39.dist-info/RECORD +37 -0
  36. api_foundry_query_engine-0.8.39.dist-info/WHEEL +4 -0
  37. 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
+ )