Skip to content

Commit c08b80c

Browse files
committed
v3.2.0 updates to magic ext
1 parent dcffe30 commit c08b80c

File tree

10 files changed

+168
-125
lines changed

10 files changed

+168
-125
lines changed

CHANGELOG.md

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

3+
## v3.2.0 (2023-10-18)
4+
5+
* implemented non `server_mode` magic extension
6+
7+
### Updates
8+
9+
* `pandas` type fixes
10+
311
## v3.1.2 (2023-10-16)
412

513
### Updates

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.1.2'
29+
release = '3.2.0'
3030

3131

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

docs/source/magic_ext.rst

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
StackqlMagic Extension for Jupyter
22
==================================
33

4-
The ``StackqlMagic`` extension for Jupyter notebooks provides a convenient interface to run SQL queries against the StackQL database directly from within the notebook environment. Results can be visualized in a tabular format using Pandas DataFrames.
4+
The ``StackqlMagic`` extension for Jupyter notebooks provides a convenient interface to run StackQL queries against cloud or SaaS providers directly from within the notebook environment. Results can be visualized in a tabular format using Pandas DataFrames.
55

66
Setup
77
-----
@@ -10,7 +10,13 @@ To enable the `StackqlMagic` extension in your Jupyter notebook, use the followi
1010

1111
.. code-block:: python
1212
13-
%load_ext pystackql
13+
%load_ext stackql_magic
14+
15+
To use the `StackqlMagic` extension in your Jupyter notebook to run queries against a StackQL server, use the following command:
16+
17+
.. code-block:: python
18+
19+
%load_ext stackql_server_magic
1420
1521
Usage
1622
-----
@@ -54,16 +60,23 @@ Example:
5460
.. code-block:: python
5561
5662
%%stackql --no-display
57-
SELECT name, status
63+
SELECT SPLIT_PART(machineType, '/', -1) as machine_type, count(*) as num_instances
5864
FROM google.compute.instances
5965
WHERE project = '$project' AND zone = '$zone'
66+
GROUP BY machine_type
6067
6168
This will run the query but won't display the results in the notebook. Instead, you can later access the results via the `stackql_df` variable.
6269

6370
.. note::
6471

6572
The results of the queries are always saved in a Pandas DataFrame named `stackql_df` in the notebook's current namespace. This allows you to further process or visualize the data as needed.
6673

74+
An example of visualizing the results using Pandas is shown below:
75+
76+
.. code-block:: python
77+
78+
stackql_df.plot(kind='pie', y='num_instances', labels=_['machine_type'], title='Instances by Type', autopct='%1.1f%%')
79+
6780
--------
6881

6982
This documentation provides a basic overview and usage guide for the `StackqlMagic` extension. For advanced usage or any additional features provided by the extension, refer to the source code or any other accompanying documentation.

pystackql/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from .stackql import StackQL
2-
from .stackql_magic import load_ipython_extension
2+
from .stackql_magic import StackqlMagic, load_non_server_magic
3+
from .stackql_server_magic import StackqlServerMagic, load_server_magic

