Skip to content

Commit cb218e0

Browse files
authored
Merge pull request #33 from stackql/feature/updates
v3.5.4
2 parents 4181c0a + 33b24f2 commit cb218e0

File tree

6 files changed

+96
-64
lines changed

6 files changed

+96
-64
lines changed

.github/workflows/test.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ jobs:
2626
name: 'Run Tests on ${{matrix.os}} with Python ${{matrix.python-version}}'
2727

2828
steps:
29-
- uses: actions/checkout@v3
29+
- uses: actions/checkout@v4.1.1
3030

3131
- name: Set up Python ${{ matrix.python-version }}
32-
uses: actions/setup-python@v4
32+
uses: actions/setup-python@v5.1.0
3333
with:
3434
python-version: ${{ matrix.python-version }}
3535

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## v3.5.4 (2024-04-11)
4+
5+
### Updates
6+
7+
* added `suppress_errors` argument to the `execute` function
8+
39
## v3.5.3 (2024-04-08)
410

511
### Updates

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,4 @@ To publish the package to PyPI, run the following command:
194194

195195
::
196196

197-
twine upload dist/pystackql-3.5.3.tar.gz
197+
twine upload dist/pystackql-3.5.4.tar.gz

docs/source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
# The short X.Y version
2727
version = ''
2828
# The full version, including alpha/beta/rc tags
29-
release = '3.5.3'
29+
release = '3.5.4'
3030

3131

3232
# -- General configuration ---------------------------------------------------

pystackql/stackql.py

