api-foundry-query-engine 0.0.4__tar.gz → 0.0.6__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.4 → api_foundry_query_engine-0.0.6}/PKG-INFO +3 -2
  2. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/adapters/gateway_adapter.py +8 -0
  3. api_foundry_query_engine-0.0.6/api_foundry_query_engine/adapters/security_adapter.py +106 -0
  4. api_foundry_query_engine-0.0.6/api_foundry_query_engine/connectors/athena_connector.py +120 -0
  5. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/connectors/connection_factory.py +2 -3
  6. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/connectors/postgres_connection.py +2 -3
  7. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/operation_dao.py +19 -6
  8. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/sql_custom_query_handler.py +8 -6
  9. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/sql_delete_query_handler.py +35 -0
  10. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/sql_insert_query_handler.py +18 -3
  11. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/sql_query_handler.py +87 -22
  12. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/sql_select_query_handler.py +60 -47
  13. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +6 -3
  14. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/sql_update_query_handler.py +21 -10
  15. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/lambda_handler.py +0 -1
  16. api_foundry_query_engine-0.0.6/api_foundry_query_engine/operation.py +59 -0
  17. api_foundry_query_engine-0.0.6/api_foundry_query_engine/services/security_service.py +105 -0
  18. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/services/transactional_service.py +7 -2
  19. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/utils/api_model.py +26 -10
  20. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/pyproject.toml +1 -2
  21. api_foundry_query_engine-0.0.4/api_foundry_query_engine/operation.py +0 -15
  22. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/.gitignore +0 -0
  23. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/LICENSE +0 -0
  24. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/__init__.py +0 -0
  25. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/adapters/adapter.py +0 -0
  26. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
  27. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/connectors/connection.py +0 -0
  28. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
  29. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/dao/dao.py +0 -0
  30. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/services/service.py +0 -0
  31. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/utils/app_exception.py +0 -0
  32. {api_foundry_query_engine-0.0.4 → api_foundry_query_engine-0.0.6}/api_foundry_query_engine/utils/logger.py +0 -0
@@ -1,10 +1,11 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: api-foundry-query-engine
3
- Version: 0.0.4
3
+ Version: 0.0.6
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
7
7
  Author-email: Daniel Repik <danrepik@icloud.com>
8
+ License-File: LICENSE
8
9
  Classifier: Operating System :: OS Independent
9
10
  Classifier: Programming Language :: Python :: 3.9
10
11
  Requires-Python: >=3.9
@@ -56,12 +56,20 @@ class GatewayAdapter(Adapter):
56
56
  if body is not None and len(body) > 0:
57
57
  store_params = json.loads(body)
58
58
 
59
+ roles = []
60
+ authorizer_info = event.get("requestContext", {}).get("authorizer", {})
61
+ claims = authorizer_info.get("claims", {})
62
+ roles = claims.get("roles", [])
63
+ subject = claims.get("subject")
64
+
59
65
  return Operation(
60
66
  entity=entity,
61
67
  action=action,
62
68
  store_params=store_params,
63
69
  query_params=query_params,
64
70
  metadata_params=metadata_params,
71
+ roles=roles,
72
+ subject=subject,
65
73
  )
66
74
 
67
75
  def _convert_parameters(self, parameters):
