Skip to content

Commit b3a4b35

Browse files
committed
output formatting
1 parent 5d1155d commit b3a4b35

File tree

6 files changed

+295
-709
lines changed

6 files changed

+295
-709
lines changed

pystackql/magic_ext/base.py

Lines changed: 141 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,101 @@
1+
# # pystackql/magic_ext/base.py
2+
3+
# """
4+
# Base Jupyter magic extension for PyStackQL.
5+
6+
# This module provides the base class for PyStackQL Jupyter magic extensions.
7+
# """
8+
9+
# from __future__ import print_function
10+
# from IPython.core.magic import Magics
11+
# from string import Template
12+
13+
# class BaseStackqlMagic(Magics):
14+
# """Base Jupyter magic extension enabling running StackQL queries.
15+
16+
# This extension allows users to conveniently run StackQL queries against cloud
17+
# or SaaS resources directly from Jupyter notebooks, and visualize the results in a tabular
18+
# format using Pandas DataFrames.
19+
# """
20+
# def __init__(self, shell, server_mode):
21+
# """Initialize the BaseStackqlMagic class.
22+
23+
# :param shell: The IPython shell instance.
24+
# :param server_mode: Whether to use server mode.
25+
# """
26+
# from ..core import StackQL
27+
# super(BaseStackqlMagic, self).__init__(shell)
28+
# self.stackql_instance = StackQL(server_mode=server_mode, output='pandas')
29+
30+
# def get_rendered_query(self, data):
31+
# """Substitute placeholders in a query template with variables from the current namespace.
32+
33+
# :param data: SQL query template containing placeholders.
34+
# :type data: str
35+
# :return: A SQL query with placeholders substituted.
36+
# :rtype: str
37+
# """
38+
# t = Template(data)
39+
# return t.substitute(self.shell.user_ns)
40+
41+
# def run_query(self, query):
42+
# """Execute a StackQL query
43+
44+
# :param query: StackQL query to be executed.
45+
# :type query: str
46+
# :return: Query results, returned as a Pandas DataFrame.
47+
# :rtype: pandas.DataFrame
48+
# """
49+
# # Check if the query starts with "registry pull" (case insensitive)
50+
# if query.strip().lower().startswith("registry pull"):
51+
# return self.stackql_instance.executeStmt(query)
52+
53+
# return self.stackql_instance.execute(query)
54+
55+
# def _display_with_csv_download(self, df):
56+
# """Display a CSV download link for the DataFrame without displaying the DataFrame again.
57+
58+
# :param df: The DataFrame to make downloadable.
59+
# """
60+
# import IPython.display
61+
62+
# try:
63+
# # Generate CSV data
64+
# import io
65+
# import base64
66+
# csv_buffer = io.StringIO()
67+
# df.to_csv(csv_buffer, index=False)
68+
# csv_data = csv_buffer.getvalue()
69+
70+
# # Encode to base64 for data URI
71+
# csv_base64 = base64.b64encode(csv_data.encode()).decode()
72+
73+
# # Create download link
74+
# download_link = f'data:text/csv;base64,{csv_base64}'
75+
76+
# # Only display the download button, not the DataFrame
77+
# download_html = f'''
78+
# <div style="margin-top: 15px; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', sans-serif;">
79+
# <a href="{download_link}" download="stackql_results.csv"
80+
# style="display: inline-flex; align-items: center; gap: 8px; padding: 9px 16px;
81+
# background-color: #2196F3; color: white; text-decoration: none;
82+
# border-radius: 4px; font-size: 14px; font-weight: 500;
83+
# box-shadow: 0 2px 4px rgba(0,0,0,0.08); transition: all 0.2s ease;">
84+
# <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
85+
# <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
86+
# <polyline points="7 10 12 15 17 10"></polyline>
87+
# <line x1="12" y1="15" x2="12" y2="3"></line>
88+
# </svg>
89+
# Download CSV
90+
# </a>
91+
# </div>
92+
# '''
93+
# IPython.display.display(IPython.display.HTML(download_html))
94+
95+
# except Exception as e:
96+
# # If CSV generation fails, just print an error message without displaying anything
97+
# print(f"Error generating CSV download: {e}")
98+
199
# pystackql/magic_ext/base.py
2100

