api-foundry-query-engine 0.0.6__tar.gz → 0.8.1__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 (32) hide show
  1. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/PKG-INFO +1 -1
  2. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/adapters/adapter.py +6 -3
  3. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/connectors/connection_factory.py +16 -9
  4. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/connectors/postgres_connection.py +11 -8
  5. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/dao/operation_dao.py +19 -10
  6. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/dao/sql_custom_query_handler.py +1 -1
  7. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/dao/sql_insert_query_handler.py +18 -12
  8. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/dao/sql_query_handler.py +6 -6
  9. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/dao/sql_select_query_handler.py +21 -18
  10. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/dao/sql_update_query_handler.py +16 -12
  11. api_foundry_query_engine-0.8.1/api_foundry_query_engine/lambda_handler.py +58 -0
  12. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/services/service.py +18 -6
  13. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/services/transactional_service.py +13 -9
  14. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/utils/api_model.py +79 -53
  15. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/pyproject.toml +1 -1
  16. api_foundry_query_engine-0.0.6/api_foundry_query_engine/lambda_handler.py +0 -43
  17. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/.gitignore +0 -0
  18. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/LICENSE +0 -0
  19. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/__init__.py +0 -0
  20. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
  21. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/adapters/gateway_adapter.py +0 -0
  22. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
  23. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/connectors/athena_connector.py +0 -0
  24. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/connectors/connection.py +0 -0
  25. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
  26. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/dao/dao.py +0 -0
  27. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/dao/sql_delete_query_handler.py +0 -0
  28. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +0 -0
  29. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/operation.py +0 -0
  30. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/services/security_service.py +0 -0
  31. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/api_foundry_query_engine/utils/app_exception.py +0 -0
  32. {api_foundry_query_engine-0.0.6 → api_foundry_query_engine-0.8.1}/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.0.6
3
+ Version: 0.8.1
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
@@ -1,5 +1,5 @@
1
1
  import abc
2
- from typing import Optional
2
+ from typing import Mapping, Optional
3
3
 
4
4
  from api_foundry_query_engine.operation import Operation
5
5
  from api_foundry_query_engine.services.transactional_service import TransactionalService
@@ -10,7 +10,9 @@ log = logger(__name__)
10
10
 
11
11
 
12
12
  class Adapter(metaclass=abc.ABCMeta):
13
+
13
14
  service: Service
15
+ config: Mapping[str, str]
14
16
 
15
17
  @classmethod
16
18
  def __subclasshook__(cls, __subclass: type) -> bool:
@@ -21,8 +23,9 @@ class Adapter(metaclass=abc.ABCMeta):
21
23
  and callable(__subclass.unmarshal)
22
24
  )
23
25
 
24
- def __init__(self, service: Optional[Service] = None) -> None:
25
- self.service = service if service is not None else TransactionalService()
26
+ def __init__(self, config: Mapping[str, str] = {}, service: Optional[Service] = None) -> None:
27
+ self.config = config
28
+ self.service = service if service is not None else TransactionalService(config=self.config)
26
29
 
27
30
  def unmarshal(self, event) -> Operation:
28
31
  """
@@ -1,3 +1,4 @@
1
+ from typing import Mapping
1
2
  import boto3
2
3
  import json
3
4
  import os
@@ -11,9 +12,11 @@ log = logger(__name__)
11
12
 
12
13
  class ConnectionFactory:
13
14
  db_config_map: dict[str, dict]
15
+ config: Mapping[str, str]
14
16
 
15
- def __init__(self):
17
+ def __init__(self, config: Mapping[str, str] = {}):
16
18
  self.db_config_map = dict()
19
+ self.config = config
17
20
 
18
21
  def get_connection(self, database: str) -> Connection:
19
22
  """
@@ -33,7 +36,11 @@ class ConnectionFactory:
33
36
  log.info(f"database: {database}")
34
37
  db_config = self.db_config_map.get(database)
35
38
  if not db_config:
36
- secret_name = json.loads(os.environ.get("SECRETS", "{}")).get(database)
39
+ # Use config dict for secrets
40
+ secrets_map = self.config.get("SECRETS", {})
41
+ if isinstance(secrets_map, str):
42
+ secrets_map = json.loads(secrets_map)
43
+ secret_name = secrets_map.get(database)
37
44
  log.debug(f"secret_name: {secret_name}")
38
45
 
39
46
  if secret_name:
@@ -66,15 +73,18 @@ class ConnectionFactory:
66
73
  Returns:
67
74
  - dict: The database configuration obtained from the secret.
