api-foundry-query-engine 0.8.10__tar.gz → 0.8.11__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.10 → api_foundry_query_engine-0.8.11}/PKG-INFO +7 -1
  2. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/adapters/gateway_adapter.py +1 -0
  3. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/dao/sql_select_query_handler.py +97 -46
  4. api_foundry_query_engine-0.8.11/api_foundry_query_engine/operation.py +81 -0
  5. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/utils/api_model.py +3 -1
  6. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/pyproject.toml +18 -1
  7. api_foundry_query_engine-0.8.10/api_foundry_query_engine/operation.py +0 -59
  8. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/.gitignore +0 -0
  9. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/LICENSE +0 -0
  10. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
  11. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/__init__.py +0 -0
  12. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/adapters/adapter.py +0 -0
  13. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
  14. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
  15. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/connectors/athena_connector.py +0 -0
  16. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/connectors/connection.py +0 -0
  17. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
  18. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
  19. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/connectors/postgres_connection.py +0 -0
  20. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/dao/dao.py +0 -0
  21. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/dao/operation_dao.py +0 -0
  22. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/dao/sql_custom_query_handler.py +0 -0
  23. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/dao/sql_delete_query_handler.py +0 -0
  24. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/dao/sql_insert_query_handler.py +0 -0
  25. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/dao/sql_query_handler.py +0 -0
  26. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +0 -0
  27. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/dao/sql_update_query_handler.py +0 -0
  28. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/lambda_handler.py +0 -0
  29. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/services/security_service.py +0 -0
  30. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/services/service.py +0 -0
  31. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/services/transactional_service.py +0 -0
  32. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/utils/app_exception.py +0 -0
  33. {api_foundry_query_engine-0.8.10 → api_foundry_query_engine-0.8.11}/api_foundry_query_engine/utils/logger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: api-foundry-query-engine
3
- Version: 0.8.10
3
+ Version: 0.8.11
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
@@ -12,3 +12,9 @@ Requires-Python: >=3.9
12
12
  Requires-Dist: boto3
13
13
  Requires-Dist: pyhumps
14
14
  Requires-Dist: pyyaml
15
+ Provides-Extra: dev
16
+ Requires-Dist: black>=23.0.0; extra == 'dev'
17
+ Requires-Dist: fixture-foundry>=0.1.0; extra == 'dev'
18
+ Requires-Dist: isort>=5.12.0; extra == 'dev'
19
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
20
+ Requires-Dist: pytest>=7.0; extra == 'dev'
@@ -70,6 +70,7 @@ class GatewayAdapter(Adapter):
70
70
  metadata_params=metadata_params,
71
71
  roles=roles,
72
72
  subject=subject,
73
+ claims=claims,
73
74
  )
74
75
 
75
76
  def _convert_parameters(self, parameters):
@@ -1,17 +1,60 @@
1
- from api_foundry_query_engine.dao.sql_query_handler import SQLSchemaQueryHandler
2
- from api_foundry_query_engine.operation import Operation
1
+ import re
2
+ from typing import Match
3
+ from api_foundry_query_engine.dao.sql_query_handler import (
4
+ SQLSchemaQueryHandler,
5
+ )
3
6
  from api_foundry_query_engine.utils.app_exception import ApplicationException
4
- from api_foundry_query_engine.utils.api_model import SchemaObject, SchemaObjectProperty
5
- from api_foundry_query_engine.utils.logger import logger
6
-
7
- log = logger(__name__)
7
+ from api_foundry_query_engine.utils.api_model import SchemaObjectProperty
8
8
 
9
9
 
10
10
  class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