@@ -0,0 +1,106 @@
1
+ from typing import Optional
2
+
3
+ from api_foundry_query_engine.operation import Operation
4
+ from api_foundry_query_engine.services.service import Service
5
+ from api_foundry_query_engine.adapters.adapter import Adapter
6
+ from api_foundry_query_engine.utils.logger import logger
7
+
8
+ log = logger(__name__)
9
+
10
+
11
+ class SecurityAdapter(Adapter):
12
+ def __init__(self, service: Optional[Service] = None, permissions: dict = None):
13
+ """
14
+ Initialize SecurityAdapter with a service and permissions.
15
+
16
+ Parameters:
17
+ - service (Service): The service used to process operations.
18
+ - permissions (dict): A dictionary containing the user's permissions,
19
+ structured as:
20
+ {
21
+ "read": ["field1", "field2", ...],
22
+ "write": ["field3", "field4", ...]
23
+ }
24
+ """
25
+ super().__init__(service)
26
+ self.permissions = permissions or {"read": [], "write": []}
27
+
28
+ def unmarshal(self, event) -> Operation:
29
+ """
30
+ Unmarshal the event into an Operation object after validating query and store params.
31
+
32
+ Parameters:
33
+ - event (dict): Lambda event object.
34
+
35
+ Returns:
36
+ - Operation: The validated operation.
37
+ """
38
+ entity = event.get("entity")
39
+ action = event.get("action")
40
+ query_params = event.get("query_params", {})
41
+ store_params = event.get("store_params", {})
42
+
43
+ # Validate read permissions for query_params
44
+ invalid_query_params = [
45
+ key for key in query_params if key not in self.permissions["read"]
46
+ ]
47
+ if invalid_query_params:
48
+ raise PermissionError(
49
+ f"Query parameters not permitted: {invalid_query_params}"
50
+ )
51
+
52
+ # Validate write permissions for store_params
53
+ invalid_store_params = [
54
+ key for key in store_params if key not in self.permissions["write"]
55
+ ]
56
+ if invalid_store_params:
57
+ raise PermissionError(
58
+ f"Store parameters not permitted: {invalid_store_params}"
59
+ )
60
+
61
+ return Operation(
62
+ entity=entity,
63
+ action=action,
64
+ query_params=query_params,
65
+ store_params=store_params,
66
+ )
67
+
68
+ def marshal(self, result: list[dict]):
69
+ """
70
+ Filter the result based on read permissions before returning.
71
+
72
+ Parameters:
73
+ - result (list[dict]): The data set to return in the response.
74
+
75
+ Returns:
76
+ - list[dict]: Filtered response with only permitted fields.
77
+ """
78
+ filtered_result = []
79
+ for record in result:
80
+ filtered_record = {
81
+ key: value
82
+ for key, value in record.items()
83
+ if key in self.permissions["read"]
84
+ }
85
+ filtered_result.append(filtered_record)
86
+
87
+ return filtered_result
88
+
89
+ def process_event(self, event):
90
+ """
91
+ Process Lambda event using a domain function.
92
+
93
+ Parameters:
94
+ - event (dict): Lambda event object.
95
+
96
+ Returns:
97
+ - any: Result of the domain function.
98
+ """
99
+ try:
100
+ operation = self.unmarshal(event)
101
+ result = self.service.execute(operation)
102
+ log.debug(f"adapter result: {result}")
103
+ return self.marshal(result)
104
+ except PermissionError as e:
105
+ log.error(f"Permission error: {e}")
106
+ return {"error": str(e), "status": "permission_denied"}
@@ -0,0 +1,120 @@
1
+ from api_foundry_query_engine.connectors.connection import Connection, Cursor
2
+ from api_foundry_query_engine.utils.logger import logger
3
+ import boto3
4
+ import time
5
+
6
+ # Initialize the logger
7
+ log = logger(__name__)
8
+
9
+
10
+ class AthenaCursor(Cursor):
11
+ def __init__(self, athena_client, db_config):
12
+ self.athena_client = athena_client
13
+ self.db_config = db_config
14
+
15
+ def execute(self, sql: str, parameters: dict, result_columns: list[str]) -> list:
16
+ """
17
+ Execute a SQL statement on the Athena database.
18
+
19
+ Parameters:
20
+ - sql (str): The SQL query to execute.
21
+ - parameters (dict): Parameters for the query (currently unused; substitute manually in the query).
22
+ - result_columns (list[str]): Column names for the results.
23
+
24
+ Returns:
25
+ - list[dict]: Query results as a list of dictionaries.
26
+ """
27
+ # Substitute parameters into the query manually (Athena doesn't support placeholders)
28
+ for key, value in parameters.items():
29
+ placeholder = f":{key}"
30
+ if isinstance(value, str):
31
+ value = f"'{value}'" # Wrap strings in quotes
32
+ sql = sql.replace(placeholder, str(value))
33
+
34
+ log.info(f"Executing query: {sql}")
35
+
36
+ # Start the query execution
37
+ response = self.athena_client.start_query_execution(
38
+ QueryString=sql,
39
+ QueryExecutionContext={"Database": self.db_config["database"]},
40
+ ResultConfiguration={"OutputLocation": self.db_config["output_location"]},
41
+ )
42
+ query_execution_id = response["QueryExecutionId"]
43
+ log.info(f"Query execution started: {query_execution_id}")
44
+
45
+ # Wait for the query to complete
46
+ while True:
47
+ status_response = self.athena_client.get_query_execution(
48
+ QueryExecutionId=query_execution_id
49
+ )
50
+ status = status_response["QueryExecution"]["Status"]["State"]
51
+
52
+ if status in ["SUCCEEDED", "FAILED", "CANCELLED"]:
53
+ break
54
+ time.sleep(2)
55
+
56
+ if status == "FAILED":
57
+ raise Exception(
58
+ f"Query failed: {status_response['QueryExecution']['Status']['StateChangeReason']}"
59
+ )
60
+ elif status == "CANCELLED":
61
+ raise Exception("Query was cancelled.")
62
+
63
+ # Fetch results
64
+ results_response = self.athena_client.get_query_results(
65
+ QueryExecutionId=query_execution_id
66
+ )
67
+ rows = results_response["ResultSet"]["Rows"]
68
+
69
+ # Convert rows to dictionaries
70
+ result = []
71
+ for row in rows[1:]: # Skip the first row (header row)
72
+ record = {
73
+ col: value.get("VarCharValue", None)
74
+ for col, value in zip(result_columns, row["Data"])
75
+ }
76
+ result.append(record)
77
+
78
+ return result
79
+
80
+ def close(self):
81
+ log.info("Closing Athena cursor (no persistent connection to close).")
82
+
83
+
84
+ class AthenaConnection(Connection):
85
+ def __init__(self, db_config: dict) -> None:
86
+ """
87
+ Initialize the AthenaConnection with the given configuration.
88
+
89
+ Parameters:
90
+ - db_config (dict): A dictionary containing Athena connection configuration.
91
+ Required keys:
92
+ - 'region': AWS region of the Athena service.
93
+ - 'output_location': S3 bucket location for query results.
94
+ - 'database': Athena database name.
95
+ """
96
+ super().__init__(db_config)
97
+ self.athena_client = self.get_athena_client()
98
+
99
+ def engine(self) -> str:
100
+ return "athena"
101
+
102
+ def cursor(self) -> Cursor:
103
+ return AthenaCursor(self.athena_client, self.db_config)
104
+
105
+ def commit(self):
106
+ # Athena queries are read-only; no commit operation is required
107
+ log.info("Athena does not support transactions. Commit is a no-op.")
108
+
109
+ def close(self):
110
+ # Athena uses a stateless API, so there's nothing to close
111
+ log.info("Closing Athena connection (no persistent connection to close).")
112
+
113
+ def get_athena_client(self):
114
+ """
115
+ Get a Boto3 client for Athena.
116
+ """
117
+ region = self.db_config.get("region")
118
+ if not region:
119
+ raise ValueError("Athena configuration must include 'region'.")
120
+ return boto3.client("athena", region_name=region)
@@ -34,7 +34,7 @@ class ConnectionFactory:
34
34
  db_config = self.db_config_map.get(database)
35
35
  if not db_config:
36
36
  secret_name = json.loads(os.environ.get("SECRETS", "{}")).get(database)
37
- log.info(f"secret_name: {secret_name}")
37
+ log.debug(f"secret_name: {secret_name}")
38
38
 
39
39
  if secret_name:
40
40
  db_config = self.__get_secret(secret_name)
@@ -70,7 +70,7 @@ class ConnectionFactory:
70
70
  sts_client = boto3.client("sts", endpoint_url=endpoint_url)
71
71
 
72
72
  secret_account_id = os.environ.get("SECRET_ACCOUNT_ID", None)