pystackql/base_stackql_magic.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from __future__ import print_function
2+
import pandas as pd
3+
import json, argparse
4+
from IPython.core.magic import (Magics, line_cell_magic)
5+
from string import Template
6+
7+
class BaseStackqlMagic(Magics):
8+
"""Base Jupyter magic extension enabling running StackQL queries.
9+
10+
This extension allows users to conveniently run StackQL queries against cloud
11+
or SaaS reources directly from Jupyter notebooks, and visualize the results in a tabular
12+
format using Pandas DataFrames.
13+
"""
14+
def __init__(self, shell, server_mode):
15+
"""Initialize the StackqlMagic class.
16+
17+
:param shell: The IPython shell instance.
18+
"""
19+
from . import StackQL
20+
super(BaseStackqlMagic, self).__init__(shell)
21+
self.stackql_instance = StackQL(server_mode=server_mode, output='pandas')
22+
23+
def get_rendered_query(self, data):
24+
"""Substitute placeholders in a query template with variables from the current namespace.
25+
26+
:param data: SQL query template containing placeholders.
27+
:type data: str
28+
:return: A SQL query with placeholders substituted.
29+
:rtype: str
30+
"""
31+
t = Template(data)
32+
return t.substitute(self.shell.user_ns)
33+
34+
def run_query(self, query):
35+
"""Execute a StackQL query
36+
37+
:param query: StackQL query to be executed.
38+
:type query: str
39+
:return: Query results, returned as a Pandas DataFrame.
40+
:rtype: pandas.DataFrame
41+
"""
42+
return self.stackql_instance.execute(query)
43+
44+
@line_cell_magic
45+
def stackql(self, line, cell=None):
46+
"""A Jupyter magic command to run StackQL queries.
47+
48+
Can be used as both line and cell magic:
49+
- As a line magic: `%stackql QUERY`
50+
- As a cell magic: `%%stackql [OPTIONS]` followed by the QUERY in the next line.
51+
52+
:param line: The arguments and/or StackQL query when used as line magic.
53+
:param cell: The StackQL query when used as cell magic.
54+
:return: StackQL query results as a named Pandas DataFrame (`stackql_df`).
55+
"""
56+
is_cell_magic = cell is not None
57+
58+
if is_cell_magic:
59+
parser = argparse.ArgumentParser()
60+
parser.add_argument("--no-display", action="store_true", help="Suppress result display.")
61+
args = parser.parse_args(line.split())
62+
query_to_run = self.get_rendered_query(cell)
63+
else:
64+
args = None
65+
query_to_run = self.get_rendered_query(line)
66+
67+
results = self.run_query(query_to_run)
68+
self.shell.user_ns['stackql_df'] = results
69+
70+
if is_cell_magic and args and not args.no_display:
71+
return results
72+
elif not is_cell_magic:
73+
return results

pystackql/stackql_magic.py

Lines changed: 7 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,12 @@
1-
from __future__ import print_function
2-
import pandas as pd
3-
import json, argparse
4-
from IPython.core.magic import (Magics, magics_class, line_cell_magic)
5-
from string import Template
1+
# stackql_magic.py
2+
from IPython.core.magic import magics_class
3+
from .base_stackql_magic import BaseStackqlMagic
64

75
@magics_class
8-
class StackqlMagic(Magics):
9-
"""
10-
A Jupyter magic extension enabling SQL querying against a StackQL database.
11-
12-
This extension allows users to conveniently run SQL queries against the StackQL
13-
database directly from Jupyter notebooks, and visualize the results in a tabular
14-
format using Pandas DataFrames.
15-
"""
16-
6+
class StackqlMagic(BaseStackqlMagic):
177
def __init__(self, shell):
18-
"""
19-
Initialize the StackqlMagic class.
20-
21-
:param shell: The IPython shell instance.
22-
"""
23-
from . import StackQL
24-
super(StackqlMagic, self).__init__(shell)
25-
self.stackql_instance = StackQL(server_mode=True, output='pandas')
26-
27-
def get_rendered_query(self, data):
28-
"""
29-
Substitute placeholders in a query template with variables from the current namespace.
30-
31-
:param data: SQL query template containing placeholders.
32-
:type data: str
33-
:return: A SQL query with placeholders substituted.
34-
:rtype: str
35-
"""
36-
t = Template(data)
37-
return t.substitute(self.shell.user_ns)
38-
39-
def run_query(self, query):
40-
"""
41-
Execute a StackQL query
42-
43-
:param query: StackQL query to be executed.
44-
:type query: str
45-
:return: Query results, returned as a Pandas DataFrame.
46-
:rtype: pandas.DataFrame
47-
"""
48-
return self.stackql_instance.execute(query)
49-
50-
@line_cell_magic
51-
def stackql(self, line, cell=None):
52-
"""
53-
A Jupyter magic command to run SQL queries against the StackQL database.
54-
55-
Can be used as both line and cell magic:
56-
- As a line magic: `%stackql QUERY`
57-
- As a cell magic: `%%stackql [OPTIONS]` followed by the QUERY in the next line.
58-
59-
:param line: The arguments and/or StackQL query when used as line magic.
60-
:param cell: The StackQL query when used as cell magic.
61-
:return: StackQL query results as a named Pandas DataFrame (`stackql_df`).
62-
"""
63-
is_cell_magic = cell is not None
64-
65-
if is_cell_magic:
66-
parser = argparse.ArgumentParser()
67-
parser.add_argument("--no-display", action="store_true", help="Suppress result display.")
68-
args = parser.parse_args(line.split())
69-
query_to_run = self.get_rendered_query(cell)
70-
else:
71-
args = None
72-
query_to_run = self.get_rendered_query(line)
73-
74-
results = self.run_query(query_to_run)
75-
self.shell.user_ns['stackql_df'] = results
76-
77-
if is_cell_magic and args and not args.no_display:
78-
return results
79-
elif not is_cell_magic:
80-
return results
8+
super().__init__(shell, server_mode=False)
819

