api-foundry-query-engine 0.8.12__tar.gz → 0.8.30__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 (30) hide show
  1. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/PKG-INFO +1 -1
  2. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/adapters/gateway_adapter.py +29 -3
  3. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/dao/sql_query_handler.py +29 -6
  4. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/dao/sql_select_query_handler.py +17 -3
  5. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/operation.py +4 -4
  6. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/utils/api_model.py +107 -28
  7. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/pyproject.toml +1 -1
  8. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/.gitignore +0 -0
  9. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/LICENSE +0 -0
  10. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/.pre-commit-config.yaml +0 -0
  11. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/__init__.py +0 -0
  12. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/adapters/adapter.py +0 -0
  13. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/adapters/case_change_adapter.py +0 -0
  14. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/adapters/security_adapter.py +0 -0
  15. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/connectors/connection.py +0 -0
  16. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/connectors/connection_factory.py +0 -0
  17. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/connectors/oracle_connector.py +0 -0
  18. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/connectors/postgres_connection.py +0 -0
  19. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/dao/dao.py +0 -0
  20. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/dao/operation_dao.py +0 -0
  21. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/dao/sql_custom_query_handler.py +0 -0
  22. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/dao/sql_delete_query_handler.py +0 -0
  23. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/dao/sql_insert_query_handler.py +0 -0
  24. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/dao/sql_subselect_query_handler.py +0 -0
  25. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/dao/sql_update_query_handler.py +0 -0
  26. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/lambda_handler.py +0 -0
  27. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/services/service.py +0 -0
  28. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/services/transactional_service.py +0 -0
  29. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/api_foundry_query_engine/utils/app_exception.py +0 -0
  30. {api_foundry_query_engine-0.8.12 → api_foundry_query_engine-0.8.30}/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.12
3
+ Version: 0.8.30
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
@@ -67,10 +67,36 @@ class GatewayAdapter(Adapter):
67
67
 
68
68
  authorizer_info = event.get("requestContext", {}).get("authorizer", {})
69
69
  claims = authorizer_info.get("claims", {})
70
- roles = claims.get("roles", [])
71
- groups = claims.get("groups", [])
70
+
71
+ # Decode JSON-encoded arrays from OAuth context
72
+ roles_raw = claims.get("roles", [])
73
+ if isinstance(roles_raw, str):
74
+ try:
75
+ roles = json.loads(roles_raw)
76
+ except (json.JSONDecodeError, TypeError):
77
+ roles = []
78
+ else:
79
+ roles = roles_raw if isinstance(roles_raw, list) else []
80
+
81
+ groups_raw = claims.get("groups", [])
82
+ if isinstance(groups_raw, str):
83
+ try:
84
+ groups = json.loads(groups_raw)
85
+ except (json.JSONDecodeError, TypeError):
86
+ groups = []
87
+ else:
88
+ groups = groups_raw if isinstance(groups_raw, list) else []
89
+
90
+ permissions_raw = claims.get("permissions", [])
91
+ if isinstance(permissions_raw, str):
92
+ try:
93
+ permissions = json.loads(permissions_raw)
94
+ except (json.JSONDecodeError, TypeError):
95
+ permissions = []
96
+ else:
97
+ permissions = permissions_raw if isinstance(permissions_raw, list) else []
98
+
72
99
  subject = claims.get("sub")
73
- permissions = claims.get("permissions", [])
74
100
  scope_str = claims.get("scope")
75
101
 
76
102
  # Enforce OAuth scopes (simulating API Gateway authorizer behavior)
@@ -193,20 +193,43 @@ class SQLQueryHandler:
193
193
  log.info(f"role: {role}, role_permissions: {role_permissions}")
194
194
  if len(role_permissions) == 0:
195
195
  continue
196
+
197
+ # Extract permission patterns for this role
198
+ read_perm = role_permissions.get("read", "")
199
+ write_perm = role_permissions.get("write", "")
200
+ read_pattern = self._extract_permission_pattern(read_perm)
201
+ write_pattern = self._extract_permission_pattern(write_perm)
202
+
196
203
  for prop_name, property in properties.items():
197
204
  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
- ):
205
+ if permission_type == "read" and re.match(read_pattern, prop_name):
201
206
  allowed_properties[prop_name] = property
202
- if permission_type == "write" and re.match(
203
- role_permissions.get("write", ""), prop_name
204
- ):
207
+ if permission_type == "write" and re.match(write_pattern, prop_name):
205
208
  allowed_properties[prop_name] = property
206
209
 
207
210
  log.info(f"allowed_properties: {allowed_properties}")