73
- log.info(f"secret_account_id: {secret_account_id}")
73
+ log.debug(f"secret_account_id: {secret_account_id}")
74
74
 
75
75
  if secret_account_id:
76
76
  # If a secret account ID is provided, assume a role in that account
@@ -99,7 +99,6 @@ class ConnectionFactory:
99
99
 
100
100
  # Get the secret value from AWS Secrets Manager
101
101
  log.info(f"db_secret_name: {db_secret_name}")
102
- db_secret = secretsmanager.describe_secret(SecretId=db_secret_name)
103
102
  db_secret = secretsmanager.get_secret_value(SecretId=db_secret_name)
104
103
  log.debug(f"loading secret name: {db_secret}")
105
104
 
@@ -26,11 +26,10 @@ class PostgresCursor(Cursor):
26
26
  """
27
27
  from psycopg2 import Error, IntegrityError, ProgrammingError
28
28
 
29
- log.info(f"sql: {sql}, parameters: {parameters}")
29
+ log.info(f"sql: {sql}")
30
30
 
31
31
  try:
32
32
  # Execute the SQL statement with parameters
33
- log.info(f"sql: {self.__cursor.mogrify(sql, parameters)}")
34
33
  self.__cursor.execute(sql, parameters)
35
34
  result = []
36
35
  for record in self.__cursor:
@@ -99,7 +98,7 @@ class PostgresConnection(Connection):
99
98
 
100
99
  connection_params.update(additional_config)
101
100
 
102
- log.info(f"connection_params: {connection_params}")
101
+ log.info(f"connection_params: dbname: {dbname}, user: {user}, host: {host}, port: {port}")
103
102
 
104
103
  # Create a connection to the PostgreSQL database
105
104
  return connect(**connection_params)
@@ -1,16 +1,29 @@
1
1
  from typing import Union
2
2
 
3
3
  from api_foundry_query_engine.dao.sql_custom_query_handler import SQLCustomQueryHandler
4
- from api_foundry_query_engine.dao.sql_delete_query_handler import SQLDeleteSchemaQueryHandler
5
- from api_foundry_query_engine.dao.sql_insert_query_handler import SQLInsertSchemaQueryHandler
6
- from api_foundry_query_engine.dao.sql_select_query_handler import SQLSelectSchemaQueryHandler
7
- from api_foundry_query_engine.dao.sql_subselect_query_handler import SQLSubselectSchemaQueryHandler
8
- from api_foundry_query_engine.dao.sql_update_query_handler import SQLUpdateSchemaQueryHandler
4
+ from api_foundry_query_engine.dao.sql_delete_query_handler import (
5
+ SQLDeleteSchemaQueryHandler,
6
+ )
7
+ from api_foundry_query_engine.dao.sql_insert_query_handler import (
8
+ SQLInsertSchemaQueryHandler,
9
+ )
10
+ from api_foundry_query_engine.dao.sql_select_query_handler import (
11
+ SQLSelectSchemaQueryHandler,
12
+ )
13
+ from api_foundry_query_engine.dao.sql_subselect_query_handler import (
14
+ SQLSubselectSchemaQueryHandler,
15
+ )
16
+ from api_foundry_query_engine.dao.sql_update_query_handler import (
17
+ SQLUpdateSchemaQueryHandler,
18
+ )
9
19
  from api_foundry_query_engine.utils.app_exception import ApplicationException
10
20
  from api_foundry_query_engine.dao.dao import DAO
11
21
  from api_foundry_query_engine.connectors.connection import Cursor
12
22
  from api_foundry_query_engine.operation import Operation
13
- from api_foundry_query_engine.utils.api_model import get_schema_object, get_path_operation
23
+ from api_foundry_query_engine.utils.api_model import (
24
+ get_schema_object,
25
+ get_path_operation,
26
+ )
14
27
  from api_foundry_query_engine.dao.sql_query_handler import SQLQueryHandler
15
28
 
16
29
 
@@ -33,9 +33,14 @@ class SQLCustomQueryHandler(SQLQueryHandler):
33
33
  def select_list_columns(self) -> List[SchemaObjectProperty]:
34
34
  raise NotImplementedError()
35
35
 
36
- def selection_result_map(self) -> Dict:
37
- log.info(f"outputs: {self.path_operation.outputs}")
38
- return self.path_operation.outputs
36
+ @property
37
+ def selection_results(self) -> Dict:
38
+ if not hasattr(self, "_selection_results"):
39
+ self._selection_results = self.check_permissions(
40
+ "read", self.path_operation.permissions, self.path_operation.outputs
41
+ )
42
+ log.debug(f"selection_results: {self._selection_results}")
43
+ return self._selection_results
39
44
 
40
45
  def _compile(self):
41
46
  placeholder_pattern = re.compile(r":(\w+)")
@@ -59,8 +64,5 @@ class SQLCustomQueryHandler(SQLQueryHandler):
59
64
  if placeholder_name in self.operation.query_params
60
65
  else property.default
61
66
  )
62
- log.info(f"placeholder_name: {placeholder_name}")
63
- log.info(f"value: {value}, default: {property.default}")
64
- log.info(f"placeholders: {self.generate_placeholders(property, value)}")
65
67
  self._placeholders.update(self.generate_placeholders(property, value))
66
68
  return self.placeholder(property, placeholder_name)
@@ -2,6 +2,9 @@ from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
2
2
  from api_foundry_query_engine.operation import Operation
3
3
  from api_foundry_query_engine.utils.app_exception import ApplicationException
4
4
  from api_foundry_query_engine.utils.api_model import SchemaObject
5
+ from api_foundry_query_engine.utils.logger import logger
6
+
7
+ log = logger(__name__)
5
8
 
6
9
 
7
10
  class SQLDeleteSchemaQueryHandler(SQLSchemaQueryHandler):
@@ -10,8 +13,40 @@ class SQLDeleteSchemaQueryHandler(SQLSchemaQueryHandler):
10
13
  ) -> None:
11
14
  super().__init__(operation, schema_object, engine)
12
15
 
16
+ def check_permission(self) -> bool:
17
+ """
18
+ Checks the user's permissions for the specified permission type.
19
+
20
+ Args:
21
+ permission_type (str): The type of permission to check ("read" or "write").
22
+ properties (List[str], optional): Specific properties to check. If None,
23
+ all schema properties are checked.
24
+
25
+ Returns:
26
+ List[str]: A list of properties the user is permitted to access.
27
+ """
28
+ # if permissions are not defined then no restrictions are applied
29
+ if not self.schema_object.permissions:
30
+ return True
31
+
32
+ for role in self.operation.roles:
33
+ role_permissions = self.schema_object.permissions.get(role, {})
34
+ log.info(f"role: {role}, role_permissions: {role_permissions}")
35
+ if len(role_permissions) == 0:
36
+ continue
37
+ allowed = role_permissions.get("delete", False)
38
+ if allowed:
39
+ return True
40
+
41
+ return False
42
+
13
43
  @property
14
44
  def sql(self) -> str:
45
+ if not self.check_permission():
46
+ raise ApplicationException(
47
+ 402, f"Subject is not allowed to delete {self.schema_object.api_name}"
48
+ )
49
+
15
50
  concurrency_property = self.schema_object.concurrency_property
16
51
  if concurrency_property:
17
52
  if not self.operation.query_params.get(concurrency_property.api_name):
@@ -3,6 +3,9 @@ from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
3
3
  from api_foundry_query_engine.operation import Operation
4
4
  from api_foundry_query_engine.utils.app_exception import ApplicationException
5
5
  from api_foundry_query_engine.utils.api_model import SchemaObject, SchemaObjectProperty
6
+ from api_foundry_query_engine.utils.logger import logger
7
+
8
+ log = logger(__name__)
6
9
 
7
10
 
8
11
  class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
@@ -67,6 +70,10 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
67
70
  placeholders = []
68
71
  columns = []
69
72
 
73
+ allowed_properties = self.check_permissions(
74
+ "write", self.schema_object.permissions, self.schema_object.properties
75
+ )
76
+ log.info(f"allowed properties: {allowed_properties}")
70
77
  for name, value in self.operation.store_params.items():
71
78
  parts = name.split(".")
72
79
 
@@ -77,13 +84,21 @@ class SQLInsertSchemaQueryHandler(SQLSchemaQueryHandler):
77
84
  "Properties can not be set on associated objects " + name,
78
85
  )
79
86
 
80
- property = self.schema_object.properties[parts[0]]
87
+ property = allowed_properties[parts[0]]
81
88
  except KeyError:
82
- raise ApplicationException(400, f"Invalid property: {name}")
89
+ if parts[0] not in self.schema_object.properties:
90
+ raise ApplicationException(400, f"Invalid property: {name}")
91
+ else:
92
+ raise ApplicationException(
93
+ 402,
94
+ f"Subject is not allowed to create with property: {parts[0]}",
95
+ )
83
96
 
84
97
  columns.append(property.column_name)
85
98
  placeholders.append(self.placeholder(property, property.api_name))
86
- self.store_placeholders[property.api_name] = property.convert_to_db_value(value)
99
+ self.store_placeholders[property.api_name] = property.convert_to_db_value(
100
+ value
101
+ )
87
102
 
88
103
  if self.key_property:
89
104
  if self.key_property.key_type == "sequence":
@@ -108,10 +108,12 @@ class SQLQueryHandler:
108
108
  operation: Operation
109
109
  engine: str
110
110
 
111
- def __init__(self, operation: Operation, engine: str):
111
+ def __init__(
112
+ self, operation: Operation, engine: str
113
+ ): # , schema_object: SchemaObject):
112
114
  self.operation = operation
113
- self.__select_list_columns = None
114
115
  self.engine = engine
116
+ self.__select_list_columns = None
115
117
 
116
118
  @property
117
119
  def sql(self) -> str:
@@ -124,14 +126,26 @@ class SQLQueryHandler:
124
126
  @property
125
127
  def select_list_columns(self) -> List[str]:
126
128
  if not self.__select_list_columns:
129
+ # Filter columns based on read permissions
127
130
  self.__select_list_columns = list(self.selection_results.keys())
128
131
  return self.__select_list_columns
129
132
 
130
133
  def marshal_record(self, record: dict) -> dict:
134
+ """
135
+ Converts a database record to an API-compatible dictionary,
136
+ removing properties the user is not allowed to read.
137
+
138
+ Args:
139
+ record (dict): A dictionary representing a single database record.
140
+
141
+ Returns:
142
+ dict: A dictionary with only the properties the user is allowed to see.
143
+ """
131
144
  result = {}
132
145
  for name, value in record.items():
133
- property = self.selection_results[name]
134
- result[property.api_name] = property.convert_to_api_value(value)
146
+ if name in self.selection_results: # Check if the property is allowed
147
+ property = self.selection_results[name]
148
+ result[property.api_name] = property.convert_to_api_value(value)
135
149
  return result
136
150
 
137
151
  def placeholder(self, property: SchemaObjectProperty, param: str = "") -> str:
@@ -148,11 +162,59 @@ class SQLQueryHandler:
148
162
  return f":{param}"
149
163
  return f"%({param})s"
150
164
 
165
+ def check_permissions(
166
+ self,
167
+ permission_type: str,
168
+ permissions: Optional[dict],
169
+ properties: Optional[List[str]],
170
+ ) -> Dict[str, SchemaObjectProperty]:
171
+ """
172
+ Checks the user's permissions for the specified permission type.
173
+
174
+ Args:
175
+ permission_type (str): The type of permission to check ("read" or "write").
176
+ properties (List[str], optional): Specific properties to check. If None,
177
+ all schema properties are checked.
178
+
179
+ Returns:
180
+ List[str]: A list of properties the user is permitted to access.
181
+ """
182
+ # if permissions are not defined then no restrictions are applied
183
+ log.info(
184
+ f"checking permissions permission_type: {permission_type}, permissions: {permissions}"
185
+ )
186
+ if not permissions:
187
+ return properties
188
+
189
+ allowed_properties = {}
190
+
191
+ for role in self.operation.roles:
192
+ role_permissions = permissions.get(role, {})
193
+ log.info(f"role: {role}, role_permissions: {role_permissions}")
194
+ if len(role_permissions) == 0:
195
+ continue
196
+ for prop_name, property in properties.items():
197
+ log.info(f"prop_name: {prop_name}, property: {property}")
198
+ if permission_type == "read" and re.match(
199
+ role_permissions.get("read", ""), prop_name
200
+ ):
201
+ allowed_properties[prop_name] = property
202
+ if permission_type == "write" and re.match(
203
+ role_permissions.get("write", ""), prop_name
204
+ ):
205
+ allowed_properties[prop_name] = property
206
+
207
+ log.info(f"allowed_properties: {allowed_properties}")
208
+ return allowed_properties
209
+
210
+ @property
211
+ def selection_results(self) -> Dict:
212
+ raise NotImplementedError()
213
+
151
214
  def generate_sql_condition(
152
215
  self, property: SchemaObjectProperty, value, prefix: Optional[str] = None
153
216
  ) -> str:
154
217
  operand = "="
155
-
156
218
  if isinstance(value, str):
157
219
  parts = value.split("::", 1)
158
220
  operand = RELATIONAL_TYPES.get(parts[0], "=") if len(parts) > 1 else "="
@@ -223,15 +285,6 @@ class SQLQueryHandler:
223
285
  placeholders = self.generate_placeholders(property, value, prefix)
224
286
  return sql_condition, placeholders
225
287
 
226
- @property
227
- def selection_results(self) -> Dict:
228
- if not hasattr(self, "_selection_results"):
229
- self._selection_results = self.selection_result_map()
230
- return self._selection_results
231
-
232
- def selection_result_map(self) -> Dict:
233
- raise NotImplementedError()
234
-
235
288
 
236
289
  class SQLSchemaQueryHandler(SQLQueryHandler):
237
290
  schema_object: SchemaObject
@@ -295,6 +348,26 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
295
348
  def table_expression(self) -> str:
296
349
  return self.schema_object.table_name
297
350
 
351
+ @property
352
+ def selection_results(self) -> Dict:
353
+ """
354
+ Filters the schema properties to include only those the user is allowed to read.
355
+
356
+ Returns:
357
+ dict: A dictionary of allowed schema properties.
358
+ """
359
+ log.info("selection_result")
360
+ if not hasattr(self, "__selection_results"):
361
+ log.info(f"prefix_map: {self.prefix_map}")
362
+ filters = self.operation.metadata_params.get("_properties", ".*").split()
363
+ allowed_properties = self.check_permissions(
364
+ "read", self.schema_object.permissions, self.schema_object.properties
365
+ )
366
+ self.__selection_results = self.filter_and_prefix_keys(
367
+ filters, allowed_properties
368
+ )
369
+ return self.__selection_results
370
+
298
371
  @property
299
372
  def search_condition(self) -> str:
300
373
  self.search_placeholders = {}
@@ -330,14 +403,6 @@ class SQLSchemaQueryHandler(SQLQueryHandler):
330
403
  self.search_placeholders.update(holders)
331
404
  return f" WHERE {' AND '.join(conditions)}" if conditions else ""
332
405
 
333
- def selection_result_map(self) -> dict:
334
- if not self.__selection_result_map:
335
- filters = self.operation.metadata_params.get("_properties", ".*").split()
336
- self.__selection_result_map = self.filter_and_prefix_keys(
337
- filters, self.schema_object.properties
338
- )
339
- return self.__selection_result_map
340
-
341
406
  def filter_and_prefix_keys(
342
407
  self, regex_list: List[str], properties: dict, prefix: Optional[str] = None
343
408
  ) -> dict:
@@ -1,10 +1,7 @@
1
1
  from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
2
2
  from api_foundry_query_engine.operation import Operation
3
3
  from api_foundry_query_engine.utils.app_exception import ApplicationException
4
- from api_foundry_query_engine.utils.api_model import (
5
- SchemaObject,
6
- SchemaObjectProperty
7
- )
4
+ from api_foundry_query_engine.utils.api_model import SchemaObject, SchemaObjectProperty
8
5
  from api_foundry_query_engine.utils.logger import logger
9
6
 
10
7
  log = logger(__name__)
@@ -119,52 +116,64 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
119
116
  + (f" {' '.join(joins)}" if len(joins) > 0 else "")
120
117
  )
121
118
 
122
- def selection_result_map(self) -> dict:
123
- if "count" in self.operation.metadata_params:
124
- self._selection_results = {
125
- "count": SchemaObjectProperty( {
126
- "api_name": "count",
127
- "api_type": "integer",
128
- "column_name": "count(*)",
129
- "column_type": "integer"
130
- } )
131
- }
132
- return self._selection_results
133
-
134
- filter_str = self.operation.metadata_params.get("properties", ".*")
135
- self._selection_results = {}
136
-
137
- for relation, reg_exs in self.get_regex_map(filter_str).items():
138
- # Extract the schema object for the current entity
139
- relation_property = self.schema_object.relations.get(relation)
140
-
141
- if relation_property:
142
- if relation_property.type == "array":
143
- continue
144
-
145
- # Use a default value if relation_property is None
146
- schema_object = relation_property.child_schema_object
147
- else:
148
- schema_object = self.schema_object
119
+ @property
120
+ def selection_results(self) -> dict:
121
+ if not hasattr(self, "_selection_results"):
122
+ self._selection_results = {}
123
+ if "count" in self.operation.metadata_params:
124
+ self._selection_results = {
125
+ "count": SchemaObjectProperty(
126
+ {
127
+ "api_name": "count",
128
+ "api_type": "integer",
129
+ "column_name": "count(*)",
130
+ "column_type": "integer",
131
+ }
132
+ )
133
+ }
134
+ return self._selection_results
149
135
 
150
- if relation not in self.prefix_map:
151
- raise ApplicationException(
152
- 400,
153
- "Bad object association: "
154
- + schema_object.api_name
155
- + " does not have a "
156
- + relation
157
- + " property",
136
+ filter_str = self.operation.metadata_params.get("properties", ".*")
137
+
138
+ for relation, reg_exs in self.get_regex_map(filter_str).items():
139
+ # Extract the schema object for the current entity
140
+ relation_property = self.schema_object.relations.get(relation)
141
+
142
+ if relation_property:
143
+ if relation_property.type == "array":
144
+ continue
145
+
146
+ # Use a default value if relation_property is None
147
+ schema_object = relation_property.child_schema_object
148
+ else:
149
+ schema_object = self.schema_object
150
+
151
+ if relation not in self.prefix_map:
152
+ raise ApplicationException(
153
+ 400,
154
+ "Bad object association: "
155
+ + schema_object.api_name
156
+ + " does not have a "
157
+ + relation
158
+ + " property",
159
+ )
160
+ # Filter and prefix keys for the current entity
161
+ # and regular expressions
162
+ allowed_properties = self.check_permissions(
163
+ "read", schema_object.permissions, schema_object.properties
164
+ )
165
+ filtered_keys = self.filter_and_prefix_keys(
166
+ reg_exs, allowed_properties, self.prefix_map[relation]
158
167
  )
159
- # Filter and prefix keys for the current entity
160
- # and regular expressions
161
- filtered_keys = self.filter_and_prefix_keys(
162
- reg_exs, schema_object.properties, self.prefix_map[relation]
163
- )
164
168
 
165
- # Extend the result map with the filtered keys
166
- self._selection_results.update(filtered_keys)
169
+ # Extend the result map with the filtered keys
170
+ self._selection_results.update(filtered_keys)
167
171
 
172
+ if len(self._selection_results) == 0:
173
+ raise ApplicationException(
174
+ 402,
175
+ "After applying permissions there are no properties returned in response",
176
+ )
168
177
  return self._selection_results
169
178
 
170
179
  def get_regex_map(self, filter_str: str) -> dict[str, list]:
@@ -190,7 +199,11 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
190
199
  for name, value in record.items():
191
200
  property = self.selection_results[name]
192
201
  parts = name.split(".")
193
- component = parts[0] if len(parts) > 1 else self.prefix_map[self.schema_object.api_name]
202
+ component = (
203
+ parts[0]
204
+ if len(parts) > 1
205
+ else self.prefix_map[self.schema_object.api_name]
206
+ )
194
207
  object = object_set.get(component, {})
195
208
  if not object:
196
209
  object_set[component] = object
@@ -1,7 +1,9 @@
1
1
  from typing import Optional
2
2
 
3
3
  from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
4
- from api_foundry_query_engine.dao.sql_select_query_handler import SQLSelectSchemaQueryHandler
4
+ from api_foundry_query_engine.dao.sql_select_query_handler import (
5
+ SQLSelectSchemaQueryHandler,
6
+ )
5
7
  from api_foundry_query_engine.operation import Operation
6
8
  from api_foundry_query_engine.utils.api_model import SchemaObjectAssociation
7
9
 
@@ -19,8 +21,9 @@ class SQLSubselectSchemaQueryHandler(SQLSelectSchemaQueryHandler):
19
21
  self.relation = relation
20
22
  self.parent_generator = parent_generator
21
23
 
22
- def selection_result_map(self) -> dict:
23
- filter_str = self.operation.metadata_params.get("properties", "")
24
+ @property
25
+ def selection_results(self) -> dict:
26
+ filter_str = self.operation.metadata_params.get("properties", ".*")
24
27
  result = {self.relation.child_property: self.relation.child_property}
25
28
 
26
29
  for relation_name, reg_exs in self.get_regex_map(filter_str).items():
@@ -39,21 +39,32 @@ class SQLUpdateSchemaQueryHandler(SQLSchemaQueryHandler):
39
39
 
40
40
  @property
41
41
  def update_values(self) -> str:
42
+ allowed_properties = self.check_permissions(
43
+ "write", self.schema_object.permissions, self.schema_object.properties
44
+ )
42
45
  self.store_placeholders = {}
43
46
  columns = []
47
+ invalid_columns = []
44
48
 
45
49
  for name, value in self.operation.store_params.items():
46
- try:
47
- property = self.schema_object.properties[name]
48
- except KeyError:
49
- raise ApplicationException(
50
- 400, f"Search condition column not found {name}"
51
- )
50
+ if name not in allowed_properties:
51
+ invalid_columns.append(name)
52
+ else:
53
+ property = allowed_properties.get(name, None)
52
54
 
53
- placeholder = property.api_name
54
- column_name = property.column_name
55
+ placeholder = property.api_name
56
+ column_name = property.column_name
55
57
 
56
- columns.append(f"{column_name} = {self.placeholder(property, placeholder)}")
57
- self.store_placeholders[placeholder] = property.convert_to_db_value(value)
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
+ )
58
64
 
65
+ if invalid_columns:
66
+ raise ApplicationException(
67
+ 402,
68
+ f"Subject does not have permission to update properties: {invalid_columns}",
69
+ )
59
70
  return f" SET {', '.join(columns)}"
@@ -41,4 +41,3 @@ def handler(event, _):
41
41
  "headers": {"Content-Type": "application/json"},
42
42
  "body": json.dumps({"message": f"exception: {e}"}),
43
43
  }
44
-
@@ -0,0 +1,59 @@
1
+ class Operation:
2
+ """
3
+ Represents an action to be performed on an entity.
4
+
5
+ The `Operation` class encapsulates the details required to execute an operation
6
+ on a given entity, including query parameters for selecting records, parameters
7
+ for storing or updating values, metadata for operational instructions, and
8
+ roles defining the contexts in which the operation can be performed.
9
+
10
+ Attributes:
11
+ entity (str): The name of the resource or object being targeted (e.g., "User", "Order").
12
+ action (str): The type of action being performed (e.g., "create", "read", "update", "delete").
13
+ query_params (dict): Parameters used to filter or select the records affected by the operation.
14
+ store_params (dict): Parameters that define the values to be stored or updated for the selected records.
15
+ metadata_params (dict): Additional instructions for the operation, such as sorting or pagination.
16
+ roles (dict): Defines the roles under which the operation is allowed to be performed.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ entity: str,
23
+ action: str,
24
+ query_params: dict = {},
25
+ store_params: dict = {},
26
+ metadata_params: dict = {},
27
+ roles: dict = {},
28
+ subject: str = None,
29
+ ):
30
+ """
31
+ Initializes the Operation instance.
32
+
33
+ Args:
34
+ entity (str): The name of the entity to perform the operation on.
35
+ action (str): The action to perform on the entity.
36
+ query_params (dict, optional): Parameters for selecting affected records (default: {}).
37
+ store_params (dict, optional): Parameters defining new or updated values (default: {}).
38
+ metadata_params (dict, optional): Metadata for the operation, such as sorting or pagination (default: {}).
39
+ roles (dict, optional): Defines the roles allowed to perform the operation (default: {}).
40
+ """
41
+ # The target entity for the operation (e.g., "User", "Order").
42
+ self.entity = entity
43
+
44
+ # The type of action to perform (e.g., "create", "read", "update", "delete").
45
+ self.action = action
46
+
47
+ # Query parameters to filter or identify the affected records.
48
+ self.query_params = query_params
49
+
50
+ # Parameters defining the values to be stored or updated for the operation.
51
+ self.store_params = store_params
52
+
53
+ # Metadata for operational instructions like sorting, limiting, or offsetting results.
54
+ self.metadata_params = metadata_params
55
+
56
+ # Roles defining the context in which the operation is allowed.
57
+ self.roles = roles
58
+
59
+ self.subject = subject
@@ -0,0 +1,105 @@
1
+ from api_foundry_query_engine.utils.logger import logger
2
+ from api_foundry_query_engine.operation import Operation
3
+ from api_foundry_query_engine.services.service import ServiceAdapter
4
+
5
+ log = logger(__name__)
6
+
7
+
8
+ class SecurityService(ServiceAdapter):
9
+ def execute(self, operation: Operation) -> list[dict]:
10
+ """
11
+ Execute the operation after validating security instructions.
12
+
13
+ Parameters:
14
+ - operation (Operation): The operation to execute.
15
+
16
+ Returns:
17
+ - list[dict]: The result of the operation, filtered based on security rules.
18
+ """
19
+ log.debug(f"Executing operation: {operation}")
20
+
21
+ # Resolve the schema or path operation to retrieve the `security` attribute
22
+ schema_or_path = self.schema_or_path_resolver(
23
+ operation.entity, operation.action
24
+ )
25
+ if not schema_or_path or "security" not in schema_or_path:
26
+ raise ValueError(
27
+ f"No security instructions found for entity {operation.entity}"
28
+ )
29
+
30
+ security_rules = schema_or_path["security"]
31
+
32
+ # Validate query and store parameters
33
+ self._validate_query_params(operation.query_params, security_rules)
34
+ self._validate_store_params(operation.store_params, security_rules)
35
+
36
+ # Execute the next service
37
+ result = self.next_service.execute(operation)
38
+
39
+ # Filter the result based on security rules
40
+ filtered_result = self._filter_result(result, security_rules)
41
+ return filtered_result
42
+
43
+ def _validate_query_params(self, query_params: dict, security_rules: dict):
44
+ """
45
+ Validate query parameters against read permissions.
46
+
47
+ Parameters:
48
+ - query_params (dict): Query parameters to validate.
49
+ - security_rules (dict): Security rules for validation.
50
+
51
+ Raises:
52
+ - PermissionError: If a query parameter violates read permissions.
53
+ """
54
+ read_permissions = security_rules.get("read", [])
55
+ invalid_params = [
56
+ key
57
+ for key in query_params
58
+ if key not in read_permissions and read_permissions != ["*"]
59
+ ]
60
+ if invalid_params:
61
+ raise PermissionError(f"Query parameters not permitted: {invalid_params}")
62
+
63
+ def _validate_store_params(self, store_params: dict, security_rules: dict):
64
+ """
65
+ Validate store parameters against write permissions.
66
+
67
+ Parameters:
68
+ - store_params (dict): Store parameters to validate.
69
+ - security_rules (dict): Security rules for validation.
70
+
71
+ Raises:
72
+ - PermissionError: If a store parameter violates write permissions.
73
+ """
74
+ write_permissions = security_rules.get("write", [])
75
+ invalid_params = [
76
+ key
77
+ for key in store_params
78
+ if key not in write_permissions and write_permissions != ["*"]
79
+ ]
80
+ if invalid_params:
81
+ raise PermissionError(f"Store parameters not permitted: {invalid_params}")
82
+
83
+ def _filter_result(self, result: list[dict], security_rules: dict) -> list[dict]:
84
+ """
85
+ Filter the result based on read permissions.
86
+
87
+ Parameters:
88
+ - result (list[dict]): The original result set.
89
+ - security_rules (dict): Security rules for filtering.
90
+
91
+ Returns:
92
+ - list[dict]: The filtered result set.
93
+ """
94
+ read_permissions = security_rules.get("read", [])
95
+ if read_permissions == ["*"]:
96
+ return result # No filtering needed
97
+
98
+ filtered_result = []
99
+ for record in result:
100
+ filtered_record = {
101
+ key: value for key, value in record.items() if key in read_permissions
102
+ }
103
+ filtered_result.append(filtered_record)
104
+
105
+ return filtered_result
@@ -6,7 +6,10 @@ from api_foundry_query_engine.operation import Operation
6
6
  from api_foundry_query_engine.services.service import ServiceAdapter
