api-foundry-query-engine 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. api_foundry_query_engine/adapters/adapter.py +67 -0
  2. api_foundry_query_engine/adapters/case_change_adapter.py +79 -0
  3. api_foundry_query_engine/adapters/gateway_adapter.py +112 -0
  4. api_foundry_query_engine/connectors/connection.py +32 -0
  5. api_foundry_query_engine/connectors/connection_factory.py +110 -0
  6. api_foundry_query_engine/connectors/oracle_connector.py +29 -0
  7. api_foundry_query_engine/connectors/postgres_connection.py +105 -0
  8. api_foundry_query_engine/dao/dao.py +23 -0
  9. api_foundry_query_engine/dao/operation_dao.py +141 -0
  10. api_foundry_query_engine/dao/sql_custom_query_handler.py +66 -0
  11. api_foundry_query_engine/dao/sql_delete_query_handler.py +36 -0
  12. api_foundry_query_engine/dao/sql_insert_query_handler.py +102 -0
  13. api_foundry_query_engine/dao/sql_query_handler.py +386 -0
  14. api_foundry_query_engine/dao/sql_select_query_handler.py +305 -0
  15. api_foundry_query_engine/dao/sql_subselect_query_handler.py +63 -0
  16. api_foundry_query_engine/dao/sql_update_query_handler.py +59 -0
  17. api_foundry_query_engine/handler.py +48 -0
  18. api_foundry_query_engine/operation.py +15 -0
  19. api_foundry_query_engine/services/service.py +60 -0
  20. api_foundry_query_engine/services/transactional_service.py +43 -0
  21. api_foundry_query_engine/utils/api_model.py +175 -0
  22. api_foundry_query_engine/utils/app_exception.py +22 -0
  23. api_foundry_query_engine/utils/logger.py +60 -0
  24. api_foundry_query_engine-0.0.1.dist-info/METADATA +14 -0
  25. api_foundry_query_engine-0.0.1.dist-info/RECORD +27 -0
  26. api_foundry_query_engine-0.0.1.dist-info/WHEEL +4 -0
  27. api_foundry_query_engine-0.0.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,67 @@
