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,22 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v3.4.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: end-of-file-fixer
|
|
7
|
+
- id: check-yaml
|
|
8
|
+
- id: check-added-large-files
|
|
9
|
+
|
|
10
|
+
- repo: https://github.com/psf/black
|
|
11
|
+
rev: 23.12.1
|
|
12
|
+
hooks:
|
|
13
|
+
- id: black
|
|
14
|
+
language_version: python3.11
|
|
15
|
+
|
|
16
|
+
- repo: https://github.com/PyCQA/flake8
|
|
17
|
+
rev: 7.0.0
|
|
18
|
+
hooks:
|
|
19
|
+
- id: flake8
|
|
20
|
+
args: [--max-line-length=120]
|
|
21
|
+
|
|
22
|
+
exclude: '^$'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.39"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import Mapping, Optional
|
|
3
|
+
|
|
4
|
+
from api_foundry_query_engine.operation import Operation
|
|
5
|
+
from api_foundry_query_engine.services.transactional_service import TransactionalService
|
|
6
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
7
|
+
from api_foundry_query_engine.services.service import Service
|
|
8
|
+
|
|
9
|
+
log = logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Adapter(metaclass=abc.ABCMeta):
|
|
13
|
+
service: Service
|
|
14
|
+
config: Mapping[str, str]
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def __subclasshook__(cls, __subclass: type) -> bool:
|
|
18
|
+
return (
|
|
19
|
+
hasattr(__subclass, "marshal")
|
|
20
|
+
and callable(__subclass.marshal)
|
|
21
|
+
and hasattr(__subclass, "umarshal")
|
|
22
|
+
and callable(__subclass.unmarshal)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self, config: Mapping[str, str] = {}, service: Optional[Service] = None
|
|
27
|
+
) -> None:
|
|
28
|
+
self.config = config
|
|
29
|
+
self.service = (
|
|
30
|
+
service if service is not None else TransactionalService(config=self.config)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def unmarshal(self, event) -> Operation:
|
|
34
|
+
"""
|
|
35
|
+
Unmarshal the event in a tuple for processing
|
|
36
|
+
|
|
37
|
+
Parameters:
|
|
38
|
+
- event (dict): Lambda event object.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
- Operation containing the entity, action, and parameters
|
|
42
|
+
"""
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
def marshal(self, result: list[dict]):
|
|
46
|
+
"""
|
|
47
|
+
Marshal the result into a event response
|
|
48
|
+
|
|
49
|
+
Parameters:
|
|
50
|
+
- result (list): the data set to return in the response
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
- the event response
|
|
54
|
+
"""
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
def process_event(self, event):
|
|
58
|
+
"""
|
|
59
|
+
Process Lambda event using a domain function.
|
|
60
|
+
|
|
61
|
+
Parameters:
|
|
62
|
+
- service_function (callable): The service function to be executed.
|
|
63
|
+
- event (dict): Lambda event object.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
- any: Result of the domain function.
|
|
67
|
+
"""
|
|
68
|
+
operation = self.unmarshal(event)
|
|
69
|
+
|
|
70
|
+
result = self.service.execute(operation)
|
|
71
|
+
log.debug("adapter result: %s", result)
|
|
72
|
+
|
|
73
|
+
return self.marshal(result)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from humps import camelize, decamelize
|
|
2
|
+
|
|
3
|
+
from api_foundry_query_engine.adapters.adapter import Adapter
|
|
4
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
5
|
+
from api_foundry_query_engine.operation import Operation
|
|
6
|
+
|
|
7
|
+
log = logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CaseChangeAdapter(Adapter):
|
|
11
|
+
"""
|
|
12
|
+
Handles changing case from snake to camel and back
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def unmarshal(self, event) -> Operation:
|
|
16
|
+
"""
|
|
17
|
+
Unmarshal the event in a tuple for processing
|
|
18
|
+
|
|
19
|
+
Parameters:
|
|
20
|
+
- event (dict): Lambda event object.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
- tuple: Tuple containing entity operation, store_params, query_params and metadata params.
|
|
24
|
+
"""
|
|
25
|
+
operation = super().unmarshal(event)
|
|
26
|
+
|
|
27
|
+
# determine case
|
|
28
|
+
self.camel_case = (
|
|
29
|
+
operation.metadata_params.get("_case", "snake") == "camel"
|
|
30
|
+
or self.__check_camel_case(operation.store_params)
|
|
31
|
+
or self.__check_camel_case(operation.query_params)
|
|
32
|
+
)
|
|
33
|
+
log.info("camel_case: %s", self.camel_case)
|
|
34
|
+
|
|
35
|
+
if self.camel_case:
|
|
36
|
+
return Operation(
|
|
37
|
+
path=operation.entity,
|
|
38
|
+
action=operation.action,
|
|
39
|
+
store_params=decamelize(operation.store_params),
|
|
40
|
+
query_params=decamelize(operation.query_params),
|
|
41
|
+
metadata_params=operation.metadata_params,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return operation
|
|
45
|
+
|
|
46
|
+
def marshal(self, result: list[dict]):
|
|
47
|
+
"""
|
|
48
|
+
Marshal the result into a event response
|
|
49
|
+
|
|
50
|
+
Parameters:
|
|
51
|
+
- result (list): the data set to return in the response
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
- the event response
|
|
55
|
+
"""
|
|
56
|
+
super().marshal(result)
|
|
57
|
+
|
|
58
|
+
if not self.camel_case:
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
converted_result = []
|
|
62
|
+
for item in result:
|
|
63
|
+
converted_result.append(camelize(item))
|
|
64
|
+
|
|
65
|
+
# convert back to camel case if needed
|
|
66
|
+
return converted_result
|
|
67
|
+
|
|
68
|
+
def __check_camel_case(self, params: dict) -> bool:
|
|
69
|
+
if params is not None:
|
|
70
|
+
# check the keys for an upper case character
|
|
71
|
+
for param in params:
|
|
72
|
+
if (
|
|
73
|
+
param != param.lower()
|
|
74
|
+
and param != param.upper()
|
|
75
|
+
and "_" not in param
|
|
76
|
+
):
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
return False
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from api_foundry_query_engine.adapters.adapter import Adapter
|
|
5
|
+
from api_foundry_query_engine.operation import Operation
|
|
6
|
+
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
7
|
+
|
|
8
|
+
actions_map = {
|
|
9
|
+
"GET": "read",
|
|
10
|
+
"POST": "create",
|
|
11
|
+
"PUT": "update",
|
|
12
|
+
"DELETE": "delete",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GatewayAdapter(Adapter):
|
|
17
|
+
def marshal(self, result: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
18
|
+
"""
|
|
19
|
+
Marshal the result into a event response
|
|
20
|
+
|
|
21
|
+
Parameters:
|
|
22
|
+
- result (list): the data set to return in the response
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
- the event response
|
|
26
|
+
"""
|
|
27
|
+
return super().marshal(result)
|
|
28
|
+
|
|
29
|
+
def unmarshal(self, event: Dict[str, Any]) -> Operation:
|
|
30
|
+
"""
|
|
31
|
+
Get parameters from the Lambda event.
|
|
32
|
+
|
|
33
|
+
Parameters:
|
|
34
|
+
- event (dict): Lambda event object.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
- tuple: Tuple containing data, query and metadata parameters.
|
|
38
|
+
"""
|
|
39
|
+
resource = event.get("resource")
|
|
40
|
+
if resource is not None and "/" in resource:
|
|
41
|
+
parts = resource.split("/")
|
|
42
|
+
entity = parts[1] if len(parts) > 1 else None
|
|
43
|
+
else:
|
|
44
|
+
entity = None
|
|
45
|
+
|
|
46
|
+
method = str(event.get("httpMethod", "")).upper()
|
|
47
|
+
action = actions_map.get(method, "read")
|
|
48
|
+
|
|
49
|
+
# Extract JWT claims early for batch operations
|
|
50
|
+
claims = event.get("requestContext", {}).get("authorizer", {})
|
|
51
|
+
|
|
52
|
+
# Handle different authorizer types
|
|
53
|
+
if isinstance(claims, dict):
|
|
54
|
+
# TOKEN authorizer puts claims directly in authorizer object
|
|
55
|
+
if "sub" in claims or "iss" in claims:
|
|
56
|
+
# Already have JWT claims at top level
|
|
57
|
+
pass
|
|
58
|
+
elif "claims" in claims:
|
|
59
|
+
# Some configurations nest claims
|
|
60
|
+
claims = claims["claims"]
|
|
61
|
+
elif "iam" in claims:
|
|
62
|
+
# IAM authorizer fallback
|
|
63
|
+
claims = claims["iam"]
|
|
64
|
+
elif "lambda" in claims:
|
|
65
|
+
# Lambda authorizer fallback
|
|
66
|
+
claims = claims["lambda"]
|
|
67
|
+
else:
|
|
68
|
+
# Empty or unknown format
|
|
69
|
+
claims = {}
|
|
70
|
+
else:
|
|
71
|
+
# Non-dict authorizer context
|
|
72
|
+
claims = {}
|
|
73
|
+
|
|
74
|
+
# Handle batch requests
|
|
75
|
+
if entity == "batch" and method == "POST":
|
|
76
|
+
body = event.get("body")
|
|
77
|
+
if body:
|
|
78
|
+
batch_request = json.loads(body)
|
|
79
|
+
return Operation(
|
|
80
|
+
entity="batch",
|
|
81
|
+
action="create",
|
|
82
|
+
store_params=batch_request,
|
|
83
|
+
claims=claims,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
event_params = {}
|
|
87
|
+
|
|
88
|
+
path_parameters = self._convert_parameters(event.get("pathParameters"))
|
|
89
|
+
if path_parameters is not None:
|
|
90
|
+
event_params.update(path_parameters)
|
|
91
|
+
|
|
92
|
+
queryStringParameters = self._convert_parameters(
|
|
93
|
+
event.get("queryStringParameters")
|
|
94
|
+
)
|
|
95
|
+
if queryStringParameters is not None:
|
|
96
|
+
event_params.update(queryStringParameters)
|
|
97
|
+
|
|
98
|
+
query_params, metadata_params = self.split_params(event_params)
|
|
99
|
+
|
|
100
|
+
store_params = {}
|
|
101
|
+
body = event.get("body")
|
|
102
|
+
if body is not None and len(body) > 0:
|
|
103
|
+
store_params = json.loads(body)
|
|
104
|
+
scope_str = claims.get("scope")
|
|
105
|
+
|
|
106
|
+
# Enforce OAuth scopes (simulating API Gateway authorizer behavior)
|
|
107
|
+
# Required scope pattern: read|write|delete:<entity>
|
|
108
|
+
if entity and scope_str:
|
|
109
|
+
required_action = {
|
|
110
|
+
"GET": "read",
|
|
111
|
+
"POST": "write",
|
|
112
|
+
"PUT": "write",
|
|
113
|
+
"PATCH": "write",
|
|
114
|
+
"DELETE": "delete",
|
|
115
|
+
}.get(method, "read")
|
|
116
|
+
required_scope = f"{required_action}:{entity}"
|
|
117
|
+
token_scopes = set(str(scope_str).split())
|
|
118
|
+
|
|
119
|
+
def _has_scope(required: str) -> bool:
|
|
120
|
+
return (
|
|
121
|
+
required in token_scopes
|
|
122
|
+
or f"{required_action}:*" in token_scopes
|
|
123
|
+
or "*" in token_scopes
|
|
124
|
+
or "*:*" in token_scopes
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if not _has_scope(required_scope):
|
|
128
|
+
raise ApplicationException(
|
|
129
|
+
401,
|
|
130
|
+
("insufficient_scope: required_scope=" + required_scope),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return Operation(
|
|
134
|
+
entity=entity,
|
|
135
|
+
action=action,
|
|
136
|
+
store_params=store_params,
|
|
137
|
+
query_params=query_params,
|
|
138
|
+
metadata_params=metadata_params,
|
|
139
|
+
claims=claims,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def _convert_parameters(
|
|
143
|
+
self, parameters: Optional[Dict[str, Any]]
|
|
144
|
+
) -> Optional[Dict[str, Any]]:
|
|
145
|
+
"""
|
|
146
|
+
Convert parameters to appropriate types.
|
|
147
|
+
|
|
148
|
+
Parameters:
|
|
149
|
+
- parameters (dict): Dictionary of parameters.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
- dict: Dictionary with parameters converted to appropriate types.
|
|
153
|
+
"""
|
|
154
|
+
if parameters is None:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
result = {}
|
|
158
|
+
for parameter, value in parameters.items():
|
|
159
|
+
try:
|
|
160
|
+
result[parameter] = int(value)
|
|
161
|
+
except ValueError:
|
|
162
|
+
try:
|
|
163
|
+
result[parameter] = float(value)
|
|
164
|
+
except ValueError:
|
|
165
|
+
result[parameter] = value
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
def split_params(
|
|
169
|
+
self, parameters: Dict[str, Any]
|
|
170
|
+
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
171
|
+
"""
|
|
172
|
+
Split a dictionary into two dictionaries based on keys.
|
|
173
|
+
|
|
174
|
+
Parameters:
|
|
175
|
+
- dictionary (dict): Input dictionary.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
- tuple: A tuple containing two dictionaries.
|
|
179
|
+
The first dictionary contains metadata_params,
|
|
180
|
+
and the second dictionary query_params.
|
|
181
|
+
"""
|
|
182
|
+
query_params = {}
|
|
183
|
+
metadata_params = {}
|
|
184
|
+
|
|
185
|
+
for key, value in parameters.items():
|
|
186
|
+
if key.startswith("__"):
|
|
187
|
+
metadata_params[key] = value
|
|
188
|
+
else:
|
|
189
|
+
query_params[key] = value
|
|
190
|
+
|
|
191
|
+
return query_params, metadata_params
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from api_foundry_query_engine.operation import Operation
|
|
4
|
+
from api_foundry_query_engine.services.service import Service
|
|
5
|
+
from api_foundry_query_engine.adapters.adapter import Adapter
|
|
6
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
7
|
+
|
|
8
|
+
log = logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SecurityAdapter(Adapter):
|
|
12
|
+
def __init__(self, service: Optional[Service] = None, permissions: dict = None):
|
|
13
|
+
"""
|
|
14
|
+
Initialize SecurityAdapter with a service and permissions.
|
|
15
|
+
|
|
16
|
+
Parameters:
|
|
17
|
+
- service (Service): The service used to process operations.
|
|
18
|
+
- permissions (dict): A dictionary containing the user's permissions,
|
|
19
|
+
structured as:
|
|
20
|
+
{
|
|
21
|
+
"read": ["field1", "field2", ...],
|
|
22
|
+
"write": ["field3", "field4", ...]
|
|
23
|
+
}
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(service)
|
|
26
|
+
self.permissions = permissions or {"read": [], "write": []}
|
|
27
|
+
|
|
28
|
+
def unmarshal(self, event) -> Operation:
|
|
29
|
+
"""
|
|
30
|
+
Unmarshal the event into an Operation object after validating query and store params.
|
|
31
|
+
|
|
32
|
+
Parameters:
|
|
33
|
+
- event (dict): Lambda event object.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
- Operation: The validated operation.
|
|
37
|
+
"""
|
|
38
|
+
entity = event.get("entity")
|
|
39
|
+
action = event.get("action")
|
|
40
|
+
query_params = event.get("query_params", {})
|
|
41
|
+
store_params = event.get("store_params", {})
|
|
42
|
+
|
|
43
|
+
# Validate read permissions for query_params
|
|
44
|
+
invalid_query_params = [
|
|
45
|
+
key for key in query_params if key not in self.permissions["read"]
|
|
46
|
+
]
|
|
47
|
+
if invalid_query_params:
|
|
48
|
+
raise PermissionError(
|
|
49
|
+
f"Query parameters not permitted: {invalid_query_params}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Validate write permissions for store_params
|
|
53
|
+
invalid_store_params = [
|
|
54
|
+
key for key in store_params if key not in self.permissions["write"]
|
|
55
|
+
]
|
|
56
|
+
if invalid_store_params:
|
|
57
|
+
raise PermissionError(
|
|
58
|
+
f"Store parameters not permitted: {invalid_store_params}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return Operation(
|
|
62
|
+
entity=entity,
|
|
63
|
+
action=action,
|
|
64
|
+
query_params=query_params,
|
|
65
|
+
store_params=store_params,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def marshal(self, result: list[dict]):
|
|
69
|
+
"""
|
|
70
|
+
Filter the result based on read permissions before returning.
|
|
71
|
+
|
|
72
|
+
Parameters:
|
|
73
|
+
- result (list[dict]): The data set to return in the response.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
- list[dict]: Filtered response with only permitted fields.
|
|
77
|
+
"""
|
|
78
|
+
filtered_result = []
|
|
79
|
+
for record in result:
|
|
80
|
+
filtered_record = {
|
|
81
|
+
key: value
|
|
82
|
+
for key, value in record.items()
|
|
83
|
+
if key in self.permissions["read"]
|
|
84
|
+
}
|
|
85
|
+
filtered_result.append(filtered_record)
|
|
86
|
+
|
|
87
|
+
return filtered_result
|
|
88
|
+
|
|
89
|
+
def process_event(self, event):
|
|
90
|
+
"""
|
|
91
|
+
Process Lambda event using a domain function.
|
|
92
|
+
|
|
93
|
+
Parameters:
|
|
94
|
+
- event (dict): Lambda event object.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
- any: Result of the domain function.
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
operation = self.unmarshal(event)
|
|
101
|
+
result = self.service.execute(operation)
|
|
102
|
+
log.debug("adapter result: %s", result)
|
|
103
|
+
return self.marshal(result)
|
|
104
|
+
except PermissionError as e:
|
|
105
|
+
log.error("Permission error: %s", e)
|
|
106
|
+
return {"error": str(e), "status": "permission_denied"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
2
|
+
|
|
3
|
+
# Initialize the logger
|
|
4
|
+
log = logger(__name__)
|
|
5
|
+
|
|
6
|
+
db_config_map = dict()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Cursor:
|
|
10
|
+
def execute(self, sql: str, params: dict, selection_results: dict) -> list[dict]:
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
|
|
13
|
+
def close(self):
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Connection:
|
|
18
|
+
def __init__(self, db_config: dict) -> None:
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.db_config = db_config
|
|
21
|
+
|
|
22
|
+
def engine(self) -> str:
|
|
23
|
+
return self.db_config["engine"]
|
|
24
|
+
|
|
25
|
+
def cursor(self) -> Cursor:
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
def commit(self):
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
def close(self):
|
|
32
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from typing import Mapping
|
|
2
|
+
import boto3
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from api_foundry_query_engine.connectors.connection import Connection
|
|
6
|
+
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
7
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
8
|
+
|
|
9
|
+
log = logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConnectionFactory:
|
|
13
|
+
db_config_map: dict[str, dict]
|
|
14
|
+
config: Mapping[str, str]
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: Mapping[str, str] = {}):
|
|
17
|
+
self.db_config_map = dict()
|
|
18
|
+
self.config = config
|
|
19
|
+
|
|
20
|
+
def get_connection(self, database: str) -> Connection:
|
|
21
|
+
"""
|
|
22
|
+
Factory function to create a database connector based on the
|
|
23
|
+
specified engine and schema.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
- engine (str): The database engine type
|
|
27
|
+
('postgres', 'oracle', or 'mysql').
|
|
28
|
+
- schema (str): The schema for the database.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
- Connector: An instance of the appropriate Connector subclass.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Get the secret name based on the engine and database from the secrets map
|
|
35
|
+
log.info("database: %s", database)
|
|
36
|
+
db_config = self.db_config_map.get(database)
|
|
37
|
+
if not db_config:
|
|
38
|
+
# Use config dict for secrets
|
|
39
|
+
secrets_map = self.config.get("SECRETS", {})
|
|
40
|
+
if isinstance(secrets_map, str):
|
|
41
|
+
secrets_map = json.loads(secrets_map)
|
|
42
|
+
secret_name = secrets_map.get(database)
|
|
43
|
+
log.debug("secret_name: %s", secret_name)
|
|
44
|
+
|
|
45
|
+
if secret_name:
|
|
46
|
+
db_config = self.__get_secret(secret_name)
|
|
47
|
+
else:
|
|
48
|
+
raise ValueError(f"Secret not found for database: {database}")
|
|
49
|
+
|
|
50
|
+
engine = db_config.get("engine")
|
|
51
|
+
if not engine:
|
|
52
|
+
raise ApplicationException(
|
|
53
|
+
500, "Database 'engine' is not defined in the secret."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if engine == "postgres":
|
|
57
|
+
from .postgres_connection import PostgresConnection
|
|
58
|
+
|
|
59
|
+
return PostgresConnection(db_config)
|
|
60
|
+
|
|
61
|
+
# Add support for other engines here if needed in the future
|
|
62
|
+
|
|
63
|
+
raise ValueError(f"Unsupported database engine: {engine}")
|
|
64
|
+
|
|
65
|
+
def __get_secret(self, db_secret_name: str):
|
|
66
|
+
"""
|
|
67
|
+
Get the secret from AWS Secrets Manager.
|
|
68
|
+
|
|
69
|
+
Parameters:
|
|
70
|
+
- db_secret_name (str): The name of the AWS Secrets Manager secret.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
- dict: The database configuration obtained from the secret.
|
|
74
|
+
"""
|
|
75
|
+
if self.config.get(db_secret_name):
|
|
76
|
+
return self.config.get(db_secret_name)
|
|
77
|
+
|
|
78
|
+
endpoint_url = self.config.get("AWS_ENDPOINT_URL") # LocalStack endpoint
|
|
79
|
+
sts_client = boto3.client("sts", endpoint_url=endpoint_url)
|
|
80
|
+
|
|
81
|
+
secret_account_id = self.config.get("SECRET_ACCOUNT_ID", None)
|
|
82
|
+
log.debug("secret_account_id: %s", secret_account_id)
|
|
83
|
+
|
|
84
|
+
if secret_account_id:
|
|
85
|
+
# If a secret account ID is provided, assume a role in that account
|
|
86
|
+
secret_role = self.config.get("ROLE_NAME", None)
|
|
87
|
+
assume_role_response = sts_client.assume_role(
|
|
88
|
+
RoleArn=f"arn:aws:iam::{secret_account_id}:role/{secret_role}",
|
|
89
|
+
RoleSessionName="AssumeRoleSession",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
credentials = assume_role_response["Credentials"]
|
|
93
|
+
|
|
94
|
+
secretsmanager = boto3.client(
|
|
95
|
+
"secretsmanager",
|
|
96
|
+
aws_access_key_id=credentials["AccessKeyId"],
|
|
97
|
+
aws_secret_access_key=credentials["SecretAccessKey"],
|
|
98
|
+
aws_session_token=credentials["SessionToken"],
|
|
99
|
+
endpoint_url=endpoint_url,
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
# If no secret account ID is provided, use the default account
|
|
103
|
+
log.info("endpoint_url: %s", endpoint_url)
|
|
104
|
+
secretsmanager = boto3.client(
|
|
105
|
+
"secretsmanager",
|
|
106
|
+
endpoint_url=endpoint_url,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Get the secret value from AWS Secrets Manager
|
|
110
|
+
log.info("db_secret_name: %s", db_secret_name)
|
|
111
|
+
db_secret = secretsmanager.get_secret_value(SecretId=db_secret_name)
|
|
112
|
+
log.debug("loading secret name: %s", db_secret)
|
|
113
|
+
|
|
114
|
+
# Return the parsed JSON secret string
|
|
115
|
+
return json.loads(db_secret.get("SecretString"))
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
2
|
+
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
3
|
+
from api_foundry_query_engine.connectors.connection import Connector
|
|
4
|
+
|
|
5
|
+
log = logger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OracleConnnector(Connector):
|
|
9
|
+
def __init__(self, db_secret_name: str) -> None:
|
|
10
|
+
super().__init__(db_secret_name)
|
|
11
|
+
|
|
12
|
+
def close(self):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def execute(self, cursor, sql: str, parameters: dict):
|
|
16
|
+
from oracledb import Error, IntegrityError, ProgrammingError
|
|
17
|
+
|
|
18
|
+
log.debug("sql: %s, parameters: %s", sql, parameters)
|
|
19
|
+
try:
|
|
20
|
+
cursor.execute(sql, parameters)
|
|
21
|
+
except IntegrityError as err:
|
|
22
|
+
(error,) = err.args
|
|
23
|
+
raise ApplicationException(409, error.message)
|
|
24
|
+
except ProgrammingError as err:
|
|
25
|
+
(error,) = err.args
|
|
26
|
+
raise ApplicationException(400, error.message)
|
|
27
|
+
except Error as err:
|
|
28
|
+
(error,) = err.args
|
|
29
|
+
raise ApplicationException(500, error.message)
|