api-foundry-query-engine 0.8.57__tar.gz → 0.8.59__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 (37) hide show
  1. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/PKG-INFO +1 -1
  2. api_foundry_query_engine-0.8.59/api_foundry_query_engine/__init__.py +1 -0
  3. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/adapters/gateway_adapter.py +18 -23
  4. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/lambda_handler.py +1 -1
  5. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/utils/claims_check.py +30 -37
  6. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/pyproject.toml +7 -0
  7. api_foundry_query_engine-0.8.57/api_foundry_query_engine/__init__.py +0 -1
  8. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/.gitignore +0 -0
  9. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/LICENSE +0 -0
  10. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
  11. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/adapters/adapter.py +0 -0
  12. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
  13. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
  14. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/connectors/connection.py +0 -0
  15. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
  16. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
  17. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/connectors/postgres_connection.py +0 -0
  18. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/batch_operation_handler.py +0 -0
  19. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/dao.py +0 -0
  20. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/operation_dao.py +0 -0
  21. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_custom_query_handler.py +0 -0
  22. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_delete_query_handler.py +0 -0
  23. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_insert_query_handler.py +0 -0
  24. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_query_handler.py +0 -0
  25. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_restore_query_handler.py +0 -0
  26. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_select_query_handler.py +0 -0
  27. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +0 -0
  28. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/dao/sql_update_query_handler.py +0 -0
  29. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/operation.py +0 -0
  30. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/services/service.py +0 -0
  31. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/services/transactional_service.py +0 -0
  32. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/utils/api_model.py +0 -0
  33. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/utils/app_exception.py +0 -0
  34. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/utils/dependency_resolver.py +0 -0
  35. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/utils/logger.py +0 -0
  36. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/utils/reference_resolver.py +0 -0
  37. {api_foundry_query_engine-0.8.57 → api_foundry_query_engine-0.8.59}/api_foundry_query_engine/utils/token_decoder.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: api-foundry-query-engine
3
- Version: 0.8.57
3
+ Version: 0.8.59
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 @@
1
+ __version__ = "0.8.59"
@@ -4,7 +4,6 @@ from typing import Any, Dict, List, Optional, Tuple
4
4
 
5
5
  from api_foundry_query_engine.adapters.adapter import Adapter
6
6
  from api_foundry_query_engine.operation import Operation
7
- from api_foundry_query_engine.utils.app_exception import ApplicationException
8
7
 
9
8
  log = logging.getLogger(__name__)
10
9
 
@@ -40,12 +39,12 @@ class GatewayAdapter(Adapter):
40
39
  - tuple: Tuple containing data, query and metadata parameters.
41
40
  """
42
41
  resource = event.get("resource")
43
- if resource is not None and "/" in resource:
42
+ if resource:
44
43
  parts = [p for p in resource.split("/") if p] # Remove empty strings
45
44
  # Skip common API prefixes like "api", "v1", "v2", etc.
46
45
  api_prefixes = {"api", "v1", "v2", "v3"}
47
- filtered_parts = [p for p in parts if p not in api_prefixes and not p.startswith("{")]
48
- entity = filtered_parts[0] if filtered_parts else None
46
+ literal_parts = [p for p in parts if p not in api_prefixes and not p.startswith("{")]
47
+ entity = "_".join(literal_parts) if literal_parts else None
49
48
  else:
50
49
  entity = None
51
50
 
@@ -57,14 +56,15 @@ class GatewayAdapter(Adapter):
57
56
 
58
57
  # Handle different authorizer types
59
58
  if isinstance(claims, dict):
60
- # Parse JSON-stringified fields (roles, permissions) from token validator
61
- for key in ['roles', 'permissions']:
59
+ # Parse JSON-stringified fields (roles, permissions)
60
+ # from token validator
61
+ for key in ["roles", "permissions"]:
62
62
  if key in claims and isinstance(claims[key], str):
63
63
  try:
64
64
  claims[key] = json.loads(claims[key])
65
65
  except (json.JSONDecodeError, ValueError):
66
66
  pass # Keep as string if not valid JSON
67
-
67
+
68
68
  # TOKEN authorizer puts claims directly in authorizer object
69
69
  if "sub" in claims or "iss" in claims or "roles" in claims:
70
70
  # Already have JWT claims at top level
@@ -103,19 +103,18 @@ class GatewayAdapter(Adapter):
103
103
  if path_parameters is not None:
104
104
  event_params.update(path_parameters)
105
105
 
106
- queryStringParameters = self._convert_parameters(
107
- event.get("queryStringParameters")
108
- )
109
- if queryStringParameters is not None:
110
- event_params.update(queryStringParameters)
111
-
106
+ query_string_parameters = self._convert_parameters(event.get("queryStringParameters"))
107
+ if query_string_parameters is not None:
108
+ event_params.update(query_string_parameters)
112
109
  query_params, metadata_params = self.split_params(event_params)
110
+ log.info("Unmarshalled query parameters: %s", query_params)
111
+ log.info("Unmarshalled metadata parameters: %s", metadata_params)
113
112
 
114
113
  store_params = {}
115
114
  body = event.get("body")
116
115
  if body is not None and len(body) > 0:
117
116
  store_params = json.loads(body)
118
- \
117
+
119
118
  return Operation(
120
119
  entity=entity,
121
120
  action=action,
@@ -125,9 +124,7 @@ class GatewayAdapter(Adapter):
125
124
  claims=claims,
126
125
  )
127
126
 
128
- def _convert_parameters(
129
- self, parameters: Optional[Dict[str, Any]]
130
- ) -> Optional[Dict[str, Any]]:
127
+ def _convert_parameters(self, parameters: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
131
128
  """