7
7
  from api_foundry_query_engine.connectors.connection_factory import connection_factory
8
8
  from api_foundry_query_engine.dao.operation_dao import OperationDAO
9
- from api_foundry_query_engine.utils.api_model import get_path_operation, get_schema_object
9
+ from api_foundry_query_engine.utils.api_model import (
10
+ get_path_operation,
11
+ get_schema_object,
12
+ )
10
13
 
11
14
  log = logger(__name__)
12
15
 
@@ -21,7 +24,9 @@ class TransactionalService(ServiceAdapter):
21
24
  if schema_object:
22
25
  database = schema_object.database
23
26
  else:
24
- raise ApplicationException(500, f"Unknown operation: {operation.entity}")
27
+ raise ApplicationException(
28
+ 500, f"Unknown operation: {operation.entity}"
29
+ )
25
30
 
26
31
  connection = connection_factory.get_connection(database)
27
32
 
@@ -1,4 +1,3 @@
1
-
2
1
  import yaml
3
2
 
4
3
  from datetime import datetime
@@ -29,7 +28,7 @@ class SchemaObjectProperty:
29
28
 
30
29
  def __repr__(self):
31
30
  return f"SchemaObjectProperty(api_name={self.api_name}, column_name={self.column_name}, type={self.type})"
32
-
31
+
33
32
  def convert_to_db_value(self, value: str) -> Optional[Any]:
34
33
  if value is None:
35
34
  return None
@@ -63,7 +62,6 @@ class SchemaObjectProperty:
63
62
  return conversion_func(value)
64
63
 
65
64
 
66
-
67
65
  class SchemaObjectAssociation:
68
66
  """Represents an association (relationship) between schema objects."""
69
67
 
@@ -77,21 +75,32 @@ class SchemaObjectAssociation:
77
75
 
78
76
  @property
79
77
  def child_property(self) -> str:
80
- return self._child_property if self._child_property else get_schema_object(self.schema_name).primary_key.column_name
78
+ return (
79
+ self._child_property
80
+ if self._child_property
81
+ else get_schema_object(self.schema_name).primary_key.column_name
82
+ )
81
83
 
82
84
  @property
83
85
  def parent_property(self) -> str:
84
- return self._parent_property if self._parent_property else get_schema_object(self.parent_schema).primary_key.column_name
86
+ return (
87
+ self._parent_property
88
+ if self._parent_property
89
+ else get_schema_object(self.parent_schema).primary_key.column_name
90
+ )
85
91
 
86
92
  def __repr__(self):
87
- return f"SchemaObjectAssociation(name={self.api_name}, child_property={self._child_property}, parent_property={self.parent_property})"
93
+ return (
94
+ f"SchemaObjectAssociation(name={self.api_name}, "
95
+ + f"child_property={self._child_property}, "
96
+ + f"parent_property={self.parent_property})"
97
+ )
88
98
 