208
211
  return allowed_properties
209
212
 
213
+ def _extract_permission_pattern(self, permission_rule) -> str:
214
+ """Extract the regex pattern from a permission rule.
215
+
216
+ Args:
217
+ permission_rule: Can be a string (regex) or dict with
218
+ 'properties'/'fields' key
219
+
220
+ Returns:
221
+ str: The regex pattern to match property names
222
+ """
223
+ if isinstance(permission_rule, str):
224
+ return permission_rule
225
+ elif isinstance(permission_rule, dict):
226
+ # Support both 'properties' (preferred) and 'fields' (legacy)
227
+ return permission_rule.get("properties") or permission_rule.get(
228
+ "fields", ""
229
+ )
230
+ else:
231
+ return ""
232
+
210
233
  @property
211
234
  def selection_results(self) -> Dict:
212
235
  raise NotImplementedError()
@@ -33,20 +33,34 @@ class SQLSelectSchemaQueryHandler(SQLSchemaQueryHandler):
33
33
  if "default" in perms:
34
34
  provider = perms.get("default", {}) or {}
35
35
  read_map = provider.get("read", {}) or {}
36
+ role_permissions = perms.get("default", {})
36
37
  else:
37
38
  # legacy role-first -> synthesize read map
38
39
  read_map = {}
40
+ role_permissions = perms
39
41
  for role, role_perms in perms.items():
40
42
  if isinstance(role_perms, dict):
41
43
  read_map[role] = role_perms.get("read")
42
44
 
43
45
  filters = []
44
46
  for role in self.operation.roles or []:
47
+ # Check for role-level WHERE clause (hybrid approach)
48
+ role_where = None
49
+ if isinstance(role_permissions.get(role), dict):
50
+ role_where = role_permissions[role].get("where")
51
+
52
+ # Check for operation-level WHERE clause
53
+ operation_where = None
45
54
  rule = read_map.get(role)
46
55
  if isinstance(rule, dict):
47
- where = rule.get("where")
48
- if isinstance(where, str) and where.strip():
49
- filters.append(self._template_where(where))
56
+ operation_where = rule.get("where")
57
+
58
+ # Operation-level takes precedence, fallback to role-level
59
+ where_clause = operation_where if operation_where else role_where
60
+
61
+ if isinstance(where_clause, str) and where_clause.strip():
62
+ filters.append(self._template_where(where_clause))
63
+
50
64
  if not filters:
51
65
  return ""
52
66
  return "(" + ") OR (".join(filters) + ")"
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict, Optional
1
+ from typing import Any, Dict, List, Optional
2
2
 
3
3
 
4
4
  class Operation:
@@ -35,10 +35,10 @@ class Operation:
35
35
  query_params: Optional[Dict[str, Any]] = None,
36
36
  store_params: Optional[Dict[str, Any]] = None,
37
37
  metadata_params: Optional[Dict[str, Any]] = None,
38
- roles: Optional[Dict[str, Any]] = None,
38
+ roles: Optional[List[str]] = None,
39
39
  scope: Optional[Dict[str, Any]] = None,
40
- groups: Optional[Dict[str, Any]] = None,
41
- permissions: Optional[Dict[str, Any]] = None,
40
+ groups: Optional[List[str]] = None,
41
+ permissions: Optional[List[str]] = None,
42
42
  subject: Optional[str] = None,
43
43
  claims: Optional[Dict[str, Any]] = None,
44
44
  ):
@@ -49,38 +49,117 @@ class SchemaObjectProperty:
49
49
  def convert_to_db_value(self, value: str) -> Optional[Any]:
50
50
  if value is None:
51
51
  return None
