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,279 @@
1
+ """
2
+ Gateway Operation Decorator for Lambda Functions
3
+
4
+ This decorator handles the marshalling and unmarshalling of API Gateway events
5
+ into Operation objects, similar to what GatewayAdapter does but as a decorator.
6
+ It extracts the operation details and adds them to the event for downstream processing.
7
+ """
8
+
9
+ import functools
10
+ import json
11
+ import logging
12
+ from typing import Any, Dict, List, Optional, Tuple, Callable
13
+
14
+ from api_foundry_query_engine.operation import Operation
15
+ from api_foundry_query_engine.utils.app_exception import ApplicationException
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+
20
+ def gateway_operation(validate_scopes: bool = True, auto_marshal_response: bool = True):
21
+ """
22
+ Decorator to handle API Gateway event marshalling and unmarshalling.
23
+
24
+ This decorator:
25
+ 1. Unmarshals API Gateway events into Operation objects
26
+ 2. Adds the Operation to the event for downstream processing
27
+ 3. Validates OAuth scopes based on the operation
28
+ 4. Marshals response data back to API Gateway format
29
+
30
+ Args:
31
+ validate_scopes: Whether to validate OAuth scopes (default: True)
32
+ auto_marshal_response: Whether to automatically format response (default: True)
33
+
34
+ Returns:
35
+ Decorated function that processes API Gateway events
36
+
37
+ Example:
38
+ @token_decoder()
39
+ @claims_check()
40
+ @gateway_operation()
41
+ def handler(event, context):
42
+ # event now contains 'operation' key with Operation object
43
+ operation = event['operation']
44
+ return {"data": [{"id": 1, "name": "Test"}]}
45
+ """
46
+
47
+ def decorator(func: Callable) -> Callable:
48
+ @functools.wraps(func)
49
+ def wrapper(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
50
+ try:
51
+ log.debug("Gateway operation decorator starting")
52
+
53
+ # Unmarshal API Gateway event into Operation
54
+ operation = _unmarshal_gateway_event(event, validate_scopes)
55
+
56
+ # Add operation to event for downstream processing
57
+ event["operation"] = operation
58
+
59
+ log.debug(
60
+ "Created operation: entity=%s, action=%s",
61
+ operation.entity,
62
+ operation.action,
63
+ )
64
+
65
+ # Execute the original function
66
+ result = func(event, context)
67
+
68
+ # Auto-marshal response if requested
69
+ if auto_marshal_response and isinstance(result, (list, dict)):
70
+ return _marshal_response(result)
71
+ else:
72
+ return result
73
+
74
+ except ApplicationException:
75
+ raise
76
+ except Exception as e:
77
+ log.error("Gateway operation error: %s", str(e))
78
+ raise ApplicationException(
79
+ status_code=500,
80
+ message="Internal server error during operation processing",
81
+ )
82
+
83
+ return wrapper
84
+
85
+ return decorator
86
+
87
+
88
+ def _unmarshal_gateway_event(event: Dict[str, Any], validate_scopes: bool) -> Operation:
89
+ """
90
+ Unmarshal API Gateway event into Operation object.
91
+
92
+ This replicates the logic from GatewayAdapter.unmarshal() method.
93
+ """
94
+ # Extract entity from resource path
95
+ resource = event.get("resource")
96
+ if resource is not None and "/" in resource:
97
+ parts = resource.split("/")
98
+ entity = parts[1] if len(parts) > 1 else None
99
+ else:
100
+ entity = None
101
+
102
+ # Map HTTP method to action
103
+ method = str(event.get("httpMethod", "")).upper()
104
+ actions_map = {
105
+ "GET": "read",
106
+ "POST": "create",
107
+ "PUT": "update",
108
+ "DELETE": "delete",
109
+ }
110
+ action = actions_map.get(method, "read")
111
+
112
+ # Collect parameters from path and query string
113
+ event_params = {}
114
+
115
+ path_parameters = _convert_parameters(event.get("pathParameters"))
116
+ if path_parameters is not None:
117
+ event_params.update(path_parameters)
118
+
119
+ query_string_parameters = _convert_parameters(event.get("queryStringParameters"))
120
+ if query_string_parameters is not None:
121
+ event_params.update(query_string_parameters)
122
+
123
+ query_params, metadata_params = _split_params(event_params)
124
+
125
+ # Extract store params from request body
126
+ store_params = {}
127
+ body = event.get("body")
128
+ if body is not None and len(body) > 0:
129
+ store_params = json.loads(body)
130
+
131
+ # Extract claims from authorizer context
132
+ authorizer_info = event.get("requestContext", {}).get("authorizer", {})
133
+ claims = authorizer_info.get("claims", {})
134
+ scope_str = claims.get("scope", "")
135
+
136
+ # Validate OAuth scopes if requested
137
+ if validate_scopes and entity and scope_str:
138
+ _validate_oauth_scopes(method, entity, scope_str)
139
+
140
+ return Operation(
141
+ entity=entity,
142
+ action=action,
143
+ store_params=store_params,
144
+ query_params=query_params,
145
+ metadata_params=metadata_params,
146
+ claims=claims,
147
+ )
148
+
149
+
150
+ def _decode_json_array(raw_value: Any) -> List[Any]:
151
+ """Decode JSON-encoded arrays from OAuth context."""
152
+ if isinstance(raw_value, str):
153
+ try:
154
+ return json.loads(raw_value)
155
+ except (json.JSONDecodeError, TypeError):
156
+ return []
157
+ else:
158
+ return raw_value if isinstance(raw_value, list) else []
159
+
160
+
161
+ def _validate_oauth_scopes(method: str, entity: str, scope_str: str) -> None:
162
+ """
163
+ Validate OAuth scopes based on the operation.
164
+
165
+ Enforces scope pattern: read|write|delete:<entity>
166
+ """
167
+ required_action = {
168
+ "GET": "read",
169
+ "POST": "write",
170
+ "PUT": "write",
171
+ "PATCH": "write",
172
+ "DELETE": "delete",
173
+ }.get(method, "read")
174
+
175
+ required_scope = f"{required_action}:{entity}"
176
+ token_scopes = set(str(scope_str).split())
177
+
178
+ def _has_scope(required: str) -> bool:
179
+ return (
180
+ required in token_scopes
181
+ or f"{required_action}:*" in token_scopes
182
+ or "*" in token_scopes
183
+ or "*:*" in token_scopes
184
+ )
185
+
186
+ if not _has_scope(required_scope):
187
+ raise ApplicationException(
188
+ 401, f"insufficient_scope: required_scope={required_scope}"
189
+ )
190
+
191
+
192
+ def _convert_parameters(
193
+ parameters: Optional[Dict[str, Any]]
194
+ ) -> Optional[Dict[str, Any]]:
195
+ """
196
+ Convert parameters to appropriate types.
197
+
198
+ Tries to convert strings to int, then float, falls back to string.
199
+ """
200
+ if parameters is None:
201
+ return None
202
+
203
+ result = {}
204
+ for parameter, value in parameters.items():
205
+ try:
206
+ result[parameter] = int(value)
207
+ except ValueError:
208
+ try:
209
+ result[parameter] = float(value)
210
+ except ValueError:
211
+ result[parameter] = value
212
+ return result
213
+
214
+
215
+ def _split_params(parameters: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
216
+ """
217
+ Split parameters into query_params and metadata_params.
218
+
219
+ Metadata parameters start with '__'.
220
+ """
221
+ query_params = {}
222
+ metadata_params = {}
223
+
224
+ for key, value in parameters.items():
225
+ if key.startswith("__"):
226
+ metadata_params[key] = value
227
+ else:
228
+ query_params[key] = value
229
+
230
+ return query_params, metadata_params
231
+
232
+
233
+ def _marshal_response(result: Any) -> Dict[str, Any]:
234
+ """
235
+ Marshal response data into API Gateway format.
236
+
237
+ Args:
238
+ result: The response data (list or dict)
239
+
240
+ Returns:
241
+ API Gateway compatible response
242
+ """
243
+ try:
244
+ return {
245
+ "isBase64Encoded": False,
246
+ "statusCode": 200,
247
+ "headers": {"Content-Type": "application/json"},
248
+ "body": json.dumps(result) if result is not None else json.dumps([]),
249
+ }
250
+ except (TypeError, ValueError) as e:
251
+ log.error("Failed to marshal response: %s", e)
252
+ raise ApplicationException(
253
+ status_code=500, message="Failed to serialize response data"
254
+ )
255
+
256
+
257
+ # Convenience decorators for specific use cases
258
+ def gateway_read_operation(validate_scopes: bool = True):
259
+ """Convenience decorator for read operations."""
260
+ return gateway_operation(
261
+ validate_scopes=validate_scopes, auto_marshal_response=True
262
+ )
263
+
264
+
265
+ def gateway_write_operation(validate_scopes: bool = True):
266
+ """Convenience decorator for write operations."""
267
+ return gateway_operation(
268
+ validate_scopes=validate_scopes, auto_marshal_response=True
269
+ )
270
+
271
+
272
+ def gateway_operation_no_validation():
273
+ """Decorator that skips scope validation."""
274
+ return gateway_operation(validate_scopes=False, auto_marshal_response=True)
275
+
276
+
277
+ def gateway_operation_raw_response():
278
+ """Decorator that doesn't auto-marshal the response."""
279
+ return gateway_operation(validate_scopes=True, auto_marshal_response=False)
@@ -0,0 +1,60 @@
1
+ import logging
2
+ import os
3
+
4
+ # Configuring the logging module with basic settings, including format and log level,
5
+ # where the log level is obtained from the environment variable LOGGING_LEVEL
6
+ # with a default of DEBUG, and force=True to ensure the configuration is applied immediately.
7
+ logging.basicConfig(
8
+ format="%(name)s:%(lineno)s - %(levelname)s - %(message)s",
9
+ level=os.getenv("LOGGING_LEVEL", "DEBUG").upper(),
10
+ force=True,
11
+ )
12
+
13
+ WARN = logging.WARN
14
+ INFO = logging.INFO
15
+ DEBUG = logging.DEBUG
16
+
17
+
18
+ def logger(name=None):
19
+ """
20
+ Function to create a logger with a specified name or default name.
21
+
22
+ Parameters:
23
+ name (str): Name of the logger. If not provided, the root logger is returned.
24
+
25
+ Returns:
26
+ logging.Logger: Logger object with the specified name or the root logger.
27
+
28
+ """
29
+ # Retrieving the logging level from the environment variable LOGGING_LEVEL
30
+ # with a default of DEBUG, and converting it to uppercase
31
+ loggingLevel = os.getenv("LOGGING_LEVEL", "DEBUG").upper()
32
+
33
+ # Setting the logging level for the root logger to the obtained logging level
34
+ logging.getLogger().setLevel(loggingLevel)
35
+
36
+ # Returning a logger object with the specified name or the root logger
37
+ return logging.getLogger(name)
38
+
39
+
40
+ def write_logging_file(file_name, content):
41
+ """
42
+ Function to write a given string to a file in the temp/logging folder.
43
+
44
+ Parameters:
45
+ file_name (str): Name of the file to write the content to.
46
+ content (str): The string content to write to the file.
47
+
48
+ """
49
+ # Define the directory path
50
+ dir_path = os.path.join("temp", "logging")
51
+
52
+ # Ensure the directory exists
53
+ os.makedirs(dir_path, exist_ok=True)
54
+
55
+ # Define the file path
56
+ file_path = os.path.join(dir_path, file_name)
57
+
58
+ # Write the content to the file
59
+ with open(file_path, "w") as file:
60
+ file.write(content + "\n")
@@ -0,0 +1,222 @@
1
+ """
2
+ Reference Resolver for Batch Operations
3
+
4
+ Resolves $ref: placeholders in operation parameters with values from
5
+ previously executed operations.
6
+ """
7
+
8
+ import re
9
+ from typing import Dict, Any, List
10
+ from api_foundry_query_engine.utils.app_exception import ApplicationException
11
+ from api_foundry_query_engine.utils.logger import logger
12
+
13
+ log = logger(__name__)
14
+
15
+
16
+ class ReferenceResolver:
17
+ """Resolves $ref: references to values from completed operations."""
18
+
19
+ REF_PATTERN = re.compile(r"\$ref:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_.]+)")
20
+
21
+ def __init__(self, results: Dict[str, Dict[str, Any]]):
22
+ """
23
+ Initialize the reference resolver.
24
+
25
+ Args:
26
+ results: Dictionary mapping operation IDs to their results
27
+ """
28
+ self.results = results
29
+
30
+ def resolve_parameters(
31
+ self, params: Dict[str, Any], operation_id: str = None
32
+ ) -> Dict[str, Any]:
33
+ """
34
+ Resolve all $ref: references in parameters.
35
+
36
+ Args:
37
+ params: Parameter dictionary potentially containing references
38
+ operation_id: ID of the operation being resolved (for logging)
39
+
40
+ Returns:
41
+ Parameters with all references replaced with actual values
42
+
43
+ Raises:
44
+ ApplicationException: If reference cannot be resolved
45
+ """
46
+ if not params:
47
+ return params
48
+
49
+ resolved = {}
50
+ for key, value in params.items():
51
+ resolved[key] = self._resolve_value(value, key, operation_id)
52
+
53
+ return resolved
54
+
55
+ def _resolve_value(
56
+ self, value: Any, param_name: str = None, operation_id: str = None
57
+ ) -> Any:
58
+ """
59
+ Recursively resolve a parameter value.
60
+
61
+ Args:
62
+ value: The value to resolve
63
+ param_name: Name of the parameter (for error messages)
64
+ operation_id: ID of the operation (for error messages)
65
+
66
+ Returns:
67
+ Resolved value
68
+ """
69
+ if isinstance(value, str):
70
+ return self._resolve_string_value(value, param_name, operation_id)
71
+ elif isinstance(value, dict):
72
+ return {
73
+ k: self._resolve_value(v, f"{param_name}.{k}", operation_id)
74
+ for k, v in value.items()
75
+ }
76
+ elif isinstance(value, list):
77
+ return [
78
+ self._resolve_value(item, f"{param_name}[{i}]", operation_id)
79
+ for i, item in enumerate(value)
80
+ ]
81
+ else:
82
+ return value
83
+
84
+ def _resolve_string_value(
85
+ self, value: str, param_name: str = None, operation_id: str = None
86
+ ) -> Any:
87
+ """
88
+ Resolve a string value that may contain $ref: references.
89
+
90
+ Supports:
91
+ - Full replacement: "$ref:op1.customer_id" → 42
92
+ - Partial replacement: "prefix_$ref:op1.id_suffix" → "prefix_42_suffix"
93
+
94
+ Args:
95
+ value: String value to resolve
96
+ param_name: Name of the parameter (for error messages)
97
+ operation_id: ID of the operation (for error messages)
98
+
99
+ Returns:
100
+ Resolved value (may not be string if full replacement)
101
+ """
102
+ if not isinstance(value, str) or "$ref:" not in value:
103
+ return value
104
+
105
+ matches = list(self.REF_PATTERN.finditer(value))
106
+
107
+ # If entire string is a single reference, return the actual type
108
+ if len(matches) == 1 and matches[0].group(0) == value:
109
+ return self._extract_reference(
110
+ matches[0].group(1), matches[0].group(2), param_name, operation_id
111
+ )
112
+
113
+ # Otherwise, perform string substitution
114
+ result = value
115
+ for match in matches:
116
+ ref_op_id = match.group(1)
117
+ ref_path = match.group(2)
118
+ ref_value = self._extract_reference(
119
+ ref_op_id, ref_path, param_name, operation_id
120
+ )
121
+ result = result.replace(match.group(0), str(ref_value))
122
+
123
+ return result
124
+
125
+ def _extract_reference(
126
+ self,
127
+ ref_op_id: str,
128
+ ref_path: str,
129
+ param_name: str = None,
130
+ operation_id: str = None,
131
+ ) -> Any:
132
+ """
133
+ Extract a value from results using operation ID and property path.
134
+
135
+ Args:
136
+ ref_op_id: Referenced operation ID
137
+ ref_path: Dot-notation path to property (e.g., "customer_id")
138
+ param_name: Name of the parameter (for error messages)
139
+ operation_id: ID of the operation (for error messages)
140
+
141
+ Returns:
142
+ The referenced value
143
+
144
+ Raises:
145
+ ApplicationException: If reference cannot be resolved
146
+ """
147
+ # Check if referenced operation exists
148
+ if ref_op_id not in self.results:
149
+ context = f" in operation '{operation_id}'" if operation_id else ""
150
+ raise ApplicationException(
151
+ 400,
152
+ f"Reference to unknown operation '{ref_op_id}'{context}. "
153
+ f"Ensure operation is defined and appears before this one.",
154
+ )
155
+
156
+ # Check if operation completed successfully
157
+ op_result = self.results[ref_op_id]
158
+ if op_result.get("status") != "completed":
159
+ context = f" in operation '{operation_id}'" if operation_id else ""
160
+ raise ApplicationException(
161
+ 400,
162
+ f"Cannot reference operation '{ref_op_id}'{context}: "
163
+ f"operation {op_result.get('status', 'failed')}",
164
+ )
165
+
166
+ # Navigate to the referenced property
167
+ data = op_result.get("data", {})
168
+ path_parts = ref_path.split(".")
169
+
170
+ try:
171
+ value = data
172
+ for part in path_parts:
173
+ if isinstance(value, dict):
174
+ value = value[part]
175
+ elif isinstance(value, list):
176
+ # Support array indexing: items.0.id
177
+ value = value[int(part)]
178
+ else:
179
+ raise KeyError(part)
180
+ return value
181
+
182
+ except (KeyError, IndexError, ValueError) as e:
183
+ context = f" in operation '{operation_id}'" if operation_id else ""
184
+ param_context = f" for parameter '{param_name}'" if param_name else ""
185
+ # Show available properties if data is a dict, otherwise show data type
186
+ available = (
187
+ f"Available properties: {list(data.keys())}"
188
+ if isinstance(data, dict)
189
+ else f"Data is {type(data).__name__}, not a dict"
190
+ )
191
+ raise ApplicationException(
192
+ 400,
193
+ f"Cannot resolve reference '$ref:{ref_op_id}.{ref_path}'"
194
+ f"{context}{param_context}: property not found in result. "
195
+ f"{available}",
196
+ ) from e
197
+
198
+ def validate_references(self, params: Dict[str, Any]) -> List[str]:
199
+ """
200
+ Validate that all references in parameters can be resolved.
201
+
202
+ Args:
203
+ params: Parameters to validate
204
+
205
+ Returns:
206
+ List of referenced operation IDs
207
+ """
208
+ refs = []
209
+
210
+ def find_refs(value: Any):
211
+ if isinstance(value, str) and "$ref:" in value:
212
+ for match in self.REF_PATTERN.finditer(value):
213
+ refs.append(match.group(1))
214
+ elif isinstance(value, dict):
215
+ for v in value.values():
216
+ find_refs(v)
217
+ elif isinstance(value, list):
218
+ for item in value:
219
+ find_refs(item)
220
+
221
+ find_refs(params)
222
+ return list(set(refs))