82-
def load_ipython_extension(ipython):
83-
"""
84-
Enable the StackqlMagic extension in IPython.
85-
86-
This function allows the extension to be loaded via the `%load_ext` command or
87-
be automatically loaded by IPython at startup.
88-
"""
10+
def load_non_server_magic(ipython):
11+
"""Load the non-server magic in IPython."""
8912
ipython.register_magics(StackqlMagic)

pystackql/stackql_server_magic.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# stackql_server_magic.py
2+
from IPython.core.magic import magics_class
3+
from .base_stackql_magic import BaseStackqlMagic
4+
5+
@magics_class
6+
class StackqlServerMagic(BaseStackqlMagic):
7+
8+
def __init__(self, shell):
9+
super().__init__(shell, server_mode=True)
10+
11+
def load_server_magic(ipython):
12+
"""Load the extension in IPython."""
13+
ipython.register_magics(StackqlServerMagic)

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.1.2',
13+
version='3.2.0',
1414
description='A Python interface for StackQL',
1515
long_description=readme,
1616
author='Jeffrey Aven',

tests/pystackql_tests.py

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import sys, os, unittest, asyncio
22
from unittest.mock import MagicMock
33
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
4-
from pystackql import StackQL
5-
from pystackql.stackql_magic import StackqlMagic, load_ipython_extension
4+
from pystackql import StackQL, load_non_server_magic, load_server_magic, StackqlMagic, StackqlServerMagic
65
from .test_params import *
76

87
def pystackql_test_setup(**kwargs):
@@ -275,48 +274,61 @@ def instance():
275274
"""Return a mock instance of the shell."""
276275
return MockInteractiveShell()
277276

278-
class StackQLMagicTests(PyStackQLTestsBase):
279-
277+
class BaseStackQLMagicTests:
278+
MAGIC_CLASS = None # To be overridden by child classes
279+
server_mode = None # To be overridden by child classes
280280
def setUp(self):
281281
"""Set up for the magic tests."""
282+
assert self.MAGIC_CLASS, "MAGIC_CLASS should be set by child classes"
282283
self.shell = MockInteractiveShell.instance()
283-
load_ipython_extension(self.shell)
284-
self.stackql_magic = StackqlMagic(shell=self.shell)
284+
if self.server_mode:
285+
load_server_magic(self.shell)
286+
else:
287+
load_non_server_magic(self.shell)
288+
self.stackql_magic = self.MAGIC_CLASS(shell=self.shell)
285289
self.query = "SELECT 1 as fred"
286290
self.expected_result = pd.DataFrame({"fred": [1]})
287291