Lines changed: 85 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -187,31 +187,39 @@ def _run_query(self, query):
187187
"""
188188
local_params = self.params.copy()
189189
local_params.insert(1, query)
190+
output = {}
191+
190192
try:
191193
with subprocess.Popen([self.bin_path] + local_params,
192-
stdout=subprocess.PIPE, stderr=subprocess.PIPE) as iqlPopen: # Capturing stderr separately
194+
stdout=subprocess.PIPE, stderr=subprocess.PIPE) as iqlPopen:
193195
stdout, stderr = iqlPopen.communicate()
196+
194197
if self.debug:
195198
self._debug_log(f"Query: {query}")
196199
self._debug_log(f"stdout: {stdout}")
197200
self._debug_log(f"stderr: {stderr}")
201+
202+
# Check if stderr exists
198203
if stderr:
199-
# Prioritizing stderr since that’s where the expected messages seem to be
200-
return stderr.decode('utf-8') if isinstance(stderr, bytes) else str(stderr)
201-
else:
202-
# Here, we may consider concatenating stdout and stderr, or handling them separately based on the use case
203-
return stdout.decode('utf-8') if isinstance(stdout, bytes) else str(stdout)
204+
output["error"] = stderr.decode('utf-8') if isinstance(stderr, bytes) else str(stderr)
205+
206+
# Check if theres data
207+
if stdout:
208+
output["data"] = stdout.decode('utf-8') if isinstance(stdout, bytes) else str(stdout)
209+
204210
except FileNotFoundError:
205-
return "ERROR %s not found" % self.bin_path
211+
output["exception"] = f"ERROR: {self.bin_path} not found"
206212
except Exception as e:
207-
if 'stdout' in locals() and 'stderr' in locals():
208-
return f"ERROR: {str(e)} {e.__doc__}, PARAMS: {local_params}, STDOUT: {stdout}, STDERR: {stderr}"
209-
if 'stdout' in locals() and 'stderr' not in locals():
210-
return f"ERROR: {str(e)} {e.__doc__}, PARAMS: {local_params},STDOUT: {stdout}"
211-
elif 'stderr' in locals():
212-
return f"ERROR: {str(e)} {e.__doc__}, PARAMS: {local_params},STDERR: {stderr}"
213-
else:
214-
return f"ERROR: {str(e)} {e.__doc__}, PARAMS: {local_params}"
213+
error_details = {
214+
"exception": str(e),
215+
"doc": e.__doc__,
216+
"params": local_params,
217+
"stdout": stdout.decode('utf-8') if 'stdout' in locals() and isinstance(stdout, bytes) else "",
218+
"stderr": stderr.decode('utf-8') if 'stderr' in locals() and isinstance(stderr, bytes) else ""
219+
}
220+
output["exception"] = f"ERROR: {json.dumps(error_details)}"
221+
222+
return output
215223

216224
def __init__(self,
217225
server_mode=False,
@@ -474,70 +482,82 @@ def executeStmt(self, query):
474482
else:
475483
return result
476484
else:
477-
result_msg = self._run_query(query)
485+
result = self._run_query(query)
486+
if "exception" in result:
487+
return {"error": result["exception"]}
488+
489+
# message on stderr
490+
message = result["error"]
491+
478492
if self.output == 'pandas':
479-
return pd.DataFrame({'message': [result_msg]})
493+
return pd.DataFrame({'message': [message]}) if message else pd.DataFrame({'message': []})
480494
elif self.output == 'csv':
481-
return result_msg
495+
return message
482496
else:
483-
return [{'message': result_msg}]
497+
return [{'message': message}]
484498

485-
def execute(self, query):
486-
"""Executes a query using the StackQL instance and returns the output
487-
in the format specified by the `output` attribute.
499+
def execute(self, query, suppress_errors=True):
500+
"""
501+
Executes a StackQL query and returns the output based on the specified output format.
488502
489-
Depending on the `server_mode` and `output` attribute of the instance,
490-
this method either runs the query against the StackQL server or executes
491-
it locally using a subprocess, returning the data in a dictionary, Pandas
492-
DataFrame, or CSV format.
503+
This method supports execution both in server mode and locally using subprocess. In server mode,
504+
the query is sent to a StackQL server, while in local mode, it runs the query using a local binary.
493505
494-
:param query: The StackQL query string to be executed.
495-
:type query: str
506+
Args:
507+
query (str): The StackQL query string to be executed.
508+
suppress_errors (bool, optional): If set to True, the method will return an empty list if an error occurs.
496509
497-
:return: The output result of the query. Depending on the `output` attribute,
498-
the result can be a dictionary, a Pandas DataFrame, or a raw CSV string.
499-
CSV output is currently not supported in `server_mode`.
500-
:rtype: dict, pd.DataFrame, or str
510+
Returns:
511+
dict, pd.DataFrame, or str: The output of the query, which can be a dictionary, a Pandas DataFrame,
512+
or a raw CSV string, depending on the configured output format.
501513
502-
Example:
503-
>>> from pystackql import StackQL
514+
Raises:
515+
ValueError: If an unsupported output format is specified.
516+
517+
Examples:
504518
>>> stackql = StackQL()
505-
>>> stackql_query = \"\"\"SELECT SPLIT_PART(machineType, '/', -1) as machine_type,
506-
... status, COUNT(*) as num_instances
507-
... FROM google.compute.instances
508-
... WHERE project = 'stackql-demo'
509-
... AND zone = 'australia-southeast1-a'
510-
... GROUP BY machine_type, status
511-
... HAVING COUNT(*) > 2\"\"\"
512-
>>> result = stackql.execute(stackql_query)
519+
>>> query = '''
520+
SELECT SPLIT_PART(machineType, '/', -1) as machine_type, status, COUNT(*) as num_instances
521+
FROM google.compute.instances
522+
WHERE project = 'stackql-demo' AND zone = 'australia-southeast1-a'
523+
GROUP BY machine_type, status HAVING COUNT(*) > 2
524+
'''
525+
>>> result = stackql.execute(query)
513526
"""
514527
if self.server_mode:
515-
# Use server mode
516528
result = self._run_server_query(query)
517-
518529
if self.output == 'pandas':
519530
json_str = json.dumps(result)
520531
return pd.read_json(StringIO(json_str))
521532
elif self.output == 'csv':
522533
raise ValueError("CSV output is not supported in server_mode.")
523534
else: # Assume 'dict' output
524535
return result
525-
526536
else:
527-
# Local mode handling (existing logic)
528537
output = self._run_query(query)
529-
if self.output == 'csv':
530-
return output
531-
elif self.output == 'pandas':
532-
try:
533-
return pd.read_json(StringIO(output))
534-
except ValueError:
535-
return pd.DataFrame([{"error": "Invalid JSON output: {}".format(output.strip())}])
536-
else: # Assume 'dict' output
537-
try:
538-
return json.loads(output)
539-
except ValueError:
540-
return [{"error": "Invalid JSON output: {}".format(output.strip())}]
538+
if "exception" in output:
539+
return {"error": output["exception"]}
540+
541+
if "data" in output:
542+
# theres data, return it
543+
if self.output == 'csv':
544+
return output["data"]
545+
elif self.output == 'pandas':
546+
try:
547+
return pd.read_json(StringIO(output["data"]))
548+
except ValueError:
549+
return pd.DataFrame([{"error": "Invalid JSON output"}])
550+
else: # Assume 'dict' output
551+
try:
552+
return json.loads(output["data"])
553+
except ValueError:
554+
return {"error": "Invalid JSON output"}
555+
else:
556+
if "error" in output:
557+
if suppress_errors:
558+
return []
559+
else:
560+
return {"error": output["error"]}
541561

542562
#
543563
# asnyc query support
@@ -586,7 +606,13 @@ def _sync_query(self, query, new_connection=False):
586606
result = self._run_server_query(query) # Assuming this is a method that exists
587607
else:
588608
# Convert the JSON string to a Python object (list of dicts).
589-
result = json.loads(self._run_query(query))
609+
query_results = self._run_query(query)
610+
if "exception" in query_results:
611+
result = [{"error": query_results["exception"]}]
612+
if "error" in query_results:
613+
result = [{"error": query_results["error"]}]
614+
if "data" in query_results:
615+
result = json.loads(query_results["data"])
590616
# Convert the result to a DataFrame if necessary.
591617
if self.output == 'pandas':
592618
return pd.DataFrame(result)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
setup(
1212
name='pystackql',
13-
version='3.5.3',
13+
version='3.5.4',
1414
description='A Python interface for StackQL',
1515
long_description=readme,
1616
author='Jeffrey Aven',

0 commit comments

Comments
 (0)