Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions ask_alyf/ask_alyf/test_code_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
32 changes: 17 additions & 15 deletions ask_alyf/ask_alyf/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -89,26 +90,27 @@ 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()]

stripped = str(value).strip()
if not stripped:
return None
if isinstance(value, str):
stripped = value.strip()
if not stripped:
return None

try:
parsed = json.loads(stripped)
except (TypeError, json.JSONDecodeError):
parsed = None
try:
parsed = json.loads(stripped)
except json.JSONDecodeError:
parsed = None

if isinstance(parsed, list):
return [str(entry).strip() for entry in parsed if str(entry).strip()]
value = parsed if isinstance(parsed, list) else stripped.split(",")

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():
Expand Down Expand Up @@ -331,7 +333,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,
Expand Down
15 changes: 11 additions & 4 deletions ask_alyf/ask_alyf/toolset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
Loading