3101
"""
@@ -7,8 +105,9 @@
7105
"""
8106

9107
from __future__ import print_function
10-
from IPython.core.magic import Magics
108+
from IPython.core.magic import Magics, line_cell_magic
11109
from string import Template
110+
import argparse
12111

13112
class BaseStackqlMagic(Magics):
14113
"""Base Jupyter magic extension enabling running StackQL queries.
@@ -26,6 +125,7 @@ def __init__(self, shell, server_mode):
26125
from ..core import StackQL
27126
super(BaseStackqlMagic, self).__init__(shell)
28127
self.stackql_instance = StackQL(server_mode=server_mode, output='pandas')
128+
self.server_mode = server_mode
29129

30130
def get_rendered_query(self, data):
31131
"""Substitute placeholders in a query template with variables from the current namespace.
@@ -52,55 +152,49 @@ def run_query(self, query):
52152

53153
return self.stackql_instance.execute(query)
54154

55-
# def _display_with_csv_download(self, df):
56-
# """Display DataFrame with CSV download link.
155+
@line_cell_magic
156+
def stackql(self, line, cell=None):
157+
"""A Jupyter magic command to run StackQL queries.
57158
58-
# :param df: The DataFrame to display and make downloadable.
59-
# """
60-
# import IPython.display
159+
Can be used as both line and cell magic:
160+
- As a line magic: `%stackql QUERY`
161+
- As a cell magic: `%%stackql [OPTIONS]` followed by the QUERY in the next line.
61162
62-
# try:
63-
# # Generate CSV data
64-
# import io
65-
# import base64
66-
# csv_buffer = io.StringIO()
67-
# df.to_csv(csv_buffer, index=False)
68-
# csv_data = csv_buffer.getvalue()
69-
70-
# # Encode to base64 for data URI
71-
# csv_base64 = base64.b64encode(csv_data.encode()).decode()
72-
73-
# # Create download link
74-
# download_link = f'data:text/csv;base64,{csv_base64}'
75-
76-
# # # Display the DataFrame first
77-
# # IPython.display.display(df)
78-
79-
# # Create and display the download button
80-
# download_html = f'''
81-
# <div style="margin-top: 15px; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', sans-serif;">
82-
# <a href="{download_link}" download="stackql_results.csv"
83-
# style="display: inline-flex; align-items: center; gap: 8px; padding: 9px 16px;
84-
# background-color: #2196F3; color: white; text-decoration: none;
85-
# border-radius: 4px; font-size: 14px; font-weight: 500;
86-
# box-shadow: 0 2px 4px rgba(0,0,0,0.08); transition: all 0.2s ease;">
87-
# <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
88-
# <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
89-
# <polyline points="7 10 12 15 17 10"></polyline>
90-
# <line x1="12" y1="15" x2="12" y2="3"></line>
91-
# </svg>
92-
# Download CSV
93-
# </a>
94-
# </div>
95-
# '''
96-
97-
# IPython.display.display(IPython.display.HTML(download_html))
98-
99-
# except Exception as e:
100-
# # If CSV generation fails, just display the DataFrame normally
101-
# IPython.display.display(df)
102-
# print(f"Error generating CSV download: {e}")
163+
:param line: The arguments and/or StackQL query when used as line magic.
164+
:param cell: The StackQL query when used as cell magic.
165+
:return: StackQL query results as a named Pandas DataFrame (`stackql_df`).
166+
"""
167+
is_cell_magic = cell is not None
103168

169+
if is_cell_magic:
170+
parser = argparse.ArgumentParser()
171+
parser.add_argument("--no-display", action="store_true", help="Suppress result display.")
172+
parser.add_argument("--csv-download", action="store_true", help="Add CSV download link to output.")
173+
args = parser.parse_args(line.split())
174+
query_to_run = self.get_rendered_query(cell)
175+
else:
176+
args = None
177+
query_to_run = self.get_rendered_query(line)
178+
179+
results = self.run_query(query_to_run)
180+
self.shell.user_ns['stackql_df'] = results
181+
182+
if is_cell_magic and args and args.no_display:
183+
return None
184+
elif is_cell_magic and args and args.csv_download and not args.no_display:
185+
# First display the DataFrame
186+
import IPython.display
187+
IPython.display.display(results)
188+
# Then add the download button without displaying the DataFrame again
189+
self._display_with_csv_download(results)
190+
return results
191+
elif is_cell_magic and args and not args.no_display:
192+
return results
193+
elif not is_cell_magic:
194+
return results
195+
else:
196+
return results
197+
104198
def _display_with_csv_download(self, df):
105199
"""Display a CSV download link for the DataFrame without displaying the DataFrame again.
106200