1
+ import abc
2
+ from typing import Optional
3
+
4
+ from api_foundry_query_engine.operation import Operation
5
+ from api_foundry_query_engine.services.transactional_service import TransactionalService
6
+ from api_foundry_query_engine.utils.logger import logger
7
+ from api_foundry_query_engine.services.service import Service
8
+
9
+ log = logger(__name__)
10
+
11
+
12
+ class Adapter(metaclass=abc.ABCMeta):
13
+ service: Service
14
+
15
+ @classmethod
16
+ def __subclasshook__(cls, __subclass: type) -> bool:
17
+ return (
18
+ hasattr(__subclass, "marshal")
19
+ and callable(__subclass.marshal)
20
+ and hasattr(__subclass, "umarshal")
21
+ and callable(__subclass.unmarshal)
22
+ )
23
+
24
+ def __init__(self, service: Optional[Service] = None) -> None:
25
+ self.service = service if service is not None else TransactionalService()
26
+
27
+ def unmarshal(self, event) -> Operation:
28
+ """
29
+ Unmarshal the event in a tuple for processing
30
+
31
+ Parameters:
32
+ - event (dict): Lambda event object.
33
+
34
+ Returns:
35
+ - Operation containing the entity, action, and parameters
36
+ """
37
+ raise NotImplementedError
38
+
39
+ def marshal(self, result: list[dict]):
40
+ """
41
+ Marshal the result into a event response
42
+
43
+ Parameters:
44
+ - result (list): the data set to return in the response
45
+
46
+ Returns:
47
+ - the event response
48
+ """
49
+ return result
50
+
51
+ def process_event(self, event):
52
+ """
53
+ Process Lambda event using a domain function.
54
+
55
+ Parameters:
56
+ - service_function (callable): The service function to be executed.
57
+ - event (dict): Lambda event object.
58
+
59
+ Returns:
60
+ - any: Result of the domain function.
61
+ """
62
+ operation = self.unmarshal(event)
63
+
64
+ result = self.service.execute(operation)
65
+ log.debug(f"adapter result: {result}")
66
+
67
+ return self.marshal(result)
@@ -0,0 +1,79 @@
1
+ from humps import camelize, decamelize
2
+
3
+ from api_foundry_query_engine.adapters.adapter import Adapter
4
+ from api_foundry_query_engine.utils.logger import logger
5
+ from api_foundry_query_engine.operation import Operation
6
+
7
+ log = logger(__name__)
8
+
9
+
10
+ class CaseChangeAdapter(Adapter):
11
+ """
12
+ Handles changing case from snake to camel and back
13
+ """
14
+
15
+ def unmarshal(self, event) -> Operation:
16
+ """
17
+ Unmarshal the event in a tuple for processing
18
+
19
+ Parameters:
20
+ - event (dict): Lambda event object.
21
+
22
+ Returns:
23
+ - tuple: Tuple containing entity operation, store_params, query_params and metadata params.
24
+ """
25
+ operation = super().unmarshal(event)
26
+
27
+ # determine case
28
+ self.camel_case = (
29
+ operation.metadata_params.get("_case", "snake") == "camel"
30
+ or self.__check_camel_case(operation.store_params)
31
+ or self.__check_camel_case(operation.query_params)
32
+ )
33
+ log.info(f"camel_case: {self.camel_case}")
34
+
35
+ if self.camel_case:
36
+ return Operation(
37
+ path=operation.entity,
38
+ action=operation.action,
39
+ store_params=decamelize(operation.store_params),
40
+ query_params=decamelize(operation.query_params),
41
+ metadata_params=operation.metadata_params,
42
+ )
43
+
44
+ return operation
45
+
46
+ def marshal(self, result: list[dict]):
47
+ """
48
+ Marshal the result into a event response
49
+
50
+ Parameters:
51
+ - result (list): the data set to return in the response
52
+
53
+ Returns:
54
+ - the event response
55
+ """
56
+ super().marshal(result)
57
+
58
+ if not self.camel_case:
59
+ return result
60
+
61
+ converted_result = []
62
+ for item in result:
63
+ converted_result.append(camelize(item))
64
+
65
+ # convert back to camel case if needed
66
+ return converted_result
67
+
68
+ def __check_camel_case(self, params: dict) -> bool:
69
+ if params is not None:
70
+ # check the keys for an upper case character
71
+ for param in params:
72
+ if (
73
+ param != param.lower()
74
+ and param != param.upper()
75
+ and "_" not in param
76
+ ):
77
+ return True
78
+
79
+ return False
@@ -0,0 +1,112 @@
1
+ import json
2
+
3
+ from api_foundry_query_engine.adapters.adapter import Adapter
4
+ from api_foundry_query_engine.operation import Operation
5
+
6
+ actions_map = {
7
+ "GET": "read",
8
+ "POST": "create",
9
+ "PUT": "update",
10
+ "DELETE": "delete",
11
+ }
12
+
13
+
14
+ class GatewayAdapter(Adapter):
15
+ def marshal(self, result: list[dict]):
16
+ """
17
+ Marshal the result into a event response
18
+
19
+ Parameters:
20
+ - result (list): the data set to return in the response
21
+
22
+ Returns:
23
+ - the event response
24
+ """
25
+ return super().marshal(result)
26
+
27
+ def unmarshal(self, event):
28
+ """
29
+ Get parameters from the Lambda event.
30
+
31
+ Parameters:
32
+ - event (dict): Lambda event object.
33
+
34
+ Returns:
35
+ - tuple: Tuple containing data, query and metadata parameters.
36
+ """
37
+ entity = event.get("resource").split("/")[1]
38
+ action = actions_map.get(event.get("httpMethod").upper(), "read")
39
+
40
+ event_params = {}
41
+
42
+ path_parameters = self._convert_parameters(event.get("pathParameters"))
43
+ if path_parameters is not None:
44
+ event_params.update(path_parameters)
45
+
46
+ queryStringParameters = self._convert_parameters(
47
+ event.get("queryStringParameters")
48
+ )
49
+ if queryStringParameters is not None:
50
+ event_params.update(queryStringParameters)
51
+
52
+ query_params, metadata_params = self.split_params(event_params)
53
+
54
+ store_params = {}
55
+ body = event.get("body")
56
+ if body is not None and len(body) > 0:
57
+ store_params = json.loads(body)
58
+
59
+ return Operation(
60
+ entity=entity,
61
+ action=action,
62
+ store_params=store_params,
63
+ query_params=query_params,
64
+ metadata_params=metadata_params,
65
+ )
66
+
67
+ def _convert_parameters(self, parameters):
68
+ """
69
+ Convert parameters to appropriate types.
70
+
71
+ Parameters:
72
+ - parameters (dict): Dictionary of parameters.
73
+
74
+ Returns:
75
+ - dict: Dictionary with parameters converted to appropriate types.
76
+ """
77
+ if parameters is None:
78
+ return None
79
+
80
+ result = {}
81
+ for parameter, value in parameters.items():
82
+ try:
83
+ result[parameter] = int(value)
84
+ except ValueError:
85
+ try:
86
+ result[parameter] = float(value)
87
+ except ValueError:
88
+ result[parameter] = value
89
+ return result
90
+
91
+ def split_params(self, parameters: dict):
92
+ """
93
+ Split a dictionary into two dictionaries based on keys.
94
+
95
+ Parameters:
96
+ - dictionary (dict): Input dictionary.
97
+
98
+ Returns:
99
+ - tuple: A tuple containing two dictionaries.
100
+ The first dictionary contains metadata_params,
101
+ and the second dictionary query_params.
102
+ """
103
+ query_params = {}
104
+ metadata_params = {}
105
+
106
+ for key, value in parameters.items():
107
+ if key.startswith("__"):
108
+ metadata_params[key] = value
109
+ else:
110
+ query_params[key] = value
111
+
112
+ return query_params, metadata_params
@@ -0,0 +1,32 @@
1
+ from api_foundry_query_engine.utils.logger import logger
2
+
3
+ # Initialize the logger
4
+ log = logger(__name__)
5
+
6
+ db_config_map = dict()
7
+
8
+
9
+ class Cursor:
10
+ def execute(self, sql: str, params: dict, selection_results: dict) -> list[dict]:
11
+ raise NotImplementedError
12
+
13
+ def close(self):
14
+ raise NotImplementedError
15
+
16
+
17
+ class Connection:
18
+ def __init__(self, db_config: dict) -> None:
19
+ super().__init__()
20
+ self.db_config = db_config
21
+
22
+ def engine(self) -> str:
23
+ return self.db_config["engine"]
24
+
25
+ def cursor(self) -> Cursor:
26
+ raise NotImplementedError
27
+
28
+ def commit(self):
29
+ raise NotImplementedError
30
+
31
+ def close(self):
32
+ raise NotImplementedError
@@ -0,0 +1,110 @@
1
+ import boto3
2
+ import json
3
+ import os
4
+
5
+ from api_foundry_query_engine.connectors.connection import Connection
6
+ from api_foundry_query_engine.utils.app_exception import ApplicationException
7
+ from api_foundry_query_engine.utils.logger import logger
8
+
9
+ log = logger(__name__)
10
+
11
+
12
+ class ConnectionFactory:
13
+ db_config_map: dict[str, dict]
14
+
15
+ def __init__(self):
16
+ self.db_config_map = dict()
17
+
18
+ def get_connection(self, database: str) -> Connection:
19
+ """
20
+ Factory function to create a database connector based on the
21
+ specified engine and schema.
22
+
23
+ Args:
24
+ - engine (str): The database engine type
25
+ ('postgres', 'oracle', or 'mysql').
26
+ - schema (str): The schema for the database.
27
+
28
+ Returns:
29
+ - Connector: An instance of the appropriate Connector subclass.
30
+ """
31
+
32
+ # Get the secret name based on the engine and database from the secrets map
33
+ log.info(f"database: {database}")
34
+ db_config = self.db_config_map.get(database)
35
+ if not db_config:
36
+ secret_name = json.loads(os.environ.get("SECRETS", "{}")).get(database)
37
+ log.info(f"secret_name: {secret_name}")
38
+
39
+ if secret_name:
40
+ db_config = self.__get_secret(secret_name)
41
+ else:
42
+ raise ValueError(f"Secret not found for database: {database}")
43
+
44
+ engine = db_config.get("engine")
45
+ if not engine:
46
+ raise ApplicationException(
47
+ 500, "Database 'engine' is not defined in the secret."
48
+ )
49
+
50
+ if engine == "postgres":
51
+ from .postgres_connection import PostgresConnection
52
+
53
+ return PostgresConnection(db_config)
54
+
55
+ # Add support for other engines here if needed in the future
56
+
57
+ raise ValueError(f"Unsupported database engine: {engine}")
58
+
59
+ def __get_secret(self, db_secret_name: str):
60
+ """
61
+ Get the secret from AWS Secrets Manager.
62
+
63
+ Parameters:
64
+ - db_secret_name (str): The name of the AWS Secrets Manager secret.
65
+
66
+ Returns:
67
+ - dict: The database configuration obtained from the secret.
68
+ """
69
+ endpoint_url = os.environ.get("AWS_ENDPOINT_URL") # LocalStack endpoint
70
+ sts_client = boto3.client("sts", endpoint_url=endpoint_url)
71
+
72
+ secret_account_id = os.environ.get("SECRET_ACCOUNT_ID", None)
73
+ log.info(f"secret_account_id: {secret_account_id}")
74
+
75
+ if secret_account_id:
76
+ # If a secret account ID is provided, assume a role in that account
77
+ secret_role = os.environ.get("ROLE_NAME", None)
78
+ assume_role_response = sts_client.assume_role(
79
+ RoleArn=f"arn:aws:iam::{secret_account_id}:role/{secret_role}",
80
+ RoleSessionName="AssumeRoleSession",
81
+ )
82
+
83
+ credentials = assume_role_response["Credentials"]
84
+
85
+ secretsmanager = boto3.client(
86
+ "secretsmanager",
87
+ aws_access_key_id=credentials["AccessKeyId"],
88
+ aws_secret_access_key=credentials["SecretAccessKey"],
89
+ aws_session_token=credentials["SessionToken"],
90
+ endpoint_url=endpoint_url,
91
+ )
92
+ else:
93
+ # If no secret account ID is provided, use the default account
94
+ log.info(f"endpoint_url: {endpoint_url}")
95
+ secretsmanager = boto3.client(
96
+ "secretsmanager",
97
+ # endpoint_url=endpoint_url,
98
+ )
99
+
100
+ # Get the secret value from AWS Secrets Manager
101
+ log.info(f"db_secret_name: {db_secret_name}")
102
+ db_secret = secretsmanager.describe_secret(SecretId=db_secret_name)
103
+ db_secret = secretsmanager.get_secret_value(SecretId=db_secret_name)
104
+ log.debug(f"loading secret name: {db_secret}")
105
+
106
+ # Return the parsed JSON secret string
107
+ return json.loads(db_secret.get("SecretString"))
108
+
109
+
110
+ connection_factory = ConnectionFactory()
@@ -0,0 +1,29 @@
1
+ from api_foundry_query_engine.utils.logger import logger
2
+ from api_foundry_query_engine.utils.app_exception import ApplicationException
3
+ from api_foundry_query_engine.connectors.connection import Connector
4
+
5
+ log = logger(__name__)
6
+
7
+
8
+ class OracleConnnector(Connector):
9
+ def __init__(self, db_secret_name: str) -> None:
10
+ super().__init__(db_secret_name)
11
+
12
+ def close(self):
13
+ pass
14
+
15
+ def execute(self, cursor, sql: str, parameters: dict):
16
+ from oracledb import Error, IntegrityError, ProgrammingError
17
+
18
+ log.debug(f"sql: {sql}, parameters: {parameters}")
19
+ try:
20
+ cursor.execute(sql, parameters)
21
+ except IntegrityError as err:
22
+ (error,) = err.args
23
+ raise ApplicationException(409, error.message)
24
+ except ProgrammingError as err:
25
+ (error,) = err.args
26
+ raise ApplicationException(400, error.message)
27
+ except Error as err:
28
+ (error,) = err.args
29
+ raise ApplicationException(500, error.message)
@@ -0,0 +1,105 @@
1
+ from api_foundry_query_engine.connectors.connection import Connection, Cursor
2
+ from api_foundry_query_engine.utils.logger import logger
3
+
4
+ # Initialize the logger
5
+ log = logger(__name__)
6
+
7
+
8
+ class PostgresCursor(Cursor):
9
+ def __init__(self, cursor):
10
+ self.__cursor = cursor
11
+
12
+ def execute(self, sql: str, parameters: dict, result_columns: list[str]) -> list:
13
+ """
14
+ Execute SQL statements on the PostgreSQL database.
15
+
16
+ Parameters:
17
+ - cursor: The database cursor.
18
+ - sql (str): The SQL statement to execute.
19
+ - parameters (dict): Parameters to be used in the SQL statement.
20
+
21
+ Returns:
22
+ - None
23
+
24
+ Raises:
25
+ - AppException: Custom exception for handling database-related errors.
26
+ """
27
+ from psycopg2 import Error, IntegrityError, ProgrammingError
28
+
29
+ log.info(f"sql: {sql}, parameters: {parameters}")
30
+
31
+ try:
32
+ # Execute the SQL statement with parameters
33
+ log.info(f"sql: {self.__cursor.mogrify(sql, parameters)}")
34
+ self.__cursor.execute(sql, parameters)
35
+ result = []
36
+ for record in self.__cursor:
37
+ # Convert record tuple to dictionary using result_columns
38
+ result.append(
39
+ {col: value for col, value in zip(result_columns, record)}
40
+ )
41
+
42
+ return result
43
+ except IntegrityError as err:
44
+ # Handle integrity constraint violation (e.g., duplicate key)
45
+ raise Exception(409, err.pgerror)
46
+ except ProgrammingError as err:
47
+ # Handle programming errors (e.g., syntax error in SQL)
48
+ raise Exception(400, err.pgerror)
49
+ except Error as err:
50
+ # Handle other database errors
51
+ raise Exception(500, err.pgerror)
52
+
53
+ def close(self):
54
+ self.__cursor.close()
55
+
56
+
57
+ class PostgresConnection(Connection):
58
+ def __init__(self, db_config: dict) -> None:
59
+ super().__init__(db_config)
60
+ self.__connection = self.get_connection()
61
+
62
+ def cursor(self) -> Cursor:
63
+ return PostgresCursor(self.__connection.cursor())
64
+
65
+ def close(self):
66
+ self.__connection.close()
67
+
68
+ def commit(self):
69
+ self.__connection.commit()
70
+
71
+ def get_connection(self):
72
+ """
73
+ Get a connection to the PostgreSQL database.
74
+
75
+ Parameters:
76
+ - schema (str, optional): The database schema to set for the
77
+ connection.
78
+
79
+ Returns:
80
+ - connection: A connection to the PostgreSQL database.
81
+ """
82
+ from psycopg2 import connect
83
+
84
+ dbname = self.db_config["dbname"]
85
+ user = self.db_config["username"]
86
+ password = self.db_config["password"]
87
+ host = self.db_config["host"]
88
+ port = self.db_config.get("port", 5432)
89
+ additional_config = self.db_config.get("configuration", {})
90
+
91
+ # Merge additional configuration parameters with the main connection parameters
92
+ connection_params = {
93
+ "dbname": dbname,
94
+ "user": user,
95
+ "password": password,
96
+ "host": host,
97
+ "port": port,
98
+ }
99
+
100
+ connection_params.update(additional_config)
101
+
102
+ log.info(f"connection_params: {connection_params}")
103
+
104
+ # Create a connection to the PostgreSQL database
105
+ return connect(**connection_params)
@@ -0,0 +1,23 @@
1
+ import abc
2
+ from typing import Union
3
+
4
+ from api_foundry_query_engine.connectors.connection import Connection
5
+ from api_foundry_query_engine.operation import Operation
6
+
7
+
8
+ class DAO(metaclass=abc.ABCMeta):
9
+ @classmethod
10
+ def __subclasshook__(cls, __subclass: type) -> bool:
11
+ return hasattr(__subclass, "execute") and callable(__subclass.execute)
12
+
13
+ def execute(
14
+ self, connector: Connection, operation: Operation
15
+ ) -> Union[list[dict], dict]:
16
+ raise NotImplementedError
17
+
18
+
19
+ class DAOAdapter(DAO):
20
+ def execute(
21
+ self, connector: Connection, operation: Operation
22
+ ) -> Union[list[dict], dict]:
23
+ return super().execute(connector, operation)