@@ -23,7 +23,7 @@ class PolarsTransformError(RuntimeError):
2323class PolarsPipeline :
2424 sql : str
2525 expression : str | None
26- parquet_path : str | None
26+ output_path : str | None
2727 output_mode : OutputMode
2828
2929
@@ -37,7 +37,7 @@ class PolarsTransform:
3737
3838
3939def parse_polars_transform (command : str ) -> PolarsPipeline | None :
40- """Parse a SQL statement with optional Polars transform and Parquet output."""
40+ """Parse a SQL statement with optional Polars transform and file output."""
4141 try :
4242 tokens = sqlglot .tokenize (command )
4343 except sqlglot .errors .TokenError as exc :
@@ -56,7 +56,7 @@ def parse_polars_transform(command: str) -> PolarsPipeline | None:
5656 if following .end + 1 >= len (command ):
5757 if following .token_type == sqlglot .TokenType .PIPE :
5858 raise PolarsTransformError ('Polars transforms require a Python expression.' )
59- raise PolarsTransformError ('Parquet saves require a destination path.' )
59+ raise PolarsTransformError ('File saves require a destination path.' )
6060 if not command [following .end + 1 ].isspace ():
6161 continue
6262 if following .token_type == sqlglot .TokenType .PIPE :
@@ -65,7 +65,7 @@ def parse_polars_transform(command: str) -> PolarsPipeline | None:
6565 pipe_index = index
6666 else :
6767 if parquet_index is not None :
68- raise PolarsTransformError ('Parquet saves support only one ".>" operator.' )
68+ raise PolarsTransformError ('File saves support only one ".>" operator.' )
6969 parquet_index = index
7070
7171 if pipe_index is None and parquet_index is None :
@@ -77,20 +77,20 @@ def parse_polars_transform(command: str) -> PolarsPipeline | None:
7777 assert first_index is not None
7878 sql = command [: tokens [first_index ].start ].strip ()
7979 expression : str | None = None
80- parquet_path : str | None = None
80+ output_path : str | None = None
8181 if pipe_index is not None :
8282 pipe_operator = tokens [pipe_index + 1 ]
8383 expression_end = tokens [parquet_index ].start if parquet_index is not None else len (command )
8484 expression = command [pipe_operator .end + 1 : expression_end ].strip ()
8585 if parquet_index is not None :
8686 parquet_operator = tokens [parquet_index + 1 ]
87- parquet_path = command [parquet_operator .end + 1 :].strip ()
88- parquet_path = parquet_path .removesuffix (delimiter_command .current ).rstrip ()
89- parquet_path = parquet_path .removesuffix (r'\g' ).rstrip ()
87+ output_path = command [parquet_operator .end + 1 :].strip ()
88+ output_path = output_path .removesuffix (delimiter_command .current ).rstrip ()
89+ output_path = output_path .removesuffix (r'\g' ).rstrip ()
9090
91- has_display_terminator = any (value is not None and value .endswith ((r'\x' , r'\G' )) for value in (sql , expression , parquet_path ))
92- if parquet_path is not None and has_display_terminator :
93- raise PolarsTransformError ('Parquet saves cannot use special display terminators.' )
91+ has_display_terminator = any (value is not None and value .endswith ((r'\x' , r'\G' )) for value in (sql , expression , output_path ))
92+ if output_path is not None and has_display_terminator :
93+ raise PolarsTransformError ('File saves cannot use special display terminators.' )
9494 if sql .endswith (r'\x' ) or expression is not None and expression .endswith (r'\x' ):
9595 output_mode : OutputMode = 'explorer'
9696 elif sql .endswith (r'\G' ) or expression is not None and expression .endswith (r'\G' ):
@@ -107,23 +107,23 @@ def parse_polars_transform(command: str) -> PolarsPipeline | None:
107107 raise PolarsTransformError ('Polars transforms require a SQL statement.' )
108108 if expression is not None and not expression :
109109 raise PolarsTransformError ('Polars transforms require a Python expression.' )
110- if parquet_path is not None :
111- if not parquet_path :
112- raise PolarsTransformError ('Parquet saves require a destination path.' )
113- parquet_path = _parse_parquet_path ( parquet_path )
110+ if output_path is not None :
111+ if not output_path :
112+ raise PolarsTransformError ('File saves require a destination path.' )
113+ output_path = _parse_output_path ( output_path )
114114 _validate_sql (sql )
115- return PolarsPipeline (sql = sql , expression = expression , parquet_path = parquet_path , output_mode = output_mode )
115+ return PolarsPipeline (sql = sql , expression = expression , output_path = output_path , output_mode = output_mode )
116116
117117
118- def _parse_parquet_path (path : str ) -> str :
118+ def _parse_output_path (path : str ) -> str :
119119 if path [0 ] in ('\' ' , '"' ):
120120 if len (path ) < 2 or path [- 1 ] != path [0 ]:
121- raise PolarsTransformError ('Parquet save paths must use matching quotes.' )
121+ raise PolarsTransformError ('File save paths must use matching quotes.' )
122122 path = path [1 :- 1 ]
123123 elif any (character .isspace () for character in path ):
124- raise PolarsTransformError ('Parquet save paths containing spaces must be quoted.' )
125- if not path .lower ().endswith ('.parquet' ):
126- raise PolarsTransformError ('Parquet save paths must end in ".parquet".' )
124+ raise PolarsTransformError ('File save paths containing spaces must be quoted.' )
125+ if not path .lower ().endswith (( '.parquet' , '.png' ) ):
126+ raise PolarsTransformError ('File save paths must end in ".parquet" or ".png ".' )
127127 return path
128128
129129
@@ -181,7 +181,7 @@ def _load_vl_convert() -> None:
181181def run_polars_transform (
182182 transform : PolarsTransform ,
183183 results : Iterable [SQLResult ],
184- parquet_path : str | None = None ,
184+ output_path : str | None = None ,
185185 * ,
186186 image_protocol : ImageProtocol = 'none' ,
187187 plot_scale_factor : float = 1.0 ,
@@ -212,34 +212,49 @@ def run_polars_transform(
212212 except Exception as exc :
213213 raise PolarsTransformError (f'Polars expression failed: { type (exc ).__name__ } : { exc } ' ) from exc
214214 if isinstance (value , transform .polars .DataFrame ):
215- if parquet_path is not None :
215+ if output_path is not None :
216+ if not output_path .lower ().endswith ('.parquet' ):
217+ raise PolarsTransformError ('Polars DataFrame results can only be written to ".parquet" files.' )
216218 try :
217- value .write_parquet (parquet_path )
219+ value .write_parquet (output_path )
218220 except Exception as exc :
219- raise PolarsTransformError (f'Unable to write Parquet file "{ parquet_path } ": { type (exc ).__name__ } : { exc } ' ) from exc
220- return SQLResult (status = f'Wrote { len (value )} rows to { parquet_path } .' )
221+ raise PolarsTransformError (f'Unable to write Parquet file "{ output_path } ": { type (exc ).__name__ } : { exc } ' ) from exc
222+ return SQLResult (status = f'Wrote { len (value )} rows to { output_path } .' )
221223 return SQLResult (header = list (value .columns ), rows = list (value .iter_rows ()))
222224 if isinstance (value , transform .polars .Series ):
223225 column_name = value .name or 'value'
224- if parquet_path is not None :
226+ if output_path is not None :
227+ if not output_path .lower ().endswith ('.parquet' ):
228+ raise PolarsTransformError ('Polars Series results can only be written to ".parquet" files.' )
225229 try :
226230 series_dataframe = value .rename (column_name ).to_frame ()
227- series_dataframe .write_parquet (parquet_path )
231+ series_dataframe .write_parquet (output_path )
228232 except Exception as exc :
229- raise PolarsTransformError (f'Unable to write Parquet file "{ parquet_path } ": { type (exc ).__name__ } : { exc } ' ) from exc
230- return SQLResult (status = f'Wrote { len (series_dataframe )} rows to { parquet_path } .' )
233+ raise PolarsTransformError (f'Unable to write Parquet file "{ output_path } ": { type (exc ).__name__ } : { exc } ' ) from exc
234+ return SQLResult (status = f'Wrote { len (series_dataframe )} rows to { output_path } .' )
231235 return SQLResult (header = [column_name ], rows = [(item ,) for item in value ])
232236 if transform .altair is not None and isinstance (value , transform .altair .TopLevelMixin ):
233- if parquet_path is not None :
234- raise PolarsTransformError ('Polars transforms must return a DataFrame or Series before writing Parquet output .' )
235- if image_protocol == 'none' :
237+ if output_path is not None and not output_path . lower (). endswith ( '.png' ) :
238+ raise PolarsTransformError ('Altair charts can only be written to ".png" files .' )
239+ if output_path is None and image_protocol == 'none' :
236240 return SQLResult (status = 'image_protocol is unset in ~/.myclirc. Inline plotting is disabled.' )
237241 _load_vl_convert ()
238- png = BytesIO ()
239242 try :
240243 transform .altair .theme .enable (plot_theme )
241244 except Exception as exc :
242245 raise PolarsTransformError (f'Unable to enable Altair plot theme "{ plot_theme } ": { type (exc ).__name__ } : { exc } ' ) from exc
246+ if output_path is not None :
247+ try :
248+ value .save (
249+ output_path ,
250+ format = 'png' ,
251+ scale_factor = plot_scale_factor ,
252+ ppi = plot_ppi ,
253+ )
254+ except Exception as exc :
255+ raise PolarsTransformError (f'Unable to write PNG file "{ output_path } ": { type (exc ).__name__ } : { exc } ' ) from exc
256+ return SQLResult (status = f'Wrote PNG image to { output_path } .' )
257+ png = BytesIO ()
243258 try :
244259 value .save (
245260 png ,
@@ -250,6 +265,8 @@ def run_polars_transform(
250265 except Exception as exc :
251266 raise PolarsTransformError (f'Unable to render Altair chart: { type (exc ).__name__ } : { exc } ' ) from exc
252267 return SQLResult (image = png .getvalue (), image_protocol = image_protocol )
253- if parquet_path is not None :
254- raise PolarsTransformError ('Polars transforms must return a DataFrame or Series before writing Parquet output.' )
268+ if output_path is not None :
269+ raise PolarsTransformError (
270+ 'Polars transforms must return a DataFrame or Series for Parquet output, or an Altair chart for PNG output.'
271+ )
255272 return SQLResult (status = f'Nothing could be displayed for return type: { type (value )} ' )
0 commit comments