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,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))
|