52
- conversion_mapping = {
53
- "string": lambda x: x,
54
- "number": float,
55
- "float": float,
56
- "integer": int,
57
- "boolean": lambda x: x.lower() == "true",
58
- "date": lambda x: datetime.strptime(x, "%Y-%m-%d").date() if x else None,
59
- "date-time": lambda x: datetime.fromisoformat(x) if x else None,
60
- "time": lambda x: datetime.strptime(x, "%H:%M:%S").time() if x else None,
61
- }
62
- conversion_func = conversion_mapping.get(
63
- self.column_type if self.column_type is not None else "string", lambda x: x
64
- )
65
- return conversion_func(value)
52
+
53
+ column_type = self.column_type if self.column_type is not None else "string"
54
+
55
+ # Handle string types
56
+ if column_type in ["string", "varchar", "char", "text", "uuid"]:
57
+ return value
58
+
59
+ # Handle numeric types - float/double/numeric variations
60
+ elif column_type in ["number", "float", "double", "numeric", "decimal", "real"]:
61
+ return float(value)
62
+
63
+ # Handle boolean types - can map to boolean or integer columns
64
+ elif column_type == "boolean":
65
+ return value.lower() == "true"
66
+ elif (
67
+ column_type in ["int", "integer", "smallint", "bigint"]
68
+ and hasattr(self, "api_type")
69
+ and self.api_type == "boolean"
70
+ ):
71
+ # Boolean API type mapping to integer column type
72
+ return 1 if value.lower() == "true" else 0
73
+
74
+ # Handle integer types (after boolean check to avoid conflicts)
75
+ elif column_type in [
76
+ "integer",
77
+ "int",
78
+ "bigint",
79
+ "smallint",
80
+ "serial",
81
+ "bigserial",
82
+ ]:
83
+ return int(value)
84
+
85
+ # Handle date types
86
+ elif column_type == "date":
87
+ return datetime.strptime(value, "%Y-%m-%d").date() if value else None
88
+
89
+ # Handle datetime types - various column type names
90
+ elif column_type in ["date-time", "datetime", "timestamp", "timestamptz"]:
91
+ return datetime.fromisoformat(value) if value else None
92
+
93
+ # Handle time types
94
+ elif column_type in ["time", "timetz"]:
95
+ return datetime.strptime(value, "%H:%M:%S").time() if value else None
96
+
97
+ # Default to string conversion for unknown types
98
+ else:
99
+ return value
66
100
 
67
101
  def convert_to_api_value(self, value) -> Optional[Any]:
68
102
  if value is None:
69
103
  return None
70
- conversion_mapping = {
71
- "string": lambda x: x,
72
- "number": float,
73
- "float": float,
74
- "integer": int,
75
- "boolean": str,
76
- "date": lambda x: x.date().isoformat() if x else None,
77
- "date-time": lambda x: x.isoformat() if x else None,
78
- "time": lambda x: x.time().isoformat() if x else None,
79
- }
80
- conversion_func = conversion_mapping.get(
81
- self.api_type if self.api_type is not None else "string", lambda x: x
82
- )
83
- return conversion_func(value)
104
+
105
+ api_type = self.api_type if self.api_type is not None else "string"
106
+
107
+ # Handle string types (including UUID which is represented as string in API)
108
+ if api_type in ["string", "uuid"]:
109
+ return str(value) if value is not None else None
110
+
111
+ # Handle integer type
112
+ elif api_type == "integer":
113
+ return int(value) if value is not None else None
114
+
115
+ # Handle number and float types
116
+ elif api_type in ["number", "float"]:
117
+ return float(value) if value is not None else None
118
+
119
+ # Handle boolean type - convert any value to string representation
120
+ elif api_type == "boolean":
121
+ if isinstance(value, bool):
122
+ return str(value)
123
+ elif isinstance(value, int):
124
+ # Handle boolean stored as integer (0/1)
125
+ return "true" if value != 0 else "false"
126
+ elif isinstance(value, str):
127
+ return value.lower() in ["true", "1", "yes", "on"]
128
+ else:
129
+ return str(bool(value))
130
+
131
+ # Handle date type
132
+ elif api_type == "date":
133
+ if hasattr(value, "date"):
134
+ # If it's a datetime, extract the date part
135
+ return value.date().isoformat()
136
+ elif hasattr(value, "isoformat"):
137
+ # If it's already a date
138
+ return value.isoformat()
139
+ else:
140
+ return str(value)
141
+
142
+ # Handle datetime type
143
+ elif api_type == "date-time":
144
+ if hasattr(value, "isoformat"):
145
+ return value.isoformat()
146
+ else:
147
+ return str(value)
148
+
149
+ # Handle time type
150
+ elif api_type == "time":
151
+ if hasattr(value, "time"):
152
+ # If it's a datetime, extract the time part
153
+ return value.time().isoformat()
154
+ elif hasattr(value, "isoformat"):
155
+ # If it's already a time
156
+ return value.isoformat()
157
+ else:
158
+ return str(value)
159
+
160
+ # Default to string conversion for unknown types
161
+ else:
162
+ return str(value) if value is not None else None
84
163
 
85
164
 
86
165
  class SchemaObjectAssociation:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "api-foundry-query-engine"
7
- version = "0.8.12"
7
+ version = "0.8.30"
8
8
  authors = [
9
9
  { name="Daniel Repik", email="danrepik@icloud.com" },
10
10
  ]