89
99
  @property
90
100
  def child_schema_object(self) -> "SchemaObject":
91
101
  return get_schema_object(self.schema_name)
92
102
 
93
103
 
94
-
95
104
  class SchemaObject:
96
105
  """Represents a schema object in the API configuration."""
97
106
 
@@ -113,6 +122,7 @@ class SchemaObject:
113
122
  else None
114
123
  )
115
124
  self._primary_key = data.get("primary_key")
125
+ self.permissions = data.get("permissions")
116
126
 
117
127
  def __repr__(self):
118
128
  return f"SchemaObject(table_name={self.table_name}, primary_key={self.primary_key})"
@@ -121,6 +131,7 @@ class SchemaObject:
121
131
  def primary_key(self):
122
132
  return self.properties.get(self._primary_key)
123
133
 
134
+
124
135
  class PathOperation:
125
136
  """Represents a path operation in the API configuration."""
126
137
 
@@ -137,6 +148,7 @@ class PathOperation:
137
148
  name: SchemaObjectProperty(output_data)
138
149
  for name, output_data in data.get("outputs", {}).items()
139
150
  }
151
+ self.permissions = data.get("security")
140
152
 
141
153
  def __repr__(self):
142
154
  return f"PathOperation(entity={self.entity}, method={self.method})"