pystackql/magic_ext/local.py

Lines changed: 80 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,82 @@
1+
# # pystackql/magic_ext/local.py
2+
3+
# """
4+
# Local Jupyter magic extension for PyStackQL.
5+
6+
# This module provides a Jupyter magic command for running StackQL queries
7+
# using a local StackQL binary.
8+
# """
9+
10+
# from IPython.core.magic import (magics_class, line_cell_magic)
11+
# from .base import BaseStackqlMagic
12+
# import argparse
13+
14+
# @magics_class
15+
# class StackqlMagic(BaseStackqlMagic):
16+
# """Jupyter magic command for running StackQL queries in local mode."""
17+
18+
# def __init__(self, shell):
19+
# """Initialize the StackqlMagic class.
20+
21+
# :param shell: The IPython shell instance.
22+
# """
23+
# super().__init__(shell, server_mode=False)
24+
25+
# @line_cell_magic
26+
# def stackql(self, line, cell=None):
27+
# """A Jupyter magic command to run StackQL queries.
28+
29+
# Can be used as both line and cell magic:
30+
# - As a line magic: `%stackql QUERY`
31+
# - As a cell magic: `%%stackql [OPTIONS]` followed by the QUERY in the next line.
32+
33+
# :param line: The arguments and/or StackQL query when used as line magic.
34+
# :param cell: The StackQL query when used as cell magic.
35+
# :return: StackQL query results as a named Pandas DataFrame (`stackql_df`).
36+
# """
37+
# is_cell_magic = cell is not None
38+
39+
# if is_cell_magic:
40+
# parser = argparse.ArgumentParser()
41+
# parser.add_argument("--no-display", action="store_true", help="Suppress result display.")
42+
# parser.add_argument("--csv-download", action="store_true", help="Add CSV download link to output.")
43+
# args = parser.parse_args(line.split())
44+
# query_to_run = self.get_rendered_query(cell)
45+
# else:
46+
# args = None
47+
# query_to_run = self.get_rendered_query(line)
48+
49+
# results = self.run_query(query_to_run)
50+
# self.shell.user_ns['stackql_df'] = results
51+
52+
# if is_cell_magic and args and args.no_display:
53+
# return None
54+
# elif is_cell_magic and args and args.csv_download and not args.no_display:
55+
# # First display the DataFrame
56+
# import IPython.display
57+
# IPython.display.display(results)
58+
# # Then add the download button without displaying the DataFrame again
59+
# self._display_with_csv_download(results)
60+
# return results
61+
# elif is_cell_magic and args and not args.no_display:
62+
# return results
63+
# elif not is_cell_magic:
64+
# return results
65+
# else:
66+
# return results
67+
68+
# def load_ipython_extension(ipython):
69+
# """Load the non-server magic in IPython.
70+
71+
# This is called when running %load_ext pystackql.magic in a notebook.
72+
# It registers the %stackql and %%stackql magic commands.
73+
74+
# :param ipython: The IPython shell instance
75+
# """
76+
# # Create an instance of the magic class and register it
77+
# magic_instance = StackqlMagic(ipython)
78+
# ipython.register_magics(magic_instance)
79+
180
# pystackql/magic_ext/local.py
281

