api-foundry-query-engine 0.8.12__tar.gz → 0.8.31__tar.gz
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-0.8.12 → api_foundry_query_engine-0.8.31}/PKG-INFO +2 -1
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/adapters/adapter.py +1 -1
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/adapters/case_change_adapter.py +1 -1
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/adapters/gateway_adapter.py +24 -10
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/adapters/security_adapter.py +2 -2
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/connectors/connection_factory.py +6 -6
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/connectors/oracle_connector.py +1 -1
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/connectors/postgres_connection.py +1 -1
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/dao/sql_custom_query_handler.py +1 -1
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/dao/sql_delete_query_handler.py +1 -1
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/dao/sql_insert_query_handler.py +1 -1
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/dao/sql_query_handler.py +38 -10
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/dao/sql_select_query_handler.py +17 -3
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/lambda_handler.py +7 -6
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/operation.py +32 -15
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/services/service.py +12 -6
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/services/transactional_service.py +2 -2
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/utils/api_model.py +109 -30
- api_foundry_query_engine-0.8.31/api_foundry_query_engine/utils/claims_check.py +471 -0
- api_foundry_query_engine-0.8.31/api_foundry_query_engine/utils/gateway_operation.py +279 -0
- api_foundry_query_engine-0.8.31/api_foundry_query_engine/utils/token_decoder.py +415 -0
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/pyproject.toml +38 -2
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/.gitignore +0 -0
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/LICENSE +0 -0
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/__init__.py +0 -0
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/connectors/connection.py +0 -0
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/dao/dao.py +0 -0
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/dao/operation_dao.py +0 -0
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/dao/sql_update_query_handler.py +0 -0
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/utils/app_exception.py +0 -0
- {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/utils/logger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api-foundry-query-engine
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.31
|
|
4
4
|
Summary: The AWS lambda service handler use by the `api_foundry` project is a powerful tool designed to automate the deployment of REST APIs on AWS using Lambda services to access and interact with relational databases (RDBMS). This project leverages the OpenAPI specification to define and manage the APIs
|
|
5
5
|
Project-URL: Documentation, https://github.com/DanRepik/api-foundry
|
|
6
6
|
Project-URL: Source, https://github.com/DanRepik/api-foundry
|
|
@@ -18,3 +18,4 @@ Requires-Dist: fixture-foundry>=0.1.0; extra == 'dev'
|
|
|
18
18
|
Requires-Dist: isort>=5.12.0; extra == 'dev'
|
|
19
19
|
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
20
20
|
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: simple-oauth-server; extra == 'dev'
|
|
@@ -30,7 +30,7 @@ class CaseChangeAdapter(Adapter):
|
|
|
30
30
|
or self.__check_camel_case(operation.store_params)
|
|
31
31
|
or self.__check_camel_case(operation.query_params)
|
|
32
32
|
)
|
|
33
|
-
log.info(
|
|
33
|
+
log.info("camel_case: %s", self.camel_case)
|
|
34
34
|
|
|
35
35
|
if self.camel_case:
|
|
36
36
|
return Operation(
|
|
@@ -65,12 +65,30 @@ class GatewayAdapter(Adapter):
|
|
|
65
65
|
if body is not None and len(body) > 0:
|
|
66
66
|
store_params = json.loads(body)
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
claims =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
# Extract JWT claims from API Gateway event
|
|
69
|
+
claims = event.get("requestContext", {}).get("authorizer", {})
|
|
70
|
+
|
|
71
|
+
# Handle different authorizer types
|
|
72
|
+
if isinstance(claims, dict):
|
|
73
|
+
# TOKEN authorizer puts claims directly in authorizer object
|
|
74
|
+
if "sub" in claims or "iss" in claims:
|
|
75
|
+
# Already have JWT claims at top level
|
|
76
|
+
pass
|
|
77
|
+
elif "claims" in claims:
|
|
78
|
+
# Some configurations nest claims
|
|
79
|
+
claims = claims["claims"]
|
|
80
|
+
elif "iam" in claims:
|
|
81
|
+
# IAM authorizer fallback
|
|
82
|
+
claims = claims["iam"]
|
|
83
|
+
elif "lambda" in claims:
|
|
84
|
+
# Lambda authorizer fallback
|
|
85
|
+
claims = claims["lambda"]
|
|
86
|
+
else:
|
|
87
|
+
# Empty or unknown format
|
|
88
|
+
claims = {}
|
|
89
|
+
else:
|
|
90
|
+
# Non-dict authorizer context
|
|
91
|
+
claims = {}
|
|
74
92
|
scope_str = claims.get("scope")
|
|
75
93
|
|
|
76
94
|
# Enforce OAuth scopes (simulating API Gateway authorizer behavior)
|
|
@@ -106,10 +124,6 @@ class GatewayAdapter(Adapter):
|
|
|
106
124
|
store_params=store_params,
|
|
107
125
|
query_params=query_params,
|
|
108
126
|
metadata_params=metadata_params,
|
|
109
|
-
roles=roles,
|
|
110
|
-
groups=groups,
|
|
111
|
-
subject=subject,
|
|
112
|
-
permissions=permissions,
|
|
113
127
|
claims=claims,
|
|
114
128
|
)
|
|
115
129
|
|
|
@@ -99,8 +99,8 @@ class SecurityAdapter(Adapter):
|
|
|
99
99
|
try:
|
|
100
100
|
operation = self.unmarshal(event)
|
|
101
101
|
result = self.service.execute(operation)
|
|
102
|
-
log.debug(
|
|
102
|
+
log.debug("adapter result: %s", result)
|
|
103
103
|
return self.marshal(result)
|
|
104
104
|
except PermissionError as e:
|
|
105
|
-
log.error(
|
|
105
|
+
log.error("Permission error: %s", e)
|
|
106
106
|
return {"error": str(e), "status": "permission_denied"}
|
|
@@ -32,7 +32,7 @@ class ConnectionFactory:
|
|
|
32
32
|
"""
|
|
33
33
|
|
|
34
34
|
# Get the secret name based on the engine and database from the secrets map
|
|
35
|
-
log.info(
|
|
35
|
+
log.info("database: %s", database)
|
|
36
36
|
db_config = self.db_config_map.get(database)
|
|
37
37
|
if not db_config:
|
|
38
38
|
# Use config dict for secrets
|
|
@@ -40,7 +40,7 @@ class ConnectionFactory:
|
|
|
40
40
|
if isinstance(secrets_map, str):
|
|
41
41
|
secrets_map = json.loads(secrets_map)
|
|
42
42
|
secret_name = secrets_map.get(database)
|
|
43
|
-
log.debug(
|
|
43
|
+
log.debug("secret_name: %s", secret_name)
|
|
44
44
|
|
|
45
45
|
if secret_name:
|
|
46
46
|
db_config = self.__get_secret(secret_name)
|
|
@@ -79,7 +79,7 @@ class ConnectionFactory:
|
|
|
79
79
|
sts_client = boto3.client("sts", endpoint_url=endpoint_url)
|
|
80
80
|
|
|
81
81
|
secret_account_id = self.config.get("SECRET_ACCOUNT_ID", None)
|
|
82
|
-
log.debug(
|
|
82
|
+
log.debug("secret_account_id: %s", secret_account_id)
|
|
83
83
|
|
|
84
84
|
if secret_account_id:
|
|
85
85
|
# If a secret account ID is provided, assume a role in that account
|
|
@@ -100,16 +100,16 @@ class ConnectionFactory:
|
|
|
100
100
|
)
|
|
101
101
|
else:
|
|
102
102
|
# If no secret account ID is provided, use the default account
|
|
103
|
-
log.info(
|
|
103
|
+
log.info("endpoint_url: %s", endpoint_url)
|
|
104
104
|
secretsmanager = boto3.client(
|
|
105
105
|
"secretsmanager",
|
|
106
106
|
endpoint_url=endpoint_url,
|
|
107
107
|
)
|
|
108
108
|
|
|
109
109
|
# Get the secret value from AWS Secrets Manager
|
|
110
|
-
log.info(
|
|
110
|
+
log.info("db_secret_name: %s", db_secret_name)
|
|
111
111
|
db_secret = secretsmanager.get_secret_value(SecretId=db_secret_name)
|
|
112
|
-
log.debug(
|
|
112
|
+
log.debug("loading secret name: %s", db_secret)
|
|
113
113
|
|
|
114
114
|
# Return the parsed JSON secret string
|
|
115
115
|
return json.loads(db_secret.get("SecretString"))
|
|
@@ -15,7 +15,7 @@ class OracleConnnector(Connector):
|
|
|
15
15
|
def execute(self, cursor, sql: str, parameters: dict):
|
|
16
16
|
from oracledb import Error, IntegrityError, ProgrammingError
|
|
17
17
|
|
|
18
|
-
log.debug(
|
|
18
|
+
log.debug("sql: %s, parameters: %s", sql, parameters)
|
|
19
19
|
try:
|
|
20
20
|
cursor.execute(sql, parameters)
|
|
21
21
|
except IntegrityError as err:
|
|
@@ -39,7 +39,7 @@ class SQLCustomQueryHandler(SQLQueryHandler):
|
|
|
39
39
|
self._selection_results = self.check_permissions(
|
|
40
40
|
"read", self.path_operation.permissions, self.path_operation.outputs
|
|
41
41
|
)
|
|
42
|
-
log.debug(
|
|
42
|
+
log.debug("selection_results: %s", self._selection_results)
|
|
43
43
|
return self._selection_results
|
|
44
44
|
|
|
45
45
|
def _compile(self):
|
|
@@ -31,7 +31,7 @@ class SQLDeleteSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
31
31
|
|
|
32
32
|
for role in self.operation.roles:
|
|
33
33
|
role_permissions = self.schema_object.permissions.get(role, {})
|
|
34
|
-
log.info(
|
|
34
|
+
log.info("role: %s, role_permissions: %s", role, role_permissions)
|
|
35
35
|
if len(role_permissions) == 0:
|
|
36
36
|
continue
|
|
37
37
|
allowed = role_permissions.get("delete", False)
|
|
@@ -78,7 +78,7 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
78
78
|
for k, v in self.schema_object.properties.items()
|
|
79
79
|
if k in allowed_property_names
|
|
80
80
|
}
|
|
81
|
-
log.info(
|
|
81
|
+
log.info("allowed properties: %s", allowed_properties)
|
|
82
82
|
|
|
83
83
|
import json
|
|
84
84
|
|
|
@@ -187,26 +187,54 @@ class SQLQueryHandler:
|
|
|
187
187
|
return properties
|
|
188
188
|
|
|
189
189
|
allowed_properties = {}
|
|
190
|
+
log.info(
|
|
191
|
+
"Input properties keys: %s",
|
|
192
|
+
list(properties.keys()) if properties else "None",
|
|
193
|
+
)
|
|
194
|
+
log.info("Operation roles: %s", self.operation.roles)
|
|
190
195
|
|
|
191
196
|
for role in self.operation.roles:
|
|
192
197
|
role_permissions = permissions.get(role, {})
|
|
193
|
-
log.info(
|
|
198
|
+
log.info("role: %s, role_permissions: %s", role, role_permissions)
|
|
194
199
|
if len(role_permissions) == 0:
|
|
195
200
|
continue
|
|
201
|
+
|
|
202
|
+
# Extract permission patterns for this role
|
|
203
|
+
read_perm = role_permissions.get("read", "")
|
|
204
|
+
write_perm = role_permissions.get("write", "")
|
|
205
|
+
read_pattern = self._extract_permission_pattern(read_perm)
|
|
206
|
+
write_pattern = self._extract_permission_pattern(write_perm)
|
|
207
|
+
|
|
196
208
|
for prop_name, property in properties.items():
|
|
197
|
-
log.info(
|
|
198
|
-
if permission_type == "read" and re.match(
|
|
199
|
-
role_permissions.get("read", ""), prop_name
|
|
200
|
-
):
|
|
209
|
+
log.info("prop_name: %s, property: %s", prop_name, property)
|
|
210
|
+
if permission_type == "read" and re.match(read_pattern, prop_name):
|
|
201
211
|
allowed_properties[prop_name] = property
|
|
202
|
-
if permission_type == "write" and re.match(
|
|
203
|
-
role_permissions.get("write", ""), prop_name
|
|
204
|
-
):
|
|
212
|
+
if permission_type == "write" and re.match(write_pattern, prop_name):
|
|
205
213
|
allowed_properties[prop_name] = property
|
|
206
214
|
|
|
207
|
-
log.info(
|
|
215
|
+
log.info("allowed_properties: %s", allowed_properties)
|
|
208
216
|
return allowed_properties
|
|
209
217
|
|
|
218
|
+
def _extract_permission_pattern(self, permission_rule) -> str:
|
|
219
|
+
"""Extract the regex pattern from a permission rule.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
permission_rule: Can be a string (regex) or dict with
|
|
223
|
+
'properties'/'fields' key
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
str: The regex pattern to match property names
|
|
227
|
+
"""
|
|
228
|
+
if isinstance(permission_rule, str):
|
|
229
|
+
return permission_rule
|
|
230
|
+
elif isinstance(permission_rule, dict):
|
|
231
|
+
# Support both 'properties' (preferred) and 'fields' (legacy)
|
|
232
|
+
return permission_rule.get("properties") or permission_rule.get(
|
|
233
|
+
"fields", ""
|
|
234
|
+
)
|
|
235
|
+
else:
|
|
236
|
+
return ""
|
|
237
|
+
|
|
210
238
|
@property
|
|
211
239
|
def selection_results(self) -> Dict:
|
|
212
240
|
raise NotImplementedError()
|
|
@@ -358,7 +386,7 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
|
|
|
358
386
|
"""
|
|
359
387
|
log.info("selection_result")
|
|
360
388
|
if not hasattr(self, "__selection_results"):
|
|
361
|
-
log.info(
|
|
389
|
+
log.info("prefix_map: %s", self.prefix_map)
|
|
362
390
|
filters = self.operation.metadata_params.get("_properties", ".*").split()
|
|
363
391
|
allowed_properties = self.check_permissions(
|
|
364
392
|
"read", self.schema_object.permissions, self.schema_object.properties
|
|
@@ -33,20 +33,34 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
|
|
|
33
33
|
if "default" in perms:
|
|
34
34
|
provider = perms.get("default", {}) or {}
|
|
35
35
|
read_map = provider.get("read", {}) or {}
|
|
36
|
+
role_permissions = perms.get("default", {})
|
|
36
37
|
else:
|
|
37
38
|
# legacy role-first -> synthesize read map
|
|
38
39
|
read_map = {}
|
|
40
|
+
role_permissions = perms
|
|
39
41
|
for role, role_perms in perms.items():
|
|
40
42
|
if isinstance(role_perms, dict):
|
|
41
43
|
read_map[role] = role_perms.get("read")
|
|
42
44
|
|
|
43
45
|
filters = []
|
|
44
46
|
for role in self.operation.roles or []:
|
|
47
|
+
# Check for role-level WHERE clause (hybrid approach)
|
|
48
|
+
role_where = None
|
|
49
|
+
if isinstance(role_permissions.get(role), dict):
|
|
50
|
+
role_where = role_permissions[role].get("where")
|
|
51
|
+
|
|
52
|
+
# Check for operation-level WHERE clause
|
|
53
|
+
operation_where = None
|
|
45
54
|
rule = read_map.get(role)
|
|
46
55
|
if isinstance(rule, dict):
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
operation_where = rule.get("where")
|
|
57
|
+
|
|
58
|
+
# Operation-level takes precedence, fallback to role-level
|
|
59
|
+
where_clause = operation_where if operation_where else role_where
|
|
60
|
+
|
|
61
|
+
if isinstance(where_clause, str) and where_clause.strip():
|
|
62
|
+
filters.append(self._template_where(where_clause))
|
|
63
|
+
|
|
50
64
|
if not filters:
|
|
51
65
|
return ""
|
|
52
66
|
return "(" + ") OR (".join(filters) + ")"
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Mapping, Any
|
|
5
5
|
|
|
6
6
|
from api_foundry_query_engine.utils.api_model import set_api_model
|
|
7
7
|
from api_foundry_query_engine.utils.app_exception import ApplicationException
|
|
8
8
|
from api_foundry_query_engine.adapters.gateway_adapter import GatewayAdapter
|
|
9
|
+
from api_foundry_query_engine.utils.token_decoder import token_decoder
|
|
10
|
+
from api_foundry_query_engine.utils.claims_check import claims_check
|
|
9
11
|
|
|
12
|
+
logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO"))
|
|
10
13
|
log = logging.getLogger(__name__)
|
|
11
14
|
|
|
12
15
|
|
|
@@ -44,15 +47,13 @@ class QueryEngine:
|
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
@token_decoder()
|
|
51
|
+
@claims_check()
|
|
51
52
|
def handler(event, _):
|
|
52
53
|
if not hasattr(handler, "engine_config"):
|
|
53
54
|
log.info("Loading engine config from environment variables")
|
|
54
55
|
handler.engine_config = os.environ
|
|
55
|
-
log.info(
|
|
56
|
+
log.info("engine_config: %s", handler.engine_config)
|
|
56
57
|
|
|
57
58
|
if not hasattr(handler, "query_engine"):
|
|
58
59
|
set_api_model(handler.engine_config)
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
from typing import Any, Dict, Optional
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from api_foundry_query_engine.utils.logger import logger
|
|
4
|
+
|
|
5
|
+
logger = logger(__name__)
|
|
2
6
|
|
|
3
7
|
|
|
4
8
|
class Operation:
|
|
@@ -35,11 +39,6 @@ class Operation:
|
|
|
35
39
|
query_params: Optional[Dict[str, Any]] = None,
|
|
36
40
|
store_params: Optional[Dict[str, Any]] = None,
|
|
37
41
|
metadata_params: Optional[Dict[str, Any]] = None,
|
|
38
|
-
roles: Optional[Dict[str, Any]] = None,
|
|
39
|
-
scope: Optional[Dict[str, Any]] = None,
|
|
40
|
-
groups: Optional[Dict[str, Any]] = None,
|
|
41
|
-
permissions: Optional[Dict[str, Any]] = None,
|
|
42
|
-
subject: Optional[str] = None,
|
|
43
42
|
claims: Optional[Dict[str, Any]] = None,
|
|
44
43
|
):
|
|
45
44
|
"""
|
|
@@ -76,12 +75,30 @@ class Operation:
|
|
|
76
75
|
self.metadata_params = metadata_params or {}
|
|
77
76
|
|
|
78
77
|
# Roles defining the context in which the operation is allowed.
|
|
79
|
-
self.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
self.claims = claims or {}
|
|
79
|
+
|
|
80
|
+
# Log the operation for debugging and audit purposes
|
|
81
|
+
logger.info(
|
|
82
|
+
"Operation created: entity=%s, action=%s, "
|
|
83
|
+
"query_params=%s, store_params=%s, "
|
|
84
|
+
"metadata_params=%s, claims=%s",
|
|
85
|
+
self.entity,
|
|
86
|
+
self.action,
|
|
87
|
+
self.query_params,
|
|
88
|
+
self.store_params,
|
|
89
|
+
self.metadata_params,
|
|
90
|
+
self.claims,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def roles(self) -> List[str]:
|
|
95
|
+
"""Get the roles from claims."""
|
|
96
|
+
return self.claims.get("roles", []) if self.claims else []
|
|
97
|
+
|
|
98
|
+
def subject(self) -> Optional[str]:
|
|
99
|
+
"""Get the subject from claims."""
|
|
100
|
+
return self.claims.get("sub") if self.claims else None
|
|
101
|
+
|
|
102
|
+
def groups(self) -> List[str]:
|
|
103
|
+
"""Get the groups from claims."""
|
|
104
|
+
return self.claims.get("groups", []) if self.claims else []
|
|
@@ -8,7 +8,9 @@ log = logger(__name__)
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class Service:
|
|
11
|
-
def __init__(self, config: dict =
|
|
11
|
+
def __init__(self, config: dict = None):
|
|
12
|
+
if not config:
|
|
13
|
+
config = {}
|
|
12
14
|
self.config = config
|
|
13
15
|
|
|
14
16
|
def execute(self, operation: Operation) -> list[dict]:
|
|
@@ -16,7 +18,9 @@ class Service:
|
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class ServiceAdapter(Service):
|
|
19
|
-
def __init__(self, config: dict =
|
|
21
|
+
def __init__(self, config: dict = None):
|
|
22
|
+
if not config:
|
|
23
|
+
config = {}
|
|
20
24
|
super().__init__(config)
|
|
21
25
|
|
|
22
26
|
def execute(self, operation: Operation) -> list[dict]:
|
|
@@ -24,7 +28,9 @@ class ServiceAdapter(Service):
|
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
class MutationPublisher(ServiceAdapter):
|
|
27
|
-
def __init__(self, config: dict =
|
|
31
|
+
def __init__(self, config: dict = None):
|
|
32
|
+
if not config:
|
|
33
|
+
config = {}
|
|
28
34
|
super().__init__(config)
|
|
29
35
|
|
|
30
36
|
def execute(self, operation):
|
|
@@ -34,7 +40,7 @@ class MutationPublisher(ServiceAdapter):
|
|
|
34
40
|
|
|
35
41
|
def publish_notification(self, operation):
|
|
36
42
|
topic_arn = self.config.get("BROADCAST_TOPIC", None)
|
|
37
|
-
log.debug(
|
|
43
|
+
log.debug("Topic ARN: %s", topic_arn)
|
|
38
44
|
|
|
39
45
|
if topic_arn is not None:
|
|
40
46
|
log.debug("Sending message")
|
|
@@ -46,7 +52,7 @@ class MutationPublisher(ServiceAdapter):
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
message_str = json.dumps({"default": json.dumps(message)})
|
|
49
|
-
log.debug(
|
|
55
|
+
log.debug("message_str: %s", message_str)
|
|
50
56
|
hash_object = hashlib.sha256(message_str.encode("utf-8"))
|
|
51
57
|
hex_dig = hash_object.hexdigest()
|
|
52
58
|
|
|
@@ -57,7 +63,7 @@ class MutationPublisher(ServiceAdapter):
|
|
|
57
63
|
MessageGroupId=operation.api_name,
|
|
58
64
|
Message=message_str,
|
|
59
65
|
)
|
|
60
|
-
log.info(
|
|
66
|
+
log.info("publish msg id %s", msg_id)
|
|
61
67
|
|
|
62
68
|
def __client(self, client_type):
|
|
63
69
|
import boto3
|
|
@@ -45,8 +45,8 @@ class TransactionalService(ServiceAdapter):
|
|
|
45
45
|
return [result]
|
|
46
46
|
return result
|
|
47
47
|
except Exception as error:
|
|
48
|
-
log.error(
|
|
49
|
-
log.error(
|
|
48
|
+
log.error("transaction exception: %s", error)
|
|
49
|
+
log.error("traceback: %s", traceback.format_exc())
|
|
50
50
|
raise error
|
|
51
51
|
finally:
|
|
52
52
|
connection.close()
|
|
@@ -49,38 +49,117 @@ class SchemaObjectProperty:
|
|
|
49
49
|
def convert_to_db_value(self, value: str) -> Optional[Any]:
|
|
50
50
|
if value is None:
|
|
51
51
|
return None
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
52
|
+
|
|
53
|
+
column_type = self.column_type if self.column_type is not None else "string"
|
|
54
|
+
|
|
55
|
+
# Handle string types
|
|
56
|
+
if column_type in ["string", "varchar", "char", "text", "uuid"]:
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
# Handle numeric types - float/double/numeric variations
|
|
60
|
+
elif column_type in ["number", "float", "double", "numeric", "decimal", "real"]:
|
|
61
|
+
return float(value)
|
|
62
|
+
|
|
63
|
+
# Handle boolean types - can map to boolean or integer columns
|
|
64
|
+
elif column_type == "boolean":
|
|
65
|
+
return value.lower() == "true"
|
|
66
|
+
elif (
|
|
67
|
+
column_type in ["int", "integer", "smallint", "bigint"]
|
|
68
|
+
and hasattr(self, "api_type")
|
|
69
|
+
and self.api_type == "boolean"
|
|
70
|
+
):
|
|
71
|
+
# Boolean API type mapping to integer column type
|
|
72
|
+
return 1 if value.lower() == "true" else 0
|
|
73
|
+
|
|
74
|
+
# Handle integer types (after boolean check to avoid conflicts)
|
|
75
|
+
elif column_type in [
|
|
76
|
+
"integer",
|
|
77
|
+
"int",
|
|
78
|
+
"bigint",
|
|
79
|
+
"smallint",
|
|
80
|
+
"serial",
|
|
81
|
+
"bigserial",
|
|
82
|
+
]:
|
|
83
|
+
return int(value)
|
|
84
|
+
|
|
85
|
+
# Handle date types
|
|
86
|
+
elif column_type == "date":
|
|
87
|
+
return datetime.strptime(value, "%Y-%m-%d").date() if value else None
|
|
88
|
+
|
|
89
|
+
# Handle datetime types - various column type names
|
|
90
|
+
elif column_type in ["date-time", "datetime", "timestamp", "timestamptz"]:
|
|
91
|
+
return datetime.fromisoformat(value) if value else None
|
|
92
|
+
|
|
93
|
+
# Handle time types
|
|
94
|
+
elif column_type in ["time", "timetz"]:
|
|
95
|
+
return datetime.strptime(value, "%H:%M:%S").time() if value else None
|
|
96
|
+
|
|
97
|
+
# Default to string conversion for unknown types
|
|
98
|
+
else:
|
|
99
|
+
return value
|
|
66
100
|
|
|
67
101
|
def convert_to_api_value(self, value) -> Optional[Any]:
|
|
68
102
|
if value is None:
|
|
69
103
|
return None
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
|
|
105
|
+
api_type = self.api_type if self.api_type is not None else "string"
|
|
106
|
+
|
|
107
|
+
# Handle string types (including UUID which is represented as string in API)
|
|
108
|
+
if api_type in ["string", "uuid"]:
|
|
109
|
+
return str(value) if value is not None else None
|
|
110
|
+
|
|
111
|
+
# Handle integer type
|
|
112
|
+
elif api_type == "integer":
|
|
113
|
+
return int(value) if value is not None else None
|
|
114
|
+
|
|
115
|
+
# Handle number and float types
|
|
116
|
+
elif api_type in ["number", "float"]:
|
|
117
|
+
return float(value) if value is not None else None
|
|
118
|
+
|
|
119
|
+
# Handle boolean type - convert any value to string representation
|
|
120
|
+
elif api_type == "boolean":
|
|
121
|
+
if isinstance(value, bool):
|
|
122
|
+
return str(value)
|
|
123
|
+
elif isinstance(value, int):
|
|
124
|
+
# Handle boolean stored as integer (0/1)
|
|
125
|
+
return "true" if value != 0 else "false"
|
|
126
|
+
elif isinstance(value, str):
|
|
127
|
+
return value.lower() in ["true", "1", "yes", "on"]
|
|
128
|
+
else:
|
|
129
|
+
return str(bool(value))
|
|
130
|
+
|
|
131
|
+
# Handle date type
|
|
132
|
+
elif api_type == "date":
|
|
133
|
+
if hasattr(value, "date"):
|
|
134
|
+
# If it's a datetime, extract the date part
|
|
135
|
+
return value.date().isoformat()
|
|
136
|
+
elif hasattr(value, "isoformat"):
|
|
137
|
+
# If it's already a date
|
|
138
|
+
return value.isoformat()
|
|
139
|
+
else:
|
|
140
|
+
return str(value)
|
|
141
|
+
|
|
142
|
+
# Handle datetime type
|
|
143
|
+
elif api_type == "date-time":
|
|
144
|
+
if hasattr(value, "isoformat"):
|
|
145
|
+
return value.isoformat()
|
|
146
|
+
else:
|
|
147
|
+
return str(value)
|
|
148
|
+
|
|
149
|
+
# Handle time type
|
|
150
|
+
elif api_type == "time":
|
|
151
|
+
if hasattr(value, "time"):
|
|
152
|
+
# If it's a datetime, extract the time part
|
|
153
|
+
return value.time().isoformat()
|
|
154
|
+
elif hasattr(value, "isoformat"):
|
|
155
|
+
# If it's already a time
|
|
156
|
+
return value.isoformat()
|
|
157
|
+
else:
|
|
158
|
+
return str(value)
|
|
159
|
+
|
|
160
|
+
# Default to string conversion for unknown types
|
|
161
|
+
else:
|
|
162
|
+
return str(value) if value is not None else None
|
|
84
163
|
|
|
85
164
|
|
|
86
165
|
class SchemaObjectAssociation:
|
|
@@ -210,7 +289,7 @@ class APIModel:
|
|
|
210
289
|
"""Class to load and expose the API configuration as objects."""
|
|
211
290
|
|
|
212
291
|
def __init__(self, config: Dict[str, Any]):
|
|
213
|
-
|
|
292
|
+
log.info("building api_model")
|
|
214
293
|
self.schema_objects = {
|
|
215
294
|
name: SchemaObject(schema_data)
|
|
216
295
|
for name, schema_data in config.get("schema_objects", {}).items()
|
|
@@ -244,4 +323,4 @@ def set_api_model(engine_config: Mapping[str, str]):
|
|
|
244
323
|
os.environ.get("API_SPEC", "/var/task/api_spec.yaml"), "r"
|
|
245
324
|
) as file:
|
|
246
325
|
api_model = APIModel(yaml.safe_load(file))
|
|
247
|
-
log.info(
|
|
326
|
+
log.info("Loaded API model: %s", api_model)
|