132
129
  Convert parameters to appropriate types.
133
130
 
@@ -151,9 +148,7 @@ class GatewayAdapter(Adapter):
151
148
  result[parameter] = value
152
149
  return result
153
150
 
154
- def split_params(
155
- self, parameters: Dict[str, Any]
156
- ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
151
+ def split_params(self, parameters: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
157
152
  """
158
153
  Split a dictionary into two dictionaries based on keys.
159
154
 
@@ -174,8 +169,8 @@ class GatewayAdapter(Adapter):
174
169
  else:
175
170
  query_params[key] = value
176
171
 
177
- log.info(f"split_params input: {parameters}")
178
- log.info(f"query_params: {query_params}")
179
- log.info(f"metadata_params: {metadata_params}")
172
+ log.info("split_params input: %s", parameters)
173
+ log.info("query_params: %s", query_params)
174
+ log.info("metadata_params: %s", metadata_params)
180
175
 
181
176
  return query_params, metadata_params
@@ -48,7 +48,7 @@ class QueryEngine:
48
48
 
49
49
 
50
50
  @token_decoder()
51
- @claims_check(validate_path_scope=False)
51
+ @claims_check(validate_path_scope=False, validate_scope_format=False)
52
52
  def handler(event, _):
53
53
  if not hasattr(handler, "engine_config"):
54
54
  log.info("Loading engine config from environment variables")
@@ -83,17 +83,13 @@ def claims_check(
83
83
  config_require_authentication = require_authentication or os.getenv(
84
84
  "REQUIRE_AUTHENTICATION", ""
85
85
  ).lower() in ("true", "1", "yes")
86
- log.debug(
87
- "config_require_authentication: %s", config_require_authentication
88
- )
86
+ log.debug("config_require_authentication: %s", config_require_authentication)
89
87
 
90
88
  # Extract claims from the event (set by token_decoder)
91
89
  claims = _extract_claims(event)
92
90
  if not claims:
93
91
  if config_require_authentication:
94
- raise ApplicationException(
95
- status_code=401, message="No authentication claims found"
96
- )
92
+ raise ApplicationException(status_code=401, message="No authentication claims found")
97
93
  else:
98
94
  log.debug("Found claims: %s", list(claims.keys()))
99
95
 
@@ -111,9 +107,7 @@ def claims_check(
111
107
 
112
108
  # Check specific scopes if required
113
109
  if required_scopes:
114
- _validate_required_scopes(
115
- claims, required_scopes, event, operation_type, entity_name
116
- )
110
+ _validate_required_scopes(claims, required_scopes, event, operation_type, entity_name)
117
111
 
118
112
  # Check specific permissions if required
119
113
  if required_permissions:
@@ -157,9 +151,7 @@ def _extract_claims(event: Dict[str, Any]) -> Optional[Dict[str, Any]]:
157
151
 
158
152
  # Check direct authorizer context (most common for TOKEN authorizer)
159
153
  claim_keys = ["sub", "scope", "permissions", "roles"]
160
- if isinstance(authorizer, dict) and any(
161
- key in authorizer for key in claim_keys
162
- ):
154
+ if isinstance(authorizer, dict) and any(key in authorizer for key in claim_keys):
163
155
  log.debug("Found claims in requestContext.authorizer")
164
156
  return authorizer
165
157
 
@@ -202,12 +194,12 @@ def _validate_scope_format(claims: Dict[str, Any]) -> None:
202
194
  elif not isinstance(scopes, list):
203
195
  scopes = []
204
196
 
205
- # Allow empty scopes if permissions exist
197
+ # Scopes and permissions are now optional - they may be present but are not required
198
+ # The x-af-permissions in the API spec handles role-based access control
206
199
  permissions = claims.get("permissions", [])
207
200
  if not scopes and not permissions:
208
- raise ApplicationException(
209
- status_code=403, message="No scopes or permissions found in token"
210
- )
201
+ log.debug("No scopes or permissions found in token - relying on role-based permissions")
202
+ return
211
203
 
212
204
  # Validate scope format: should be operation:resource or operation:*
213
205
  for scope in scopes:
@@ -238,9 +230,7 @@ def _validate_min_scope_level(claims: Dict[str, Any], min_level: str) -> None:
238
230
  user_max_level = max(user_max_level, scope_value)
239
231
 
240
232
  if user_max_level < min_level_value:
241
- raise ApplicationException(
242
- status_code=403, message=f"Insufficient scope level. Required: {min_level}"
243
- )
233
+ raise ApplicationException(status_code=403, message=f"Insufficient scope level. Required: {min_level}")
244
234
 
245
235
 
246
236
  def _validate_path_scope(
@@ -293,8 +283,7 @@ def _validate_path_scope(
293
283
  if not has_scope and not has_permission:
294
284
  raise ApplicationException(
295
285
  status_code=403,
296
- message=f"Access denied. Required scope: {required_scope} "
297
- f"or permission: {required_permission}",
286
+ message=f"Access denied. Required scope: {required_scope} " f"or permission: {required_permission}",
298
287
  )
299
288
 
300
289
  log.debug("Path scope validation passed for %s", required_scope)
@@ -321,14 +310,10 @@ def _validate_required_scopes(
321
310
  # Check each required scope
322
311
  for required_scope in required_scopes:
323
312
  if not _scope_matches(user_scopes, required_scope, operation, entity):
324
- raise ApplicationException(
325
- status_code=403, message=f"Required scope not found: {required_scope}"
326
- )
313
+ raise ApplicationException(status_code=403, message=f"Required scope not found: {required_scope}")
327
314
 
328
315
 
329
- def _validate_required_permissions(
330
- claims: Dict[str, Any], required_permissions: list
331
- ) -> None:
316
+ def _validate_required_permissions(claims: Dict[str, Any], required_permissions: list) -> None:
332
317
  """Validate that user has required permissions."""
333
318
  user_permissions = claims.get("permissions", [])
334
319
  if isinstance(user_permissions, str):
@@ -418,22 +403,30 @@ def _extract_entity_from_path(event: Dict[str, Any]) -> Optional[str]:
418
403
  # Remove leading slash and split by slash
419
404
  path_parts = path.lstrip("/").split("/")
420
405
 
421
- # Find the first non-parameter part (entity is typically first)
406
+ # Find literal path parts (ignoring params and common prefixes)
422
407
  # For paths like "/comment/123" or "/comment/123/version/1"
423
- # Skip common API prefixes like "api", "v1", etc.
424
408
  api_prefixes = {"api", "v1", "v2", "v3"}
409
+ literal_parts = []
425
410
  for part in path_parts:
426
- if part and not part.startswith("{") and part not in api_prefixes:
427
- # Skip numeric IDs and known path keywords
428
- if not part.isdigit() and part not in {"version", "batch"}:
429
- return part
411
+ if not part or part.startswith("{"):
412
+ continue
413
+ if part in api_prefixes:
414
+ continue
415
+ # Skip numeric IDs and known path keywords
416
+ if part.isdigit() or part in {"version", "batch"}:
417
+ continue
418
+ literal_parts.append(part)
419
+
420
+ if not literal_parts:
421
+ return None
422
+
423
+ if len(literal_parts) == 1:
424
+ return literal_parts[0]
430
425
 
431
- return None
426
+ return "_".join(literal_parts)
432
427
 
433
428
 
434
- def _scope_matches(
435
- user_scopes: list, required_scope: str, operation: str, entity: str
436
- ) -> bool:
429
+ def _scope_matches(user_scopes: list, required_scope: str, operation: str, entity: str) -> bool:
437
430
  """Check if user scopes match the required scope."""
438
431
  _ = entity # Silence unused warning
439
432
  # Check for exact match
@@ -90,3 +90,10 @@ serve = "mkdocs serve --dev-addr localhost:8000"
90
90
 
91
91
  [tool.hatch.publish.index]
92
92
  disable = true # Disable publishing by default for safety
93
+
94
+ # Linting / formatting configuration
95
+ [tool.ruff]
96
+ line-length = 120
97
+
98
+ [tool.black]
99
+ line-length = 120
@@ -1 +0,0 @@
1
- __version__ = "0.8.57"