68
75
  """
69
- endpoint_url = os.environ.get("AWS_ENDPOINT_URL") # LocalStack endpoint
76
+ if self.config.get(db_secret_name):
77
+ return self.config.get(db_secret_name)
78
+
79
+ endpoint_url = self.config.get("AWS_ENDPOINT_URL") # LocalStack endpoint
70
80
  sts_client = boto3.client("sts", endpoint_url=endpoint_url)
71
81
 
72
- secret_account_id = os.environ.get("SECRET_ACCOUNT_ID", None)
82
+ secret_account_id = self.config.get("SECRET_ACCOUNT_ID", None)
73
83
  log.debug(f"secret_account_id: {secret_account_id}")
74
84
 
75
85
  if secret_account_id:
76
86
  # If a secret account ID is provided, assume a role in that account
77
- secret_role = os.environ.get("ROLE_NAME", None)
87
+ secret_role = self.config.get("ROLE_NAME", None)
78
88
  assume_role_response = sts_client.assume_role(
79
89
  RoleArn=f"arn:aws:iam::{secret_account_id}:role/{secret_role}",
80
90
  RoleSessionName="AssumeRoleSession",
@@ -94,7 +104,7 @@ class ConnectionFactory:
94
104
  log.info(f"endpoint_url: {endpoint_url}")
95
105
  secretsmanager = boto3.client(
96
106
  "secretsmanager",
97
- # endpoint_url=endpoint_url,
107
+ endpoint_url=endpoint_url,
98
108
  )
99
109
 
100
110
  # Get the secret value from AWS Secrets Manager
@@ -104,6 +114,3 @@ class ConnectionFactory:
104
114
 
105
115
  # Return the parsed JSON secret string
106
116
  return json.loads(db_secret.get("SecretString"))
107
-
108
-
109
- connection_factory = ConnectionFactory()
@@ -9,17 +9,18 @@ class PostgresCursor(Cursor):
9
9
  def __init__(self, cursor):
10
10
  self.__cursor = cursor
11
11
 
12
- def execute(self, sql: str, parameters: dict, result_columns: list[str]) -> list:
12
+ def execute(self, sql: str, params: dict, selection_results: dict) -> list[dict]:
13
13
  """
14
14
  Execute SQL statements on the PostgreSQL database.
15
15
 
16
16
  Parameters:
17
17
  - cursor: The database cursor.
18
18
  - sql (str): The SQL statement to execute.
19
- - parameters (dict): Parameters to be used in the SQL statement.
19
+ - params (dict): Parameters to be used in the SQL statement.
20
+ - selection_results (dict): Mapping of result columns.
20
21
 
21
22
  Returns:
22
- - None
23
+ - list[dict]: List of result records as dictionaries.
23
24
 
24
25
  Raises:
25
26
  - AppException: Custom exception for handling database-related errors.
@@ -30,12 +31,12 @@ class PostgresCursor(Cursor):
30
31
 
31
32
  try:
32
33
  # Execute the SQL statement with parameters
33
- self.__cursor.execute(sql, parameters)
34
+ self.__cursor.execute(sql, params)
34
35
  result = []
35
36
  for record in self.__cursor:
36
- # Convert record tuple to dictionary using result_columns
37
+ # Convert record tuple to dictionary using selection_results
37
38
  result.append(
38
- {col: value for col, value in zip(result_columns, record)}
39
+ {col: value for col, value in zip(selection_results, record)}
39
40
  )
40
41
 
41
42
  return result
@@ -80,7 +81,7 @@ class PostgresConnection(Connection):
80
81
  """
81
82
  from psycopg2 import connect
82
83
 
83
- dbname = self.db_config["dbname"]
84
+ dbname = self.db_config["database"]
84
85
  user = self.db_config["username"]
85
86
  password = self.db_config["password"]
86
87
  host = self.db_config["host"]
@@ -98,7 +99,9 @@ class PostgresConnection(Connection):
98
99
 
99
100
  connection_params.update(additional_config)
100
101
 
101
- log.info(f"connection_params: dbname: {dbname}, user: {user}, host: {host}, port: {port}")
102
+ log.info(
103
+ f"connection_params: dbname: {dbname}, user: {user}, host: {host}, port: {port}"
104
+ )
102
105
 
103
106
  # Create a connection to the PostgreSQL database
104
107
  return connect(**connection_params)
@@ -20,10 +20,7 @@ from api_foundry_query_engine.utils.app_exception import ApplicationException
20
20
  from api_foundry_query_engine.dao.dao import DAO
21
21
  from api_foundry_query_engine.connectors.connection import Cursor
22
22
  from api_foundry_query_engine.operation import Operation
23
- from api_foundry_query_engine.utils.api_model import (
24
- get_schema_object,
25
- get_path_operation,
26
- )
23
+ from api_foundry_query_engine.utils.api_model import get_path_operation, get_schema_object
27
24
  from api_foundry_query_engine.dao.sql_query_handler import SQLQueryHandler
28
25
 
29
26
 
@@ -60,6 +57,10 @@ class OperationDAO(DAO):
60
57
  return self._query_handler
61
58
 
62
59
  schema_object = get_schema_object(self.operation.entity)
60
+ if not schema_object:
61
+ raise ApplicationException(
62
+ 500, f"Unknown operation: {self.operation.entity}"
63
+ )
63
64
  if self.operation.action == "read":
64
65
  self._query_handler = SQLSelectSchemaQueryHandler(
65
66
  self.operation, schema_object, self.engine
@@ -82,25 +83,33 @@ class OperationDAO(DAO):
82
83
  )
83
84
  return self._query_handler
84
85
 
85
- def execute(self, cursor: Cursor) -> Union[list[dict], dict]:
86
+ def execute(
87
+ self, connector, operation=None
88
+ ) -> Union[list[dict], dict]:
86
89
  """
