From ee27f1e459b814a941c7fdfe8fa740e16cf493ea Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:46:39 +0200 Subject: [PATCH 1/2] fix: support aggregate field dictionaries in get_list (#65) (cherry picked from commit ab0ca7aca0a6f4ed62434caa73d4dbdd488a3844) # Conflicts: # ask_alyf/ask_alyf/tools.py --- ask_alyf/ask_alyf/test_code_tools.py | 15 ++++++++++++++ ask_alyf/ask_alyf/tools.py | 31 ++++++++++++++++++---------- ask_alyf/ask_alyf/toolset.py | 15 ++++++++++---- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/ask_alyf/ask_alyf/test_code_tools.py b/ask_alyf/ask_alyf/test_code_tools.py index 548b99a..57f8164 100644 --- a/ask_alyf/ask_alyf/test_code_tools.py +++ b/ask_alyf/ask_alyf/test_code_tools.py @@ -259,6 +259,21 @@ def test_get_list_coerces_comma_separated_fields_to_list(self): ) self.assertEqual(result, []) + def test_get_list_preserves_aggregate_field_dict(self): + fields = [{"SUM": "grand_total", "as": "total"}] + with patch("ask_alyf.ask_alyf.tools.client.get_list", return_value=[]) as get_list: + result = tools.get_list("Sales Invoice", fields=fields, filters={"docstatus": 1}) + + get_list.assert_called_once_with( + doctype="Sales Invoice", + fields=fields, + filters={"docstatus": 1}, + order_by=None, + limit_page_length=20, + group_by=None, + ) + self.assertEqual(result, []) + def test_get_file_id_uses_reference_filters(self): expected_filters = { "attached_to_doctype": "Sales Invoice", diff --git a/ask_alyf/ask_alyf/tools.py b/ask_alyf/ask_alyf/tools.py index ebb3d34..367fb3f 100644 --- a/ask_alyf/ask_alyf/tools.py +++ b/ask_alyf/ask_alyf/tools.py @@ -25,6 +25,7 @@ FORBIDDEN_SQL_RE = re.compile( r"\b(insert|update|delete|drop|truncate|alter|create|grant|revoke|replace)\b", re.IGNORECASE ) +FrappeSelectField = str | dict[str, str] ENGLISH_LANGUAGE_CODES = {"en", "en-us", "en-gb"} OPERATION_KIND_BACKEND = "backend_action" OPERATION_KIND_FRONTEND = "frontend_action" @@ -89,26 +90,34 @@ def coerce_int(value: Any, default: int, *, minimum: int | None = None) -> int: return coerced -def coerce_field_list(value: str | list[str] | None) -> list[str] | None: +def coerce_field_list(value: str | list[FrappeSelectField] | None) -> list[FrappeSelectField] | None: if value is None: return None - if isinstance(value, list): - return [str(entry).strip() for entry in value if str(entry).strip()] + if isinstance(value, str): + stripped = value.strip() + if not stripped: + return None - stripped = str(value).strip() - if not stripped: - return None + try: + parsed = json.loads(stripped) + except json.JSONDecodeError: + parsed = None +<<<<<<< HEAD try: parsed = json.loads(stripped) except (TypeError, json.JSONDecodeError): parsed = None +======= + value = parsed if isinstance(parsed, list) else stripped.split(",") +>>>>>>> ab0ca7a (fix: support aggregate field dictionaries in get_list (#65)) - if isinstance(parsed, list): - return [str(entry).strip() for entry in parsed if str(entry).strip()] - - return [part.strip() for part in stripped.split(",") if part.strip()] + return [ + entry if isinstance(entry, dict) else str(entry).strip() + for entry in value + if (isinstance(entry, dict) and entry) or (not isinstance(entry, dict) and str(entry).strip()) + ] def get_settings(): @@ -331,7 +340,7 @@ def ensure_editable_doctype(doctype: str): def get_list( doctype: str, - fields: str | list[str] | None = None, + fields: str | list[FrappeSelectField] | None = None, filters: dict[str, Any] | list | None = None, order_by: str | None = None, limit: int = 20, diff --git a/ask_alyf/ask_alyf/toolset.py b/ask_alyf/ask_alyf/toolset.py index 9c90743..54cc6ba 100644 --- a/ask_alyf/ask_alyf/toolset.py +++ b/ask_alyf/ask_alyf/toolset.py @@ -161,7 +161,7 @@ def _frontend_proposal( def get_list( self, doctype: str, - fields: str | list[str] | None = None, + fields: str | list[tools.FrappeSelectField] | None = None, filters: dict[str, Any] | FrappeFilterList | None = None, order_by: str | None = None, limit: int = 20, @@ -171,7 +171,10 @@ def get_list( Args: doctype: The DocType to query. - fields: Optional field name or field list to return. + fields: Optional field name or field list to return. Use dictionary syntax + for aggregate functions, for example + [{"SUM": "grand_total", "as": "total"}]. Never pass an aggregate as + a string such as "sum(grand_total) as total". filters: Optional Frappe filters. order_by: Optional ordering expression. limit: Maximum number of rows to return. @@ -450,10 +453,14 @@ def get_print( return file_entry def run_read_only_sql(self, query: str) -> list[dict[str, Any]]: - """Run a read-only SQL query when the current user is allowed to do so. + """Run one complete read-only SQL statement when the user is allowed to do so. + + The query must include the statement keyword and all clauses, for example + "SELECT SUM(grand_total) AS total FROM `tabSales Invoice`". Do not pass only + a SELECT expression such as "sum(grand_total) as total". Args: - query: A single read-only SQL query. + query: One complete SELECT, WITH, SHOW, EXPLAIN, or DESCRIBE statement. Returns: The SQL result rows. From a8d5c2b7f4ed4accb45fa44cd928fe634e1cca34 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:00:26 +0200 Subject: [PATCH 2/2] chore: resolve merge conflict --- ask_alyf/ask_alyf/tools.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ask_alyf/ask_alyf/tools.py b/ask_alyf/ask_alyf/tools.py index 367fb3f..13ae654 100644 --- a/ask_alyf/ask_alyf/tools.py +++ b/ask_alyf/ask_alyf/tools.py @@ -104,14 +104,7 @@ def coerce_field_list(value: str | list[FrappeSelectField] | None) -> list[Frapp except json.JSONDecodeError: parsed = None -<<<<<<< HEAD - try: - parsed = json.loads(stripped) - except (TypeError, json.JSONDecodeError): - parsed = None -======= value = parsed if isinstance(parsed, list) else stripped.split(",") ->>>>>>> ab0ca7a (fix: support aggregate field dictionaries in get_list (#65)) return [ entry if isinstance(entry, dict) else str(entry).strip()