api-foundry-query-engine 0.8.0__tar.gz → 0.8.2__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.0 → api_foundry_query_engine-0.8.2}/.gitignore +2 -0
  2. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/PKG-INFO +1 -1
  3. api_foundry_query_engine-0.8.2/api_foundry_query_engine/.pre-commit-config.yaml +22 -0
  4. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/adapters/adapter.py +9 -3
  5. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/connectors/connection_factory.py +16 -10
  6. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/connectors/postgres_connection.py +8 -7
  7. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/dao/operation_dao.py +17 -7
  8. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/dao/sql_custom_query_handler.py +1 -1
  9. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/dao/sql_insert_query_handler.py +27 -12
  10. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/dao/sql_query_handler.py +6 -6
  11. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/dao/sql_select_query_handler.py +21 -18
  12. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/dao/sql_update_query_handler.py +20 -9
  13. api_foundry_query_engine-0.8.2/api_foundry_query_engine/lambda_handler.py +55 -0
  14. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/services/service.py +15 -6
  15. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/services/transactional_service.py +13 -9
  16. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/utils/api_model.py +94 -52
  17. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/pyproject.toml +1 -1
  18. api_foundry_query_engine-0.8.0/api_foundry_query_engine/lambda_handler.py +0 -43
  19. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/LICENSE +0 -0
  20. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/__init__.py +0 -0
  21. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
  22. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/adapters/gateway_adapter.py +0 -0
  23. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
  24. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/connectors/athena_connector.py +0 -0
  25. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/connectors/connection.py +0 -0
  26. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
  27. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/dao/dao.py +0 -0
  28. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/dao/sql_delete_query_handler.py +0 -0
  29. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +0 -0
  30. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/operation.py +0 -0
  31. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/services/security_service.py +0 -0
  32. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/utils/app_exception.py +0 -0
  33. {api_foundry_query_engine-0.8.0 → api_foundry_query_engine-0.8.2}/api_foundry_query_engine/utils/logger.py +0 -0
@@ -9,3 +9,5 @@ htmlcov/
9
9
  .coverage.*
10
10
  .coverage
11
11
  .vscode/
12
+ temp/
13
+ volume/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: api-foundry-query-engine
3
- Version: 0.8.0
3
+ Version: 0.8.2
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
@@ -0,0 +1,22 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v3.4.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ - id: check-added-large-files
9
+
10
+ - repo: https://github.com/psf/black
11
+ rev: 23.12.1
12
+ hooks:
13
+ - id: black
14
+ language_version: python3.11
15
+
16
+ - repo: https://github.com/PyCQA/flake8
17
+ rev: 7.0.0
18
+ hooks:
19
+ - id: flake8
20
+ args: [--max-line-length=120]
21
+
22
+ exclude: '^$'
@@ -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
@@ -11,6 +11,7 @@ log = logger(__name__)
11
11
 
12
12
  class Adapter(metaclass=abc.ABCMeta):
13
13
  service: Service
14
+ config: Mapping[str, str]
14
15
 
15
16
  @classmethod
16
17
  def __subclasshook__(cls, __subclass: type) -> bool:
@@ -21,8 +22,13 @@ class Adapter(metaclass=abc.ABCMeta):
21
22
  and callable(__subclass.unmarshal)
22
23
  )
23
24
 
24
- def __init__(self, service: Optional[Service] = None) -> None:
25
- self.service = service if service is not None else TransactionalService()
25
+ def __init__(
26
+ self, config: Mapping[str, str] = {}, service: Optional[Service] = None
27
+ ) -> None:
28
+ self.config = config
29
+ self.service = (
30
+ service if service is not None else TransactionalService(config=self.config)
31
+ )
26
32
 
27
33
  def unmarshal(self, event) -> Operation:
28
34
  """
@@ -1,6 +1,6 @@
1
+ from typing import Mapping
1
2
  import boto3
2
3
  import json
3
- import os
4
4
 
5
5
  from api_foundry_query_engine.connectors.connection import Connection
6
6
  from api_foundry_query_engine.utils.app_exception import ApplicationException
@@ -11,9 +11,11 @@ log = logger(__name__)
11
11
 
12
12
  class ConnectionFactory:
13
13
  db_config_map: dict[str, dict]
14
+ config: Mapping[str, str]
14
15
 
15
- def __init__(self):
16
+ def __init__(self, config: Mapping[str, str] = {}):
16
17
  self.db_config_map = dict()
18
+ self.config = config
17
19
 
18
20
  def get_connection(self, database: str) -> Connection:
19
21
  """