87
- Execute the database operation based on the provided cursor.
90
+ Execute the database operation based on the provided connector.
88
91
 
89
92
  Args:
90
- cursor (Cursor): The database cursor.
93
+ connector (Connection): The database connection.
94
+ operation (Operation, optional): The operation to perform.
91
95
 
92
96
  Returns:
93
97
  list[dict]: A list of dictionaries containing the results
94
98
  of the operation.
95
99
  """
96
100
 
101
+ # Use self.operation if operation is not provided
102
+ op = operation if operation is not None else self.operation
103
+ # Assume connector has a 'cursor()' method to get a Cursor
104
+ cursor = connector.cursor()
105
+
97
106
  result = self.__fetch_record_set(self.query_handler, cursor)
98
107
 
99
- if self.operation.action == "read":
100
- if self.operation.metadata_params.get("count", False):
108
+ if op.action == "read":
109
+ if op.metadata_params.get("count", False):
101
110
  return result[0]
102
111
  self.__fetch_many(result, cursor)
103
- elif self.operation.action in ["update", "delete"] and len(result) == 0:
112
+ elif op.action in ["update", "delete"] and len(result) == 0:
104
113
  raise ApplicationException(400, "No records were modified")
105
114
 
106
115
  return result
@@ -30,7 +30,7 @@ class SQLCustomQueryHandler(SQLQueryHandler):
30
30
  return self._placeholders
31
31
 
32
32
  @property
33
- def select_list_columns(self) -> List[SchemaObjectProperty]:
33
+ def select_list_columns(self) -> List[str]:
34
34
  raise NotImplementedError()
35
35
 
36
36
  @property
@@ -70,22 +70,24 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
70
70
  placeholders = []
71
71
  columns = []
72
72
 
73
- allowed_properties = self.check_permissions(
73
+ allowed_property_names = self.check_permissions(
74
74
  "write", self.schema_object.permissions, self.schema_object.properties
75
75
  )
76
+ allowed_properties = {k: v for k, v in self.schema_object.properties.items() if k in allowed_property_names}
76
77
  log.info(f"allowed properties: {allowed_properties}")
78
+
79
+ import json
77
80
  for name, value in self.operation.store_params.items():
78
81
  parts = name.split(".")
79
82
 
80
- try:
81
- if len(parts) > 1:
82
- raise ApplicationException(
83
- 400,
84
- "Properties can not be set on associated objects " + name,
85
- )
83
+ if len(parts) > 1:
84
+ raise ApplicationException(
85
+ 400,
86
+ "Properties can not be set on associated objects " + name,
87
+ )
86
88
 
87
- property = allowed_properties[parts[0]]
88
- except KeyError:
89
+ property = allowed_properties.get(parts[0], None)
90
+ if property is None:
89
91
  if parts[0] not in self.schema_object.properties:
90
92
  raise ApplicationException(400, f"Invalid property: {name}")
91
93
  else:
@@ -95,10 +97,14 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
95
97
  )
96
98
 
97
99
  columns.append(property.column_name)
100
+ if property.api_name is None:
101
+ raise ApplicationException(400, f"Property '{name}' does not have a valid api_name.")
98
102
  placeholders.append(self.placeholder(property, property.api_name))
99
- self.store_placeholders[property.api_name] = property.convert_to_db_value(
100
- value
101
- )
103
+ # Serialize embedded objects to JSON
104
+ if property.api_type == "object":
105
+ self.store_placeholders[property.api_name] = json.dumps(value)
106
+ else:
107
+ self.store_placeholders[property.api_name] = property.convert_to_db_value(value)
102
108
 
103
109
  if self.key_property:
104
110
  if self.key_property.key_type == "sequence":
@@ -150,7 +150,7 @@ class SQLQueryHandler:
150
150
 
151
151
  def placeholder(self, property: SchemaObjectProperty, param: str = "") -> str:
152
152
  if len(param) == 0:
153
- param = property.api_name
153
+ param = property.api_name if property.api_name is not None else ""
154
154
 
155
155
  if self.engine == "oracle":
156
156
  if property.column_type == "date":
@@ -166,7 +166,7 @@ class SQLQueryHandler:
166
166
  self,
167
167
  permission_type: str,
168
168
  permissions: Optional[dict],
169
- properties: Optional[List[str]],
169
+ properties: Dict[str, SchemaObjectProperty],
170
170
  ) -> Dict[str, SchemaObjectProperty]:
171
171
  """
172
172
  Checks the user's permissions for the specified permission type.
@@ -240,7 +240,7 @@ class SQLQueryHandler:
240
240
  ]
241
241
  sql = f"{column} {'NOT ' if operand == 'not-in' else ''}IN ({', '.join(assignments)})" # noqa E501
