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.
Files changed (33) hide show
  1. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/PKG-INFO +2 -1
  2. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/adapters/adapter.py +1 -1
  3. {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
  4. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/adapters/gateway_adapter.py +24 -10
  5. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/adapters/security_adapter.py +2 -2
  6. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/connectors/connection_factory.py +6 -6
  7. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/connectors/oracle_connector.py +1 -1
  8. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/connectors/postgres_connection.py +1 -1
  9. {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
  10. {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
  11. {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
  12. {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
  13. {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
  14. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/lambda_handler.py +7 -6
  15. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/operation.py +32 -15
  16. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/services/service.py +12 -6
  17. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/services/transactional_service.py +2 -2
  18. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/utils/api_model.py +109 -30
  19. api_foundry_query_engine-0.8.31/api_foundry_query_engine/utils/claims_check.py +471 -0
  20. api_foundry_query_engine-0.8.31/api_foundry_query_engine/utils/gateway_operation.py +279 -0
  21. api_foundry_query_engine-0.8.31/api_foundry_query_engine/utils/token_decoder.py +415 -0
  22. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/pyproject.toml +38 -2
  23. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/.gitignore +0 -0
  24. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/LICENSE +0 -0
  25. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
  26. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/__init__.py +0 -0
  27. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/connectors/connection.py +0 -0
  28. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/dao/dao.py +0 -0
  29. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/dao/operation_dao.py +0 -0
  30. {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
  31. {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
  32. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.31}/api_foundry_query_engine/utils/app_exception.py +0 -0
  33. {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.12
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'
@@ -68,6 +68,6 @@ class Adapter(metaclass=abc.ABCMeta):
68
68
  operation = self.unmarshal(event)
69
69
 
70
70
  result = self.service.execute(operation)
71
- log.debug(f"adapter result: {result}")
71
+ log.debug("adapter result: %s", result)
72
72
 
73
73
  return self.marshal(result)
@@ -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(f"camel_case: {self.camel_case}")
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
- authorizer_info = event.get("requestContext", {}).get("authorizer", {})
69
- claims = authorizer_info.get("claims", {})
70
- roles = claims.get("roles", [])
71
- groups = claims.get("groups", [])
72
- subject = claims.get("sub")
73
- permissions = claims.get("permissions", [])
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(f"adapter result: {result}")
102
+ log.debug("adapter result: %s", result)
103
103
  return self.marshal(result)
104
104
  except PermissionError as e:
105
- log.error(f"Permission error: {e}")
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(f"database: {database}")
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(f"secret_name: {secret_name}")
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(f"secret_account_id: {secret_account_id}")
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(f"endpoint_url: {endpoint_url}")
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(f"db_secret_name: {db_secret_name}")
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(f"loading secret name: {db_secret}")
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(f"sql: {sql}, parameters: {parameters}")
18
+ log.debug("sql: %s, parameters: %s", sql, parameters)
19
19
  try:
20
20
  cursor.execute(sql, parameters)
21
21
  except IntegrityError as err:
@@ -27,7 +27,7 @@ class PostgresCursor(Cursor):
27
27
  """
28
28
  from psycopg2 import Error, IntegrityError, ProgrammingError
29
29
 
30
- log.info(f"sql: {sql}")
30
+ log.info("sql: %s", sql)
31
31
 
32
32
  try:
33
33
  # Execute the SQL statement with parameters
@@ -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(f"selection_results: {self._selection_results}")
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(f"role: {role}, role_permissions: {role_permissions}")
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(f"allowed properties: {allowed_properties}")
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(f"role: {role}, role_permissions: {role_permissions}")
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(f"prop_name: {prop_name}, property: {property}")
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(f"allowed_properties: {allowed_properties}")
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(f"prefix_map: {self.prefix_map}")
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
- where = rule.get("where")
48
- if isinstance(where, str) and where.strip():
49
- filters.append(self._template_where(where))
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 Optional, Mapping, Any
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
- engine_config: Optional[Mapping[str, str]] = None
48
- query_engine: Optional[QueryEngine] = None
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(f"engine_config: {handler.engine_config}")
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.scope = scope or {}
80
- self.roles = roles or []
81
- self.groups = groups or []
82
- self.permissions = permissions or []
83
-
84
- self.subject = subject
85
- # Full set of claims (when available). Used for row-level filters
86
- # like where: "id = ${claims.sub}".
87
- self.claims = claims or ({"sub": subject} if subject is not None else {})
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(f"Topic ARN: {topic_arn}")
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(f"message_str: {message_str}")
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(f"publish msg id {msg_id}")
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(f"transaction exception: {error}")
49
- log.error(f"traceback: {traceback.format_exc()}")
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
- conversion_mapping = {
53
- "string": lambda x: x,
54
- "number": float,
55
- "float": float,
56
- "integer": int,
57
- "boolean": lambda x: x.lower() == "true",
58
- "date": lambda x: datetime.strptime(x, "%Y-%m-%d").date() if x else None,
59
- "date-time": lambda x: datetime.fromisoformat(x) if x else None,
60
- "time": lambda x: datetime.strptime(x, "%H:%M:%S").time() if x else None,
61
- }
62
- conversion_func = conversion_mapping.get(
63
- self.column_type if self.column_type is not None else "string", lambda x: x
64
- )
65
- return conversion_func(value)
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
- conversion_mapping = {
71
- "string": lambda x: x,
72
- "number": float,
73
- "float": float,
74
- "integer": int,
75
- "boolean": str,
76
- "date": lambda x: x.date().isoformat() if x else None,
77
- "date-time": lambda x: x.isoformat() if x else None,
78
- "time": lambda x: x.time().isoformat() if x else None,
79
- }
80
- conversion_func = conversion_mapping.get(
81
- self.api_type if self.api_type is not None else "string", lambda x: x
82
- )
83
- return conversion_func(value)
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
- print("building api_model")
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(f"Loaded API model: {api_model}")
326
+ log.info("Loaded API model: %s", api_model)