382
"""
@@ -7,9 +86,8 @@
786
using a local StackQL binary.
887
"""
988

10-
from IPython.core.magic import (magics_class, line_cell_magic)
89+
from IPython.core.magic import magics_class
1190
from .base import BaseStackqlMagic
12-
import argparse
1391

1492
@magics_class
1593
class StackqlMagic(BaseStackqlMagic):
@@ -22,86 +100,6 @@ def __init__(self, shell):
22100
"""
23101
super().__init__(shell, server_mode=False)
24102

25-
# @line_cell_magic
26-
# def stackql(self, line, cell=None):
27-
# """A Jupyter magic command to run StackQL queries.
28-
29-
# Can be used as both line and cell magic:
30-
# - As a line magic: `%stackql QUERY`
31-
# - As a cell magic: `%%stackql [OPTIONS]` followed by the QUERY in the next line.
32-
33-
# :param line: The arguments and/or StackQL query when used as line magic.
34-
# :param cell: The StackQL query when used as cell magic.
35-
# :return: StackQL query results as a named Pandas DataFrame (`stackql_df`).
36-
# """
37-
# is_cell_magic = cell is not None
38-
39-
# if is_cell_magic:
40-
# parser = argparse.ArgumentParser()
41-
# parser.add_argument("--no-display", action="store_true", help="Suppress result display.")
42-
# parser.add_argument("--csv-download", action="store_true", help="Add CSV download link to output.")
43-
# args = parser.parse_args(line.split())
44-
# query_to_run = self.get_rendered_query(cell)
45-
# else:
46-
# args = None
47-
# query_to_run = self.get_rendered_query(line)
48-
49-
# results = self.run_query(query_to_run)
50-
# self.shell.user_ns['stackql_df'] = results
51-
52-
# if is_cell_magic and args and args.no_display:
53-
# return None
54-
# elif is_cell_magic and args and args.csv_download and not args.no_display:
55-
# self._display_with_csv_download(results)
56-
# return results
57-
# elif is_cell_magic and args and not args.no_display:
58-
# return results
59-
# elif not is_cell_magic:
60-
# return results
61-
62-
@line_cell_magic
63-
def stackql(self, line, cell=None):
64-
"""A Jupyter magic command to run StackQL queries.
65-
66-
Can be used as both line and cell magic:
67-
- As a line magic: `%stackql QUERY`
68-
- As a cell magic: `%%stackql [OPTIONS]` followed by the QUERY in the next line.
69-
70-
:param line: The arguments and/or StackQL query when used as line magic.
71-
:param cell: The StackQL query when used as cell magic.
72-
:return: StackQL query results as a named Pandas DataFrame (`stackql_df`).
73-
"""
74-
is_cell_magic = cell is not None
75-
76-
if is_cell_magic:
77-
parser = argparse.ArgumentParser()
78-
parser.add_argument("--no-display", action="store_true", help="Suppress result display.")
79-
parser.add_argument("--csv-download", action="store_true", help="Add CSV download link to output.")
80-
args = parser.parse_args(line.split())
81-
query_to_run = self.get_rendered_query(cell)
82-
else:
83-
args = None
84-
query_to_run = self.get_rendered_query(line)
85-
86-
results = self.run_query(query_to_run)
87-
self.shell.user_ns['stackql_df'] = results
88-
89-
if is_cell_magic and args and args.no_display:
90-
return None
91-
elif is_cell_magic and args and args.csv_download and not args.no_display:
92-
# First display the DataFrame
93-
import IPython.display
94-
IPython.display.display(results)
95-
# Then add the download button without displaying the DataFrame again
96-
self._display_with_csv_download(results)
97-
return results
98-
elif is_cell_magic and args and not args.no_display:
99-
return results
100-
elif not is_cell_magic:
101-
return results
102-
else:
103-
return results
104-
105103
def load_ipython_extension(ipython):
106104
"""Load the non-server magic in IPython.
107105

0 commit comments

Comments
 (0)