288-
def test_23_line_magic_query(self):
289-
# Mock the run_query method to return a known DataFrame.
290-
self.stackql_magic.run_query = MagicMock(return_value=self.expected_result)
291-
# Execute the line magic with our query.
292-
result = self.stackql_magic.stackql(line=self.query, cell=None)
293-
# Check if the result is as expected and if 'stackql_df' is set in the namespace.
294-
self.assertTrue(result.equals(self.expected_result))
295-
self.assertTrue('stackql_df' in self.shell.user_ns)
296-
self.assertTrue(self.shell.user_ns['stackql_df'].equals(self.expected_result))
297-
print_test_result(f"""Line magic test""", True, True, True)
298-
299-
def test_24_cell_magic_query(self):
300-
# Mock the run_query method to return a known DataFrame.
301-
self.stackql_magic.run_query = MagicMock(return_value=self.expected_result)
302-
# Execute the cell magic with our query.
303-
result = self.stackql_magic.stackql(line="", cell=self.query)
304-
# Validate the outcome.
305-
self.assertTrue(result.equals(self.expected_result))
306-
self.assertTrue('stackql_df' in self.shell.user_ns)
307-
self.assertTrue(self.shell.user_ns['stackql_df'].equals(self.expected_result))
308-
print_test_result(f"""Cell magic test""", True, True, True)
292+
def print_test_result(self, test_name, *checks):
293+
all_passed = all(checks)
294+
print_test_result(f"{test_name}, server_mode: {self.server_mode}", all_passed, True, True)
309295

310-
def test_25_cell_magic_query_no_output(self):
296+
def run_magic_test(self, line, cell, expect_none=False):
311297
# Mock the run_query method to return a known DataFrame.
312298
self.stackql_magic.run_query = MagicMock(return_value=self.expected_result)
313-
# Execute the cell magic with our query and the no-display argument.
314-
result = self.stackql_magic.stackql(line="--no-display", cell=self.query)
299+
# Execute the magic with our query.
300+
result = self.stackql_magic.stackql(line=line, cell=cell)
315301
# Validate the outcome.
316-
self.assertIsNone(result)
317-
self.assertTrue('stackql_df' in self.shell.user_ns)
318-
self.assertTrue(self.shell.user_ns['stackql_df'].equals(self.expected_result))
319-
print_test_result(f"""Cell magic test (with --no-display)""", True, True, True)
302+
checks = []
303+
if expect_none:
304+
checks.append(result is None)
305+
else:
306+
checks.append(result.equals(self.expected_result))
307+
checks.append('stackql_df' in self.shell.user_ns)
308+
checks.append(self.shell.user_ns['stackql_df'].equals(self.expected_result))
309+
return checks
310+
311+
def test_line_magic_query(self):
312+
checks = self.run_magic_test(line=self.query, cell=None)
313+
self.print_test_result("Line magic test", *checks)
314+
315+
def test_cell_magic_query(self):
316+
checks = self.run_magic_test(line="", cell=self.query)
317+
self.print_test_result("Cell magic test", *checks)
318+
319+
def test_cell_magic_query_no_output(self):
320+
checks = self.run_magic_test(line="--no-display", cell=self.query, expect_none=True)
321+
self.print_test_result("Cell magic test (with --no-display)", *checks)
322+
323+
324+
class StackQLMagicTests(BaseStackQLMagicTests, unittest.TestCase):
325+
326+
MAGIC_CLASS = StackqlMagic
327+
server_mode = False
328+
329+
class StackQLServerMagicTests(BaseStackQLMagicTests, unittest.TestCase):
330+
MAGIC_CLASS = StackqlServerMagic
331+
server_mode = True
320332

321333
def main():
322334
unittest.main(verbosity=0)

tests/test_params.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def registry_pull_resp_pattern(provider):
6262
for region in regions
6363
]
6464

65-
def print_test_result(test_name, condition, server_mode=False, is_ipython=False):
65+
def print_test_result(test_name, condition=True, server_mode=False, is_ipython=False):
6666
status_header = colored("[PASSED] ", 'green') if condition else colored("[FAILED] ", 'red')
6767
headers = [status_header]
6868

0 commit comments

Comments
 (0)