@@ -33,7 +35,11 @@ class ConnectionFactory:
33
35
  log.info(f"database: {database}")
34
36
  db_config = self.db_config_map.get(database)
35
37
  if not db_config:
36
- secret_name = json.loads(os.environ.get("SECRETS", "{}")).get(database)
38
+ # Use config dict for secrets
39
+ secrets_map = self.config.get("SECRETS", {})
40
+ if isinstance(secrets_map, str):
41
+ secrets_map = json.loads(secrets_map)
42
+ secret_name = secrets_map.get(database)
37
43
  log.debug(f"secret_name: {secret_name}")
38
44
 
39
45
  if secret_name:
@@ -66,15 +72,18 @@ class ConnectionFactory:
66
72
  Returns:
67
73
  - dict: The database configuration obtained from the secret.
68
74
  """
69
- endpoint_url = os.environ.get("AWS_ENDPOINT_URL") # LocalStack endpoint
75
+ if self.config.get(db_secret_name):
76
+ return self.config.get(db_secret_name)
77
+
78
+ endpoint_url = self.config.get("AWS_ENDPOINT_URL") # LocalStack endpoint
70
79
  sts_client = boto3.client("sts", endpoint_url=endpoint_url)
71
80
 
72
- secret_account_id = os.environ.get("SECRET_ACCOUNT_ID", None)
81
+ secret_account_id = self.config.get("SECRET_ACCOUNT_ID", None)
73
82
  log.debug(f"secret_account_id: {secret_account_id}")
74
83
 
75
84
  if secret_account_id:
76
85
  # If a secret account ID is provided, assume a role in that account
77
- secret_role = os.environ.get("ROLE_NAME", None)
86
+ secret_role = self.config.get("ROLE_NAME", None)
78
87
  assume_role_response = sts_client.assume_role(
79
88
  RoleArn=f"arn:aws:iam::{secret_account_id}:role/{secret_role}",
80
89
  RoleSessionName="AssumeRoleSession",
@@ -94,7 +103,7 @@ class ConnectionFactory:
94
103
  log.info(f"endpoint_url: {endpoint_url}")
95
104
  secretsmanager = boto3.client(
96
105
  "secretsmanager",
97
- # endpoint_url=endpoint_url,
106
+ endpoint_url=endpoint_url,
98
107
  )
99
108
 
100
109
  # Get the secret value from AWS Secrets Manager
@@ -104,6 +113,3 @@ class ConnectionFactory:
104
113
 
105
114
  # Return the parsed JSON secret string
106
115
  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"]
@@ -21,8 +21,8 @@ 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
23
  from api_foundry_query_engine.utils.api_model import (
24
- get_schema_object,
25
24
  get_path_operation,
25
+ get_schema_object,
26
26
  )
27
27
  from api_foundry_query_engine.dao.sql_query_handler import SQLQueryHandler
28
28
 
@@ -60,6 +60,10 @@ class OperationDAO(DAO):
60
60
  return self._query_handler
61
61
 
62
62
  schema_object = get_schema_object(self.operation.entity)
63
+ if not schema_object:
64
+ raise ApplicationException(
65
+ 500, f"Unknown operation: {self.operation.entity}"
66
+ )
63
67
  if self.operation.action == "read":
64
68
  self._query_handler = SQLSelectSchemaQueryHandler(
65
69
  self.operation, schema_object, self.engine
@@ -82,25 +86,31 @@ class OperationDAO(DAO):
82
86
  )
83
87
  return self._query_handler
84
88
 
85
- def execute(self, cursor: Cursor) -> Union[list[dict], dict]:
89
+ def execute(self, connector, operation=None) -> Union[list[dict], dict]:
86
90
  """
87
- Execute the database operation based on the provided cursor.
91
+ Execute the database operation based on the provided connector.
88
92
 