@@ -145,12 +157,14 @@ class PathOperation:
145
157
  schema_objects = None
146
158
  path_operations = None
147
159
 
160
+
148
161
  def get_schema_object(name: str) -> Optional[SchemaObject]:
149
162
  """Returns a schema object by name."""
150
163
  global schema_objects
151
164
  return schema_objects.get(name)
152
165
 
153
- def get_path_operation(path: str, method:str) -> Optional[PathOperation]:
166
+
167
+ def get_path_operation(path: str, method: str) -> Optional[PathOperation]:
154
168
  """Returns a path operation by name."""
155
169
  log.info(f"path: {path}, method: {method}")
156
170
  global path_operations
@@ -174,10 +188,12 @@ class APIModel:
174
188
  }
175
189
 
176
190
  def __repr__(self):
177
- return f"APIModel(schema_objects={list(self.schema_objects.keys())}, path_operations={list(self.path_operations.keys())})"
191
+ return (
192
+ f"APIModel(schema_objects={list(self.schema_objects.keys())}, "
193
+ + f"path_operations={list(self.path_operations.keys())})"
194
+ )
178
195
 
179
196
 
180
197
  def load_api(filename: str):
181
198
  with open(filename, "r") as file:
182
199
  APIModel(yaml.safe_load(file))
183
-
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "api-foundry-query-engine"
7
- version = "0.0.4"
7
+ version = "0.0.6"
8
8
  authors = [
9
9
  { name="Daniel Repik", email="danrepik@icloud.com" },
10
10
  ]
@@ -37,4 +37,3 @@ markers = [
37
37
  "unit: marks tests as unit tests that do not require a database",
38
38
  "integration: marks tests that operate on a real database",
39
39
  ]
40
-
@@ -1,15 +0,0 @@
1
- class Operation:
2
- def __init__(
3
- self,
4
- *,
5
- entity: str,
6
- action: str,
7
- query_params={},
8
- store_params={},
9
- metadata_params={},
10
- ):
11
- self.entity = entity
12
- self.action = action
13
- self.query_params = query_params
14
- self.store_params = store_params
15
- self.metadata_params = metadata_params