11
- def __init__(
12
- self, operation: Operation, schema_object: SchemaObject, engine: str
13
- ) -> None:
11
+ def _template_where(self, expr: str) -> str:
12
+ if not expr:
13
+ return expr
14
+
15
+ def _quote(val: object) -> str:
16
+ if val is None:
17
+ return "NULL"
18
+ if isinstance(val, (int, float)):
19
+ return str(val)
20
+ s = str(val).replace("'", "''")
21
+ return f"'{s}'"
22
+
23
+ def _replace(m: Match[str]) -> str:
24
+ key = m.group(1)
25
+ claims = self.operation.claims or {}
26
+ return _quote(claims.get(key))
27
+
28
+ return re.sub(r"\$\{claims\.([A-Za-z0-9_]+)\}", _replace, expr)
29
+
30
+ def _row_where_clause(self) -> str:
31
+ perms = getattr(self.schema_object, "permissions", None) or {}
32
+ # provider-first default
33
+ if "default" in perms:
34
+ provider = perms.get("default", {}) or {}
35
+ read_map = provider.get("read", {}) or {}
36
+ else:
37
+ # legacy role-first -> synthesize read map
38
+ read_map = {}
39
+ for role, role_perms in perms.items():
40
+ if isinstance(role_perms, dict):
41
+ read_map[role] = role_perms.get("read")
42
+
43
+ filters = []
44
+ for role in self.operation.roles or []:
45
+ rule = read_map.get(role)
46
+ if isinstance(rule, dict):
47
+ where = rule.get("where")
48
+ if isinstance(where, str) and where.strip():
49
+ filters.append(self._template_where(where))
50
+ if not filters:
51
+ return ""
52
+ return "(" + ") OR (".join(filters) + ")"
53
+
54
+ def __init__(self, operation, schema_object, engine: str) -> None:
14
55
  super().__init__(operation, schema_object, engine)
56
+ # Lazy cache for selection results
57
+ self._selection_results = None
15
58
 
16
59
  @property
17
60
  def sql(self) -> str:
@@ -63,38 +106,39 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
63
106
  + " does not have property "
64
107
  + parts[1],
65
108
  )
66
- property = relation.child_schema_object.properties[parts[1]]
109
+ prop = relation.child_schema_object.properties[parts[1]]
67
110
  prefix = self.prefix_map[parts[0]]
68
111
  else:
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
- )
75
- prefix = self.prefix_map[self.schema_object.api_name]
76
- except KeyError:
112
+ prop = self.schema_object.properties[parts[0]]
113
+ prefix = self.prefix_map[str(self.schema_object.api_name)]
114
+ except KeyError as exc:
77
115
  raise ApplicationException(
78
116
  500,
79
- "Invalid query parameter, property not found. "
80
- + "schema object: "
81
- + str(self.schema_object.api_name)
82
- + ", property: "
83
- + name,
84
- )
85
-
86
- assignment, holders = self.search_value_assignment(property, value, prefix)
117
+ (
118
+ "Invalid query parameter, property not found. "
119
+ + "schema object: "
120
+ + str(self.schema_object.api_name)
121
+ + ", property: "
122
+ + name
123
+ ),
124
+ ) from exc
125
+
126
+ assignment, holders = self.search_value_assignment(prop, value, prefix)
87
127
  self.active_prefixes.add(prefix)
88
128
  conditions.append(assignment)
89
129
  self.search_placeholders.update(holders)
130
+ # append row-level filters if present
131
+ row_filter = self._row_where_clause()
132
+ if row_filter:
133
+ conditions.append(f"({row_filter})")
90
134
 
91
- return f" WHERE {' AND '.join(conditions)}" if len(conditions) > 0 else ""
135
+ return (" WHERE " + " AND ".join(conditions)) if conditions else ""
92
136
 
93
137
  @property
94
138
  def table_expression(self) -> str:
95
139
  joins = []
96
140
  parent_prefix = self.prefix_map[str(self.schema_object.api_name)]
97
- for name, relation in self.schema_object.relations.items():
141
+ for _, relation in self.schema_object.relations.items():
98
142
  child_prefix = self.prefix_map[str(relation.api_name)]
99
143
  if child_prefix in self.active_prefixes:
100
144
  joins.append(
@@ -121,7 +165,7 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
121
165
 
122
166
  @property
123
167
  def selection_results(self) -> dict:
124
- if not hasattr(self, "_selection_results"):
168
+ if self._selection_results is None:
125
169
  self._selection_results = {}
126
170
  if "count" in self.operation.metadata_params:
127
171
  self._selection_results = {
@@ -163,10 +207,14 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
163
207
  # Filter and prefix keys for the current entity
164
208
  # and regular expressions
165
209
  allowed_properties = self.check_permissions(
166
- "read", schema_object.permissions, schema_object.properties
210
+ "read",
211
+ schema_object.permissions,
212
+ schema_object.properties,
167
213
  )
168
214
  filtered_keys = self.filter_and_prefix_keys(
169
- reg_exs, allowed_properties, self.prefix_map[relation]
215
+ reg_exs,
216
+ allowed_properties,
217
+ self.prefix_map[relation],
170
218
  )
171
219
 
172
220
  # Extend the result map with the filtered keys
@@ -175,15 +223,18 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
175
223
  if len(self._selection_results) == 0:
176
224
  raise ApplicationException(
177
225
  402,
178
- "After applying permissions there are no properties returned in response",
226
+ (
227
+ "After applying permissions there are no properties "
228
+ "returned in response"
229
+ ),
179
230
  )
180
231
  return self._selection_results
181
232
 
182
- def get_regex_map(self, filter_str: str) -> dict[str, list]:
233
+ def get_regex_map(self, filter_str: str) -> dict:
183
234
  result = {}
184
235
 
185
- for filter in filter_str.split():
186
- parts = filter.split(":")
236
+ for flt in filter_str.split():
237
+ parts = flt.split(":")
187
238
  entity = parts[0] if len(parts) > 1 else self.schema_object.api_name
188
239
  expression = parts[-1]
189
240
 
@@ -200,17 +251,17 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
200
251
  def marshal_record(self, record) -> dict:
201
252
  object_set = {}
202
253
  for name, value in record.items():
203
- property = self.selection_results[name]
254
+ prop = self.selection_results[name]
204
255
  parts = name.split(".")
205
256
  component = (
206
257
  parts[0]
207
258
  if len(parts) > 1
208
259
  else self.prefix_map[str(self.schema_object.api_name)]
209
260
  )
210
- object = object_set.get(component, {})
211
- if not object:
212
- object_set[component] = object
213
- object[property.api_name] = property.convert_to_api_value(value)
261
+ obj = object_set.get(component, {})
262
+ if not obj:
263
+ object_set[component] = obj
264
+ obj[prop.api_name] = prop.convert_to_api_value(value)
214
265
 
215
266
  result = object_set[self.prefix_map[str(self.schema_object.api_name)]]
216
267
  for name, prefix in self.prefix_map.items():
@@ -243,13 +294,13 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
243
294
  field_parts = field_name.split(".")
244
295
  if len(field_parts) == 1:
245
296
  prefix = self.prefix_map[str(self.schema_object.api_name)]
246
- property = self.schema_object.properties.get(field_parts[0])
247
- if not property:
297
+ prop = self.schema_object.properties.get(field_parts[0])
298
+ if not prop:
248
299
  raise ApplicationException(
249
300
  400,
250
301
  f"Invalid order by property, schema object: {self.schema_object.api_name} does not have a property: {field_parts[0]}", # noqa E501
251
302
  )
252
- column = property.column_name
303
+ column = prop.column_name
253
304
  else:
254
305
  # Extract the schema object for the current entity
255
306
  relation_property = self.schema_object.relations.get(field_parts[0])
@@ -272,13 +323,13 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
272
323
  schema_object = self.schema_object
273
324
 
274
325
  prefix = self.prefix_map[field_parts[0]]
275
- property = schema_object.properties.get(field_parts[1])
276
- if not property:
326
+ prop = schema_object.properties.get(field_parts[1])
327
+ if not prop:
277
328
  raise ApplicationException(
278
329
  400,
279
330
  f"Invalid order by property, schema object: {schema_object.api_name} does not have a property: {field_parts[1]}", # noqa E501
280
331
  )
281
- column = property.column_name
332
+ column = prop.column_name
282
333
  self.active_prefixes.add(prefix)
283
334
  use_prefixes = True
284
335
 
@@ -0,0 +1,81 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+
4
+ class Operation:
5
+ """
6
+ Represents an action to be performed on an entity.
7
+
8
+ The `Operation` class encapsulates the details required
9
+ to execute an operation on a given entity, including
10
+ query parameters for selecting records, parameters for
11
+ storing or updating values, metadata for operational
12
+ instructions, and roles defining the contexts in which
13
+ the operation can be performed.
14
+
15
+ Attributes:
16
+ entity (str): The name of the resource or object being
17
+ targeted (e.g., "User", "Order").
18
+ action (str): The type of action being performed
19
+ (e.g., "create", "read", "update", "delete").
20
+ query_params (dict): Parameters used to filter or select
21
+ the records affected by the operation.
22
+ store_params (dict): Parameters that define the values to
23
+ be stored or updated for the selected records.
24
+ metadata_params (dict): Additional instructions for the
25
+ operation, such as sorting or pagination.
26
+ roles (dict): Defines the roles under which the operation
27
+ is allowed to be performed.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ *,
33
+ entity: str,
34
+ action: str,
35
+ query_params: Optional[Dict[str, Any]] = None,
36
+ store_params: Optional[Dict[str, Any]] = None,
37
+ metadata_params: Optional[Dict[str, Any]] = None,
38
+ roles: Optional[Dict[str, Any]] = None,
39
+ subject: Optional[str] = None,
40
+ claims: Optional[Dict[str, Any]] = None,
41
+ ):
42
+ """
43
+ Initializes the Operation instance.
44
+
45
+ Args:
46
+ entity (str): The name of the entity to perform the operation on.
47
+ action (str): The action to perform on the entity.
48
+ query_params (dict, optional): Parameters for selecting
49
+ affected records (default: {}).
50
+ store_params (dict, optional): Parameters defining new or
51
+ updated values (default: {}).
52
+ metadata_params (dict, optional): Metadata for the
53
+ operation, such as sorting or pagination (default: {}).
54
+ roles (dict, optional): Defines the roles allowed to
55
+ perform the operation (default: {}).
56
+ """
57
+ # The target entity for the operation (e.g., "User", "Order").
58
+ self.entity = entity
59
+
60
+ # The type of action to perform (e.g., "create",
61
+ # "read", "update", "delete").
62
+ self.action = action
63
+
64
+ # Query parameters to filter or identify the affected records.
65
+ self.query_params = query_params or {}
66
+
67
+ # Parameters defining the values to be stored or updated
68
+ # for the operation.
69
+ self.store_params = store_params or {}
70
+
71
+ # Metadata for operational instructions like sorting,
72
+ # limiting, or offsetting results.
73
+ self.metadata_params = metadata_params or {}
74
+
75
+ # Roles defining the context in which the operation is allowed.
76
+ self.roles = roles or {}
77
+
78
+ self.subject = subject
79
+ # Full set of claims (when available). Used for row-level filters
80
+ # like where: "id = ${claims.sub}".
81
+ self.claims = claims or ({"sub": subject} if subject is not None else {})
@@ -155,7 +155,9 @@ class SchemaObject:
155
155
  self.database: str = str(data.get("database"))
156
156
  self.schema: Optional[str] = data.get("schema")
157
157
  self.table_name: str = str(data.get("table_name"))
158
- self.qualified_name: str = f"{self.schema}.{self.table_name}" if self.schema else self.table_name
158
+ self.qualified_name: str = (
159
+ f"{self.schema}.{self.table_name}" if self.schema else self.table_name
160
+ )
159
161
  self.properties: Dict[str, SchemaObjectProperty] = {
160
162
  name: SchemaObjectProperty(prop_data)
161
163
  for name, prop_data in data.get("properties", {}).items()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "api-foundry-query-engine"
7
- version = "0.8.10"
7
+ version = "0.8.11"
8
8
  authors = [
9
9
  { name="Daniel Repik", email="danrepik@icloud.com" },
10
10
  ]
@@ -19,6 +19,14 @@ dependencies = [
19
19
  "pyyaml",
20
20
  "pyhumps",
21
21
  ]
22
+ [project.optional-dependencies]
23
+ dev = [
24
+ "pytest>=7.0",
25
+ "pytest-cov>=4.0.0",
26
+ "black>=23.0.0",
27
+ "isort>=5.12.0",
28
+ "fixture_foundry>=0.1.0"
29
+ ]
22
30
 
23
31
  [project.urls]
24
32
  "Documentation" = "https://github.com/DanRepik/api-foundry"
@@ -30,6 +38,15 @@ include = ["api_foundry_query_engine"]
30
38
  [tool.hatch.build.targets.wheel]
31
39
  include = ["api_foundry_query_engine"]
32
40
 
41
+ [tool.hatch.envs.dev]
42
+ dependencies = [
43
+ "pytest>=7.0",
44
+ "pytest-cov>=4.0.0",
45
+ "black>=23.0.0",
46
+ "isort>=5.12.0",
47
+ "fixture_foundry>=0.1.0",
48
+ ]
49
+
33
50
  [project.scripts]
34
51
 
35
52
  [tool.pytest.ini_options]
@@ -1,59 +0,0 @@
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