89
93
  Args:
90
- cursor (Cursor): The database cursor.
94
+ connector (Connection): The database connection.
95
+ operation (Operation, optional): The operation to perform.
91
96
 
92
97
  Returns:
93
98
  list[dict]: A list of dictionaries containing the results
94
99
  of the operation.
95
100
  """
96
101
 
102
+ # Use self.operation if operation is not provided
103
+ op = operation if operation is not None else self.operation
104
+ # Assume connector has a 'cursor()' method to get a Cursor
105
+ cursor = connector.cursor()
106
+
97
107
  result = self.__fetch_record_set(self.query_handler, cursor)
98
108
 
99
- if self.operation.action == "read":
100
- if self.operation.metadata_params.get("count", False):
109
+ if op.action == "read":
110
+ if op.metadata_params.get("count", False):
101
111
  return result[0]
102
112
  self.__fetch_many(result, cursor)
103
- elif self.operation.action in ["update", "delete"] and len(result) == 0:
113
+ elif op.action in ["update", "delete"] and len(result) == 0:
104
114
  raise ApplicationException(400, "No records were modified")
105
115
 
106
116
  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,29 @@ 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 = {
77
+ k: v
78
+ for k, v in self.schema_object.properties.items()
79
+ if k in allowed_property_names
80
+ }
76
81
  log.info(f"allowed properties: {allowed_properties}")
82
+
83
+ import json
84
+
77
85
  for name, value in self.operation.store_params.items():
78
86
  parts = name.split(".")
79
87
 
80
- try:
81
- if len(parts) > 1:
82
- raise ApplicationException(
83
- 400,
84
- "Properties can not be set on associated objects " + name,
85
- )
88
+ if len(parts) > 1:
89
+ raise ApplicationException(
90
+ 400,
91
+ "Properties can not be set on associated objects " + name,
92
+ )
86
93
 
87
- property = allowed_properties[parts[0]]
88
- except KeyError:
94
+ property = allowed_properties.get(parts[0], None)
95
+ if property is None:
89
96
  if parts[0] not in self.schema_object.properties:
90
97
  raise ApplicationException(400, f"Invalid property: {name}")
91
98
  else:
@@ -95,10 +102,18 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
95
102
  )
96
103
 
97
104
  columns.append(property.column_name)
105
+ if property.api_name is None:
106
+ raise ApplicationException(
107
+ 400, f"Property '{name}' does not have a valid api_name."
108
+ )
98
109
  placeholders.append(self.placeholder(property, property.api_name))
99
- self.store_placeholders[property.api_name] = property.convert_to_db_value(
100
- value
101
- )
110
+ # Serialize embedded objects to JSON
111
+ if property.api_type == "object":
112
+ self.store_placeholders[property.api_name] = json.dumps(value)
113
+ else:
114
+ self.store_placeholders[
115
+ property.api_name
116
+ ] = property.convert_to_db_value(value)
102
117
 
103
118
  if self.key_property:
104
119
  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,25 +39,36 @@ 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 = {
46
+ k: v
47
+ for k, v in self.schema_object.properties.items()
48
+ if k in allowed_property_names
49
+ }
45
50
  self.store_placeholders = {}
46
51
  columns = []
47
52
  invalid_columns = []
48
53
 
54
+ import json
55
+
49
56
  for name, value in self.operation.store_params.items():
50
- if name not in allowed_properties:
57
+ property = allowed_properties.get(name, None)
58
+ if property is None:
51
59
  invalid_columns.append(name)
52
- else:
53
- property = allowed_properties.get(name, None)
60
+ continue
54
61
 
55
- placeholder = property.api_name
56
- column_name = property.column_name
62
+ placeholder = (
63
+ str(property.api_name) if property.api_name is not None else name
64
+ )
65
+ column_name = property.column_name
57
66
 
58
- columns.append(
59
- f"{column_name} = {self.placeholder(property, placeholder)}"
60
- )
67
+ columns.append(f"{column_name} = {self.placeholder(property, placeholder)}")
68
+ # Serialize embedded objects to JSON
69
+ if property.api_type == "object":
70
+ self.store_placeholders[placeholder] = json.dumps(value)
71
+ else:
61
72
  self.store_placeholders[placeholder] = property.convert_to_db_value(
62
73
  value
63
74
  )
@@ -0,0 +1,55 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ from typing import Optional, Mapping
5
+
6
+ from api_foundry_query_engine.utils.app_exception import ApplicationException
7
+ from api_foundry_query_engine.adapters.gateway_adapter import GatewayAdapter
8
+
9
+ log = logging.getLogger(__name__)
10
+
11
+ engine_config: Optional[Mapping[str, str]] = None
12
+ adapter: Optional[GatewayAdapter] = None
13
+
14
+
15
+ class QueryEngine:
16
+ def __init__(self, config: Mapping[str, str]):
17
+ self.adapter = GatewayAdapter(config)
18
+
19
+ def handler(self, event):
20
+ log.debug(f"event: {event}")
21
+ try:
22
+ response = self.adapter.process_event(event)
23
+
24
+ # Ensure the response conforms to API Gateway requirements
25
+ return {
26
+ "isBase64Encoded": False,
27
+ "statusCode": 200,
28
+ "headers": {"Content-Type": "application/json"},
29
+ "body": json.dumps(response),
30
+ }
31
+ except ApplicationException as e:
32
+ log.error(f"exception: {e}", exc_info=True)
33
+ return {
34
+ "isBase64Encoded": False,
35
+ "statusCode": e.status_code,
36
+ "headers": {"Content-Type": "application/json"},
37
+ "body": json.dumps({"message": f"exception: {e}"}),
38
+ }
39
+ except Exception as e:
40
+ log.error(f"exception: {e}", exc_info=True)
41
+ return {
42
+ "isBase64Encoded": False,
43
+ "statusCode": 500,
44
+ "headers": {"Content-Type": "application/json"},
45
+ "body": json.dumps({"message": f"exception: {e}"}),
46
+ }
47
+
48
+
49
+ def handler(event, _):
50
+ global engine_config, adapter
51
+ if engine_config is None:
52
+ engine_config = os.environ
53
+
54
+ query_engine = QueryEngine(engine_config)
55
+ return query_engine.handler(event)
@@ -1,6 +1,5 @@
1
1
  import hashlib
2
2
  import json
3
- import os
4
3
 
5
4
  from api_foundry_query_engine.utils.logger import logger
6
5
  from api_foundry_query_engine.operation import Operation
@@ -9,23 +8,32 @@ log = logger(__name__)
9
8
 
10
9
 
11
10
  class Service:
11
+ def __init__(self, config: dict = {}):
12
+ self.config = config
13
+
12
14
  def execute(self, operation: Operation) -> list[dict]:
13
15
  raise NotImplementedError
14
16
 
15
17
 
16
18
  class ServiceAdapter(Service):
17
- def execute(self, operation):
18
- super().execute(operation)
19
+ def __init__(self, config: dict = {}):
20
+ super().__init__(config)
21
+
22
+ def execute(self, operation: Operation) -> list[dict]:
23
+ return super().execute(operation)
19
24
 
20
25
 
21
26
  class MutationPublisher(ServiceAdapter):
27
+ def __init__(self, config: dict = {}):
28
+ super().__init__(config)
29
+
22
30
  def execute(self, operation):
23
31
  result = super().execute(operation)
24
32
  self.publish_notification(operation)
25
33
  return result
26
34
 
27
35
  def publish_notification(self, operation):
28
- topic_arn = os.environ.get("BROADCAST_TOPIC", None)
36
+ topic_arn = self.config.get("BROADCAST_TOPIC", None)
29
37
  log.debug(f"Topic ARN: {topic_arn}")
30
38
 
31
39
  if topic_arn is not None:
@@ -51,10 +59,11 @@ class MutationPublisher(ServiceAdapter):
51
59
  )
52
60
  log.info(f"publish msg id {msg_id}")
53
61
 
54
- def __client(client_type, region: str = os.environ.get("AWS_REGION", "us-east-1")):
62
+ def __client(self, client_type):
55
63
  import boto3
56
64
 
57
- session = boto3.session.Session()
65
+ region = self.config.get("AWS_REGION", "us-east-1")
66
+ session = boto3.Session()
58
67
  if session:
59
68
  return session.client(client_type, region_name=region)
60
69
  return boto3.client(client_type, region_name=region)
@@ -1,10 +1,11 @@
1
1
  import traceback
2
+ from typing import 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,29 @@
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
 
6
7
  from api_foundry_query_engine.utils.logger import logger
7
8
 
8
9
  log = logger(__name__)
9
10
 
11
+ api_model = None
12
+
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
+
21
+ def get_path_operation(path: str, method: str) -> Optional["PathOperation"]:
22
+ global api_model
23
+ if api_model is None:
24
+ return None
25
+ return api_model.path_operations.get(f"{path}_{method}")
26
+
10
27
 
11
28
  class SchemaObjectProperty:
12
29
  """Represents a property of a schema object."""
@@ -42,7 +59,9 @@ class SchemaObjectProperty:
42
59
  "date-time": lambda x: datetime.fromisoformat(x) if x else None,
43
60
  "time": lambda x: datetime.strptime(x, "%H:%M:%S").time() if x else None,
44
61
  }
45
- conversion_func = conversion_mapping.get(self.column_type, lambda x: x)
62
+ conversion_func = conversion_mapping.get(
63
+ self.column_type if self.column_type is not None else "string", lambda x: x
64
+ )
46
65
  return conversion_func(value)
47
66
 
48
67
  def convert_to_api_value(self, value) -> Optional[Any]:
@@ -58,7 +77,9 @@ class SchemaObjectProperty:
58
77
  "date-time": lambda x: x.isoformat() if x else None,
59
78
  "time": lambda x: x.time().isoformat() if x else None,
60
79
  }
61
- conversion_func = conversion_mapping.get(self.api_type, lambda x: x)
80
+ conversion_func = conversion_mapping.get(
81
+ self.api_type if self.api_type is not None else "string", lambda x: x
82
+ )
62
83
  return conversion_func(value)
63
84
 
64
85
 
@@ -75,19 +96,39 @@ class SchemaObjectAssociation:
75
96
 
76
97
  @property
77
98
  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
- )
99
+ if self._child_property:
100
+ return self._child_property
101
+ if not self.schema_name:
102
+ raise ValueError("schema_name is None in SchemaObjectAssociation")
103
+ child_schema = get_schema_object(self.schema_name)
104
+ if not child_schema:
105
+ raise ValueError(f"SchemaObject '{self.schema_name}' not found")
106
+ if not child_schema.primary_key:
107
+ raise ValueError(f"Primary key not defined for schema '{self.schema_name}'")
108
+ column_name = getattr(child_schema.primary_key, "column_name", None)
109
+ if column_name is None:
110
+ raise ValueError(
111
+ f"Primary key property does not have 'column_name' for schema '{self.schema_name}'"
112
+ )
113
+ return column_name
83
114
 
84
115
  @property
85
116
  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
- )
117
+ if self._parent_property:
118
+ return self._parent_property
119
+ parent_schema_obj = get_schema_object(self.parent_schema)
120
+ if not parent_schema_obj:
121
+ raise ValueError(f"SchemaObject '{self.parent_schema}' not found")
122
+ if not parent_schema_obj.primary_key:
123
+ raise ValueError(
124
+ f"Primary key not defined for schema '{self.parent_schema}'"
125
+ )
126
+ column_name = getattr(parent_schema_obj.primary_key, "column_name", None)
127
+ if column_name is None:
128
+ raise ValueError(
129
+ f"Primary key property does not have 'column_name' for schema '{self.parent_schema}'"
130
+ )
131
+ return column_name
91
132
 
92
133
  def __repr__(self):
93
134
  return (
@@ -98,30 +139,37 @@ class SchemaObjectAssociation:
98
139
 
99
140
  @property
100
141
  def child_schema_object(self) -> "SchemaObject":
101
- return get_schema_object(self.schema_name)
142
+ if self.schema_name is None:
143
+ raise ValueError("schema_name is None in SchemaObjectAssociation")
144
+ schema_obj = get_schema_object(self.schema_name)
145
+ if schema_obj is None:
146
+ raise ValueError(f"SchemaObject '{self.schema_name}' not found")
147
+ return schema_obj
102
148
 
103
149
 
104
150
  class SchemaObject:
105
151
  """Represents a schema object in the API configuration."""
106
152
 
107
153
  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 = {
154
+ self.api_name: str = str(data.get("api_name"))
155
+ self.database: str = str(data.get("database"))
156
+ self.table_name: str = str(data.get("table_name"))
157
+ self.properties: Dict[str, SchemaObjectProperty] = {
112
158
  name: SchemaObjectProperty(prop_data)
113
159
  for name, prop_data in data.get("properties", {}).items()
114
160
  }
115
161
  self.relations = {
116
- name: SchemaObjectAssociation(self.api_name, assoc_data)
162
+ name: SchemaObjectAssociation(
163
+ self.api_name if self.api_name is not None else "", assoc_data
164
+ )
117
165
  for name, assoc_data in data.get("relations", {}).items()
118
166
  }
119
167
  self.concurrency_property = (
120
- self.properties[data.get("concurrency_property")]
168
+ self.properties[str(data.get("concurrency_property"))]
121
169
  if data.get("concurrency_property")
122
170
  else None
123
171
  )
124
- self._primary_key = data.get("primary_key")
172
+ self._primary_key: str = str(data.get("primary_key"))
125
173
  self.permissions = data.get("permissions")
126
174
 
127
175
  def __repr__(self):
@@ -136,39 +184,22 @@ class PathOperation:
136
184
  """Represents a path operation in the API configuration."""
137
185
 
138
186
  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 = {
187
+ self.entity: str = data["entity"]
188
+ self.action: str = data["action"]
189
+ self.sql: str = data["sql"]
190
+ self.database: str = data["database"]
191
+ self.inputs: Dict[str, SchemaObjectProperty] = {
144
192
  name: SchemaObjectProperty(input_data)
145
193
  for name, input_data in data.get("inputs", {}).items()
146
194
  }
147
- self.outputs = {
195
+ self.outputs: Dict[str, SchemaObjectProperty] = {
148
196
  name: SchemaObjectProperty(output_data)
149
197
  for name, output_data in data.get("outputs", {}).items()
150
198
  }
151
199
  self.permissions = data.get("security")
152
200
 
153
201
  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}")
202
+ return f"PathOperation(entity={self.entity}, action={self.action})"
172
203
 
173
204
 
174
205
  class APIModel:
@@ -176,17 +207,21 @@ class APIModel:
176
207
 
177
208
  def __init__(self, config: Dict[str, Any]):
178
209
  print("building api_model")
179
- global schema_objects
180
- schema_objects = {
210
+ self.schema_objects = {
181
211
  name: SchemaObject(schema_data)
182
212
  for name, schema_data in config.get("schema_objects", {}).items()
183
213
  }
184
- global path_operations
185
- path_operations = {
214
+ self.path_operations = {
186
215
  name: PathOperation(path_data)
187
216
  for name, path_data in config.get("path_operations", {}).items()
188
217
  }
189
218
 
219
+ def get_path_operation(self, path: str, method: str) -> Optional[PathOperation]:
220
+ """Returns a path operation by name."""
221
+ if self.path_operations is None:
222
+ return None
223
+ return self.path_operations.get(f"{path}_{method}")
224
+
190
225
  def __repr__(self):
191
226
  return (
192
227
  f"APIModel(schema_objects={list(self.schema_objects.keys())}, "
@@ -194,6 +229,13 @@ class APIModel:
194
229
  )
195
230
 
196
231
 
197
- def load_api(filename: str):
198
- with open(filename, "r") as file:
199
- APIModel(yaml.safe_load(file))
232
+ def set_api_model(engine_config: Mapping[str, str]):
233
+ global api_model
234
+ if api_model is None:
235
+ if engine_config.get("API_SPEC"):
236
+ api_model = APIModel(yaml.safe_load(engine_config["API_SPEC"]))
237
+ else:
238
+ with open(
239
+ os.environ.get("API_SPEC", "/var/task/api_spec.yaml"), "r"
240
+ ) as file:
241
+ 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.8.0"
7
+ version = "0.8.2"
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
- }