242
242
  else:
243
- sql = f"{column} {operand} {self.placeholder(property, placeholder_name)}"
243
+ sql = f"{column} {operand} {self.placeholder(property, str(placeholder_name))}"
244
244
  return sql
245
245
 
246
246
  def generate_placeholders(
@@ -346,7 +346,7 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
346
346
 
347
347
  @property
348
348
  def table_expression(self) -> str:
349
- return self.schema_object.table_name
349
+ return self.schema_object.table_name or ""
350
350
 
351
351
  @property
352
352
  def selection_results(self) -> Dict:
@@ -393,9 +393,9 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
393
393
  raise ApplicationException(
394
394
  400,
395
395
  "Concurrency settings prohibit multi-record updates "
396
- + self.schema_object.api_name
396
+ + str(self.schema_object.api_name)
397
397
  + ", property: "
398
- + property.api_name,
398
+ + str(property.api_name),
399
399
  )
400
400
 
401
401
  assignment, holders = self.search_value_assignment(property, value)
@@ -50,7 +50,7 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
50
50
  raise ApplicationException(
51
51
  400,
52
52
  "Invalid selection property "
53
- + self.schema_object.api_name
53
+ + str(self.schema_object.api_name)
54
54
  + " does not have a property "
55
55
  + parts[0],
56
56
  )
@@ -59,7 +59,7 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
59
59
  raise ApplicationException(
60
60
  400,
61
61
  "Property not found, "
62
- + relation.child_schema_object.api_name
62
+ + str(relation.child_schema_object.api_name)
63
63
  + " does not have property "
64
64
  + parts[1],
65
65
  )
@@ -67,17 +67,20 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
67
67
  prefix = self.prefix_map[parts[0]]
68
68
  else:
69
69
  property = self.schema_object.properties[parts[0]]
70
+ if self.schema_object.api_name is None:
71
+ raise ApplicationException(
72
+ 500,
73
+ "schema_object.api_name is None, cannot use as key in prefix_map"
74
+ )
70
75
  prefix = self.prefix_map[self.schema_object.api_name]
71
76
  except KeyError:
72
77
  raise ApplicationException(
73
78
  500,
74
- (
75
- "Invalid query parameter, property not found. "
76
- + "schema object: "
77
- + self.schema_object.api_name
78
- + ", property: "
79
- + name
80
- ),
79
+ "Invalid query parameter, property not found. "
80
+ + "schema object: "
81
+ + str(self.schema_object.api_name)
82
+ + ", property: "
83
+ + name
81
84
  )
82
85
 
83
86
  assignment, holders = self.search_value_assignment(property, value, prefix)
@@ -90,13 +93,13 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
90
93
  @property
91
94
  def table_expression(self) -> str:
92
95
  joins = []
93
- parent_prefix = self.prefix_map[self.schema_object.api_name]
96
+ parent_prefix = self.prefix_map[str(self.schema_object.api_name)]
94
97
  for name, relation in self.schema_object.relations.items():
95
- child_prefix = self.prefix_map[relation.api_name]
98
+ child_prefix = self.prefix_map[str(relation.api_name)]
96
99
  if child_prefix in self.active_prefixes:
97
100
  joins.append(
98
101
  "INNER JOIN "
99
- + relation.child_schema_object.table_name
102
+ + str(relation.child_schema_object.table_name)
100
103
  + " AS "
101
104
  + child_prefix
102
105
  + " ON "
@@ -110,9 +113,9 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
110
113
  )
111
114
 
112
115
  return (
113
- self.schema_object.table_name
116
+ str(self.schema_object.table_name)
114
117
  + " AS "
115
- + self.prefix_map[self.schema_object.api_name]
118
+ + str(self.prefix_map[str(self.schema_object.api_name)])
116
119
  + (f" {' '.join(joins)}" if len(joins) > 0 else "")
117
120
  )
118
121
 
@@ -152,7 +155,7 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
152
155
  raise ApplicationException(
153
156
  400,
154
157
  "Bad object association: "
155
- + schema_object.api_name
158
+ + str(schema_object.api_name)
156
159
  + " does not have a "
157
160
  + relation
158
161
  + " property",
@@ -202,14 +205,14 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
202
205
  component = (
203
206
  parts[0]
204
207
  if len(parts) > 1
205
- else self.prefix_map[self.schema_object.api_name]
208
+ else self.prefix_map[str(self.schema_object.api_name)]
206
209
  )
207
210
  object = object_set.get(component, {})
208
211
  if not object:
209
212
  object_set[component] = object
210
213
  object[property.api_name] = property.convert_to_api_value(value)
211
214
 
212
- result = object_set[self.prefix_map[self.schema_object.api_name]]
215
+ result = object_set[self.prefix_map[str(self.schema_object.api_name)]]
213
216
  for name, prefix in self.prefix_map.items():
214
217
  if name != self.schema_object.api_name and prefix in object_set:
215
218
  result[name] = object_set[prefix]
@@ -239,7 +242,7 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
239
242
  # handle entity prefix
240
243
  field_parts = field_name.split(".")
241
244
  if len(field_parts) == 1:
242
- prefix = self.prefix_map[self.schema_object.api_name]
245
+ prefix = self.prefix_map[str(self.schema_object.api_name)]
243
246
  property = self.schema_object.properties.get(field_parts[0])
244
247
  if not property:
245
248
  raise ApplicationException(
@@ -39,28 +39,32 @@ class SQLUpdateSchemaQueryHandler(SQLSchemaQueryHandler):
39
39
 
40
40
  @property
41
41
  def update_values(self) -> str:
42
- allowed_properties = self.check_permissions(
42
+ allowed_property_names = self.check_permissions(
43
43
  "write", self.schema_object.permissions, self.schema_object.properties
44
44
  )
45
+ allowed_properties = {k: v for k, v in self.schema_object.properties.items() if k in allowed_property_names}
45
46
  self.store_placeholders = {}
46
47
  columns = []
47
48
  invalid_columns = []
48
49
 
50
+ import json
49
51
  for name, value in self.operation.store_params.items():
50
- if name not in allowed_properties:
52
+ property = allowed_properties.get(name, None)
53
+ if property is None:
51
54
  invalid_columns.append(name)
52
- else:
53
- property = allowed_properties.get(name, None)
55
+ continue
54
56
 
55
- placeholder = property.api_name
56
- column_name = property.column_name
57
+ placeholder = str(property.api_name) if property.api_name is not None else name
58
+ column_name = property.column_name
57
59
 
58
- columns.append(
59
- f"{column_name} = {self.placeholder(property, placeholder)}"
60
- )
61
- self.store_placeholders[placeholder] = property.convert_to_db_value(
62
- value
63
- )
60
+ columns.append(
61
+ f"{column_name} = {self.placeholder(property, placeholder)}"
62
+ )
63
+ # Serialize embedded objects to JSON
64
+ if property.api_type == "object":
65
+ self.store_placeholders[placeholder] = json.dumps(value)
66
+ else:
67
+ self.store_placeholders[placeholder] = property.convert_to_db_value(value)
64
68
 
65
69
  if invalid_columns:
66
70
  raise ApplicationException(
@@ -0,0 +1,58 @@
1
+ from fileinput import filename
2
+ import json
3
+ import logging
4
+ import os
5
+ from typing import Optional, Mapping
6
+ import yaml
7
+
8
+ from api_foundry_query_engine.utils.app_exception import ApplicationException
9
+ from api_foundry_query_engine.adapters.gateway_adapter import GatewayAdapter
10
+ from api_foundry_query_engine.utils.api_model import APIModel
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+ engine_config: Optional[Mapping[str, str]] = None
15
+ adapter: Optional[GatewayAdapter] = None
16
+
17
+ class QueryEngine:
18
+
19
+ def __init__(self, config: Mapping[str, str]):
20
+ self.adapter = GatewayAdapter(config)
21
+
22
+ def handler(self, event):
23
+ log.debug(f"event: {event}")
24
+ try:
25
+ response = self.adapter.process_event(event)
26
+
27
+ # Ensure the response conforms to API Gateway requirements
28
+ return {
29
+ "isBase64Encoded": False,
30
+ "statusCode": 200,
31
+ "headers": {"Content-Type": "application/json"},
32
+ "body": json.dumps(response),
33
+ }
34
+ except ApplicationException as e:
35
+ log.error(f"exception: {e}", exc_info=True)
36
+ return {
37
+ "isBase64Encoded": False,
38
+ "statusCode": e.status_code,
39
+ "headers": {"Content-Type": "application/json"},
40
+ "body": json.dumps({"message": f"exception: {e}"}),
41
+ }
42
+ except Exception as e:
43
+ log.error(f"exception: {e}", exc_info=True)
44
+ return {
45
+ "isBase64Encoded": False,
46
+ "statusCode": 500,
47
+ "headers": {"Content-Type": "application/json"},
48
+ "body": json.dumps({"message": f"exception: {e}"}),
49
+ }
50
+
51
+
52
+ def handler(event, _):
53
+ global engine_config, adapter
54
+ if engine_config is None:
55
+ engine_config = os.environ
56
+
57
+ query_engine = QueryEngine(engine_config)
58
+ return query_engine.handler(event)
@@ -8,24 +8,36 @@ from api_foundry_query_engine.operation import Operation
8
8
  log = logger(__name__)
9
9
 
10
10
 
11
+
11
12
  class Service:
13
+ def __init__(self, config: dict = {}):
14
+ self.config = config
15
+
12
16
  def execute(self, operation: Operation) -> list[dict]:
13
17
  raise NotImplementedError
14
18
 
15
19
 
20
+
16
21
  class ServiceAdapter(Service):
17
- def execute(self, operation):
18
- super().execute(operation)
22
+ def __init__(self, config: dict = {}):
23
+ super().__init__(config)
24
+
25
+ def execute(self, operation: Operation) -> list[dict]:
26
+ return super().execute(operation)
27
+
19
28
 
20
29
 
21
30
  class MutationPublisher(ServiceAdapter):
31
+ def __init__(self, config: dict = {}):
32
+ super().__init__(config)
33
+
22
34
  def execute(self, operation):
23
35
  result = super().execute(operation)
24
36
  self.publish_notification(operation)
25
37
  return result
26
38
 
27
39
  def publish_notification(self, operation):
28
- topic_arn = os.environ.get("BROADCAST_TOPIC", None)
40
+ topic_arn = self.config.get("BROADCAST_TOPIC", None)
29
41
  log.debug(f"Topic ARN: {topic_arn}")
30
42
 
31
43
  if topic_arn is not None:
@@ -51,10 +63,10 @@ class MutationPublisher(ServiceAdapter):
51
63
  )
52
64
  log.info(f"publish msg id {msg_id}")
53
65
 
54
- def __client(client_type, region: str = os.environ.get("AWS_REGION", "us-east-1")):
66
+ def __client(self, client_type):
55
67
  import boto3
56
-
57
- session = boto3.session.Session()
68
+ region = self.config.get("AWS_REGION", "us-east-1")
69
+ session = boto3.Session()
58
70
  if session:
59
71
  return session.client(client_type, region_name=region)
60
72
  return boto3.client(client_type, region_name=region)
@@ -1,10 +1,11 @@
1
1
  import traceback
2
+ from typing import Optional, Mapping
2
3
 
3
4
  from api_foundry_query_engine.utils.logger import logger
4
5
  from api_foundry_query_engine.utils.app_exception import ApplicationException
5
6
  from api_foundry_query_engine.operation import Operation
6
7
  from api_foundry_query_engine.services.service import ServiceAdapter
7
- from api_foundry_query_engine.connectors.connection_factory import connection_factory
8
+ from api_foundry_query_engine.connectors.connection_factory import ConnectionFactory
8
9
  from api_foundry_query_engine.dao.operation_dao import OperationDAO
9
10
  from api_foundry_query_engine.utils.api_model import (
10
11
  get_path_operation,
@@ -15,7 +16,12 @@ log = logger(__name__)
15
16
 
16
17
 
17
18
  class TransactionalService(ServiceAdapter):
18
- def execute(self, operation: Operation):
19
+ def __init__(self, config: Mapping[str, str]):
20
+ super().__init__()
21
+ self.config = config
22
+ self.connection_factory = ConnectionFactory(config)
23
+
24
+ def execute(self, operation: Operation) -> list[dict]:
19
25
  path_operation = get_path_operation(operation.entity, operation.action)
20
26
  if path_operation:
21
27
  database = path_operation.database
@@ -28,17 +34,15 @@ class TransactionalService(ServiceAdapter):
28
34
  500, f"Unknown operation: {operation.entity}"
29
35
  )
30
36
 
31
- connection = connection_factory.get_connection(database)
37
+ # Pass config to connection_factory if needed (future extension)
38
+ connection = self.connection_factory.get_connection(database)
32
39
 
33
40
  try:
34
- result = None
35
- cursor = connection.cursor()
36
- try:
37
- result = OperationDAO(operation, connection.engine()).execute(cursor)
38
- finally:
39
- cursor.close()
41
+ result = OperationDAO(operation, connection.engine()).execute(connection)
40
42
  if operation.action != "read":
41
43
  connection.commit()
44
+ if isinstance(result, dict):
45
+ return [result]
42
46
  return result
43
47
  except Exception as error:
44
48
  log.error(f"transaction exception: {error}")
@@ -1,12 +1,27 @@
1
+ import os
1
2
  import yaml
2
3
 
3
4
  from datetime import datetime
4
- from typing import Any, Dict, Optional
5
+ from typing import Any, Dict, Mapping, Optional
5
6
 
7
+ from api_foundry_query_engine.operation import Operation
6
8
  from api_foundry_query_engine.utils.logger import logger
7
9
 
8
10
  log = logger(__name__)
9
11
 
12
+ api_model = None
13
+
14
+ def get_schema_object(name: str) -> Optional["SchemaObject"]:
15
+ global api_model
16
+ if api_model is None:
17
+ return None
18
+ return api_model.schema_objects.get(name)
19
+
20
+ def get_path_operation(path: str, method: str) -> Optional["PathOperation"]:
21
+ global api_model
22
+ if api_model is None:
23
+ return None
24
+ return api_model.path_operations.get(f"{path}_{method}")
10
25
 
11
26
  class SchemaObjectProperty:
12
27
  """Represents a property of a schema object."""
@@ -42,7 +57,7 @@ class SchemaObjectProperty:
42
57
  "date-time": lambda x: datetime.fromisoformat(x) if x else None,
43
58
  "time": lambda x: datetime.strptime(x, "%H:%M:%S").time() if x else None,
44
59
  }
45
- conversion_func = conversion_mapping.get(self.column_type, lambda x: x)
60
+ conversion_func = conversion_mapping.get(self.column_type if self.column_type is not None else "string", lambda x: x)
46
61
  return conversion_func(value)
47
62
 
48
63
  def convert_to_api_value(self, value) -> Optional[Any]:
@@ -58,7 +73,7 @@ class SchemaObjectProperty:
58
73
  "date-time": lambda x: x.isoformat() if x else None,
59
74
  "time": lambda x: x.time().isoformat() if x else None,
60
75
  }
61
- conversion_func = conversion_mapping.get(self.api_type, lambda x: x)
76
+ conversion_func = conversion_mapping.get(self.api_type if self.api_type is not None else "string", lambda x: x)
62
77
  return conversion_func(value)
63
78
 
64
79
 
@@ -75,19 +90,33 @@ class SchemaObjectAssociation:
75
90
 
76
91
  @property
77
92
  def child_property(self) -> str:
78
- return (
79
- self._child_property
80
- if self._child_property
81
- else get_schema_object(self.schema_name).primary_key.column_name
82
- )
93
+ if self._child_property:
94
+ return self._child_property
95
+ if not self.schema_name:
96
+ raise ValueError("schema_name is None in SchemaObjectAssociation")
97
+ child_schema = get_schema_object(self.schema_name)
98
+ if not child_schema:
99
+ raise ValueError(f"SchemaObject '{self.schema_name}' not found")
100
+ if not child_schema.primary_key:
101
+ raise ValueError(f"Primary key not defined for schema '{self.schema_name}'")
102
+ column_name = getattr(child_schema.primary_key, "column_name", None)
103
+ if column_name is None:
104
+ raise ValueError(f"Primary key property does not have 'column_name' for schema '{self.schema_name}'")
105
+ return column_name
83
106
 
84
107
  @property
85
108
  def parent_property(self) -> str:
86
- return (
87
- self._parent_property
88
- if self._parent_property
89
- else get_schema_object(self.parent_schema).primary_key.column_name
90
- )
109
+ if self._parent_property:
110
+ return self._parent_property
111
+ parent_schema_obj = get_schema_object(self.parent_schema)
112
+ if not parent_schema_obj:
113
+ raise ValueError(f"SchemaObject '{self.parent_schema}' not found")
114
+ if not parent_schema_obj.primary_key:
115
+ raise ValueError(f"Primary key not defined for schema '{self.parent_schema}'")
116
+ column_name = getattr(parent_schema_obj.primary_key, "column_name", None)
117
+ if column_name is None:
118
+ raise ValueError(f"Primary key property does not have 'column_name' for schema '{self.parent_schema}'")
119
+ return column_name
91
120
 
92
121
  def __repr__(self):
93
122
  return (
@@ -98,30 +127,35 @@ class SchemaObjectAssociation:
98
127
 
99
128
  @property
100
129
  def child_schema_object(self) -> "SchemaObject":
101
- return get_schema_object(self.schema_name)
130
+ if self.schema_name is None:
131
+ raise ValueError("schema_name is None in SchemaObjectAssociation")
132
+ schema_obj = get_schema_object(self.schema_name)
133
+ if schema_obj is None:
134
+ raise ValueError(f"SchemaObject '{self.schema_name}' not found")
135
+ return schema_obj
102
136
 
103
137
 
104
138
  class SchemaObject:
105
139
  """Represents a schema object in the API configuration."""
106
140
 
107
141
  def __init__(self, data: Dict[str, Any]):
108
- self.api_name = data.get("api_name")
109
- self.database = data.get("database")
110
- self.table_name = data.get("table_name")
111
- self.properties = {
142
+ self.api_name: str = str(data.get("api_name"))
143
+ self.database: str = str(data.get("database"))
144
+ self.table_name: str = str(data.get("table_name"))
145
+ self.properties: Dict[str, SchemaObjectProperty] = {
112
146
  name: SchemaObjectProperty(prop_data)
113
147
  for name, prop_data in data.get("properties", {}).items()
114
148
  }
115
149
  self.relations = {
116
- name: SchemaObjectAssociation(self.api_name, assoc_data)
150
+ name: SchemaObjectAssociation(self.api_name if self.api_name is not None else "", assoc_data)
117
151
  for name, assoc_data in data.get("relations", {}).items()
118
152
  }
119
153
  self.concurrency_property = (
120
- self.properties[data.get("concurrency_property")]
154
+ self.properties[str(data.get("concurrency_property"))]
121
155
  if data.get("concurrency_property")
122
156
  else None
123
157
  )
124
- self._primary_key = data.get("primary_key")
158
+ self._primary_key: str = str(data.get("primary_key"))
125
159
  self.permissions = data.get("permissions")
126
160
 
127
161
  def __repr__(self):
@@ -136,39 +170,22 @@ class PathOperation:
136
170
  """Represents a path operation in the API configuration."""
137
171
 
138
172
  def __init__(self, data: Dict[str, Any]):
139
- self.entity = data["entity"]
140
- self.action = data["action"]
141
- self.sql = data["sql"]
142
- self.database = data["database"]
143
- self.inputs = {
173
+ self.entity: str = data["entity"]
174
+ self.action: str = data["action"]
175
+ self.sql: str = data["sql"]
176
+ self.database: str = data["database"]
177
+ self.inputs: Dict[str, SchemaObjectProperty] = {
144
178
  name: SchemaObjectProperty(input_data)
145
179
  for name, input_data in data.get("inputs", {}).items()
146
180
  }
147
- self.outputs = {
181
+ self.outputs: Dict[str, SchemaObjectProperty] = {
148
182
  name: SchemaObjectProperty(output_data)
149
183
  for name, output_data in data.get("outputs", {}).items()
150
184
  }
151
185
  self.permissions = data.get("security")
152
186
 
153
187
  def __repr__(self):
154
- return f"PathOperation(entity={self.entity}, method={self.method})"
155
-
156
-
157
- schema_objects = None
158
- path_operations = None
159
-
160
-
161
- def get_schema_object(name: str) -> Optional[SchemaObject]:
162
- """Returns a schema object by name."""
163
- global schema_objects
164
- return schema_objects.get(name)
165
-
166
-
167
- def get_path_operation(path: str, method: str) -> Optional[PathOperation]:
168
- """Returns a path operation by name."""
169
- log.info(f"path: {path}, method: {method}")
170
- global path_operations
171
- return path_operations.get(f"{path}_{method}")
188
+ return f"PathOperation(entity={self.entity}, action={self.action})"
172
189
 
173
190
 
174
191
  class APIModel:
@@ -176,24 +193,33 @@ class APIModel:
176
193
 
177
194
  def __init__(self, config: Dict[str, Any]):
178
195
  print("building api_model")
179
- global schema_objects
180
- schema_objects = {
196
+ self.schema_objects = {
181
197
  name: SchemaObject(schema_data)
182
198
  for name, schema_data in config.get("schema_objects", {}).items()
183
199
  }
184
- global path_operations
185
- path_operations = {
200
+ self.path_operations = {
186
201
  name: PathOperation(path_data)
187
202
  for name, path_data in config.get("path_operations", {}).items()
188
203
  }
189
204
 
205
+ def get_path_operation(self, path: str, method: str) -> Optional[PathOperation]:
206
+ """Returns a path operation by name."""
207
+ if self.path_operations is None:
208
+ return None
209
+ return self.path_operations.get(f"{path}_{method}")
210
+
211
+
190
212
  def __repr__(self):
191
213
  return (
192
214
  f"APIModel(schema_objects={list(self.schema_objects.keys())}, "
193
215
  + f"path_operations={list(self.path_operations.keys())})"
194
216
  )
195
217
 
196
-
197
- def load_api(filename: str):
198
- with open(filename, "r") as file:
199
- APIModel(yaml.safe_load(file))
218
+ def set_api_model(engine_config: Mapping[str, str]):
219
+ global api_model
220
+ if api_model is None:
221
+ if engine_config.get("API_SPEC"):
222
+ api_model = APIModel(yaml.safe_load(engine_config["API_SPEC"]))
223
+ else:
224
+ with open(os.environ.get("API_SPEC", "/var/task/api_spec.yaml"), "r") as file:
225
+ api_model = APIModel(yaml.safe_load(file))
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "api-foundry-query-engine"
7
- version = "0.0.6"
7
+ version = "0.8.1"
8
8
  authors = [
9
9
  { name="Daniel Repik", email="danrepik@icloud.com" },
10
10
  ]
@@ -1,43 +0,0 @@
1
- import json
2
- import logging
3
- import os
4
-
5
- from api_foundry_query_engine.utils.app_exception import ApplicationException
6
- from api_foundry_query_engine.adapters.gateway_adapter import GatewayAdapter
7
- from api_foundry_query_engine.utils.api_model import load_api
8
-
9
- log = logging.getLogger(__name__)
10
-
11
-
12
- load_api(os.environ.get("API_SPEC", "/var/task/api_spec.yaml"))
13
- adapter = GatewayAdapter()
14
-
15
-
16
- def handler(event, _):
17
- log.debug(f"event: {event}")
18
- try:
19
- response = adapter.process_event(event)
20
-
21
- # Ensure the response conforms to API Gateway requirements
22
- return {
23
- "isBase64Encoded": False,
24
- "statusCode": 200,
25
- "headers": {"Content-Type": "application/json"},
26
- "body": json.dumps(response),
27
- }
28
- except ApplicationException as e:
29
- log.error(f"exception: {e}", exc_info=True)
30
- return {
31
- "isBase64Encoded": False,
32
- "statusCode": e.status_code,
33
- "headers": {"Content-Type": "application/json"},
34
- "body": json.dumps({"message": f"exception: {e}"}),
35
- }
36
- except Exception as e:
37
- log.error(f"exception: {e}", exc_info=True)
38
- return {
39
- "isBase64Encoded": False,
40
- "statusCode": 500,
41
- "headers": {"Content-Type": "application/json"},
42
- "body": json.dumps({"message": f"exception: {e}"}),
43
- }