Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
4868db9
feat: polars functionality
stewjb Mar 24, 2026
82492e5
linting and testing
stewjb Mar 24, 2026
0030291
Fix Polars edge cases: Categorical/Enum, UInt64 overflow, tz-aware Da…
stewjb Mar 24, 2026
cef9107
Add polars to CI test requirements and document in README
stewjb Mar 24, 2026
1bd2198
Harden Polars support: validation, warnings, and edge case tests
stewjb Mar 24, 2026
6761de0
Handle Polars Null dtype on export
stewjb Mar 24, 2026
441cddb
Fix mypy error for polars import in test file
stewjb Mar 24, 2026
a0a86ce
Add reviewer-facing comments to Polars implementation
stewjb Mar 24, 2026
bf8e984
Remove set_at_idx fallback; scatter() is available in all supported P…
stewjb Mar 24, 2026
00d81cf
Address Copilot review comments
stewjb Mar 24, 2026
79d62d1
Fix dict-of-lists export bug and O(n²) iterable export loop
stewjb Mar 25, 2026
aeae3ab
Build nullable integer columns with mask in one shot on import
stewjb Mar 25, 2026
392532f
Merge branch 'main' of github.com:spotfiresoftware/spotfire-python
stewjb Mar 25, 2026
d1955df
Address review: metadata warnings, descriptive errors, and 1-copy dat…
stewjb Apr 3, 2026
392d181
Add arithmetic correctness test for Polars date epoch conversion
stewjb Apr 3, 2026
93f0b0b
Fix Polars temporal import to be genuinely zero-copy
stewjb Apr 3, 2026
a20782b
Perf: convert Date ms→days as int32 at C level (1 copy instead of 2)
stewjb Apr 3, 2026
5a90fe7
Perf: zero-copy Polars export for temporal/numeric types
stewjb Apr 3, 2026
e285cc3
Fix: remove illegal implementation signature from sbdf.pyi stub
stewjb Apr 3, 2026
71fd4c3
Fix: zero null positions before pl.Series(pl.Time) construction on im…
stewjb Apr 3, 2026
0414457
Fix: add polars to mypy ignore_missing_imports overrides
stewjb Apr 3, 2026
4bc8639
Docs: update README to show OutputFormat enum for import_data
stewjb Apr 3, 2026
e5893e7
Docs: add concrete import_data example with OutputFormat enum
stewjb Apr 3, 2026
39e01e2
Remove string-literal fallback from import_data output_format
stewjb Apr 3, 2026
17277c8
Fix pre-existing mypy errors in data_function.py and test_sbdf.py
stewjb Apr 4, 2026
e7cb389
Merge branch 'perf/fix-export-bugs'
stewjb Apr 4, 2026
a2e78bc
Perf: export Polars String columns directly from Arrow buffers
stewjb Apr 4, 2026
ffa1e7f
Fix: fall back to to_numpy() path when pyarrow is not installed
stewjb Apr 4, 2026
a62b882
Fix: wrap long type: ignore lines in test_sbdf.py to stay under 120 c…
stewjb Apr 4, 2026
503d08a
Fix pycodestyle violations flagged by cython-lint CI
stewjb Apr 4, 2026
c210ffa
Fix temporal export with nulls; add temporal_nulls and binary benchma…
stewjb Apr 4, 2026
c52db09
Fix: remove unused cdef declarations flagged by cython-lint
stewjb Apr 4, 2026
651df7e
Merge pull request #1 from stewjb/perf/polars-string-arrow-export
stewjb Apr 4, 2026
36621bd
Remove benchmark.py from repository
stewjb Apr 4, 2026
596cfd6
Perf: pass ndarray directly to scatter() instead of converting to list
stewjb Apr 4, 2026
7efcdc0
Test: add test_polars_string_multichunk to verify Arrow buffer chunk-…
stewjb Apr 4, 2026
53cddb5
CI: add no_polars test environment to verify package works without po…
stewjb Apr 4, 2026
fb65489
Test: cross-path equivalence for all dtypes with scattered nulls
stewjb Apr 5, 2026
b873848
Fix: shorten over-long test method names and rename 2-char variable f…
stewjb Apr 5, 2026
c91fd1a
Fix: add type: ignore[call-overload] for pd.array timedelta64 mypy ov…
stewjb Apr 5, 2026
fb3760b
Fix: move type: ignore comment to continuation line to fix line-too-l…
stewjb Apr 5, 2026
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ __pycache__/

# virtual environments
/venv/
/.venv/

# uv lock file (this is a library; lock files are for applications)
/uv.lock

# Claude Code
/.claude

# PyCharm project files
/.idea
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,22 @@ simply `spotfire`) to include the required Python packages to support optional f
| `spotfire[plot-matplotlib]` | Plotting support using just `matplotlib` |
| `spotfire[plot-pil]` | Plotting support using just `Pillow` |
| `spotfire[plot-seaborn]` | Plotting support using just `seaborn` |
| `spotfire[polars]` | Polars DataFrame support |
| `spotfire[dev,lint]` | Internal development |

Once installed, `export_data()` accepts `polars.DataFrame` and `polars.Series` directly, and
`import_data()` can return a `polars.DataFrame`:

```python
import spotfire.sbdf as sbdf

df = sbdf.import_data("data.sbdf", output_format=sbdf.OutputFormat.POLARS)
```

> **Note for Spotfire data functions:** Spotfire's bundled Python interpreter does not include
> Polars. To use Polars inside a data function, configure Spotfire to use a custom Python
> environment that has `polars` installed. Polars is a large binary package (~44 MB), so
> Spotfire Packages (SPKs) that bundle it will be significantly larger than typical packages.

### License
BSD-type 3-Clause License. See the file ```LICENSE``` included in the package.
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,13 @@ plot-seaborn = [
"seaborn >= 0.13.2",
]
plot = [ "spotfire[plot-matplotlib,plot-pil,plot-seaborn]" ]
# Polars support
polars = [
"polars >= 0.20.0",
]
# Development requirements
dev = [
"spotfire[geo,plot]",
"spotfire[geo,plot,polars]",
"Cython >= 3.0.4",
"html-testRunner",
]
Expand Down Expand Up @@ -283,5 +287,6 @@ plugins = ["numpy.typing.mypy_plugin"]
module = [
"geopandas",
"HtmlTestRunner",
"polars",
]
ignore_missing_imports = true
1 change: 1 addition & 0 deletions spotfire/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
"""User visible utility functions."""

from spotfire.public import copy_metadata, get_spotfire_types, set_spotfire_types, set_geocoding_table
from spotfire.sbdf import OutputFormat
12 changes: 6 additions & 6 deletions spotfire/data_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,19 +165,19 @@ def read(self, globals_dict: _Globals, debug_fn: _LogFunction) -> None:

# Argument type
if self._type == "column":
dataframe = dataframe[dataframe.columns[0]]
dataframe = dataframe[dataframe.columns[0]] # type: ignore[assignment]
if self._type == "value":
value = dataframe.at[0, dataframe.columns[0]]
if type(value).__module__ == "numpy":
dataframe = value.tolist()
dataframe = value.tolist() # type: ignore[assignment, union-attr]
elif type(value).__module__ == "pandas._libs.tslibs.timedeltas":
dataframe = value.to_pytimedelta()
dataframe = value.to_pytimedelta() # type: ignore[assignment, union-attr]
elif type(value).__module__ == "pandas._libs.tslibs.timestamps":
dataframe = value.to_pydatetime()
dataframe = value.to_pydatetime() # type: ignore[assignment, union-attr]
elif type(value).__module__ == "pandas._libs.tslibs.nattype":
dataframe = None
dataframe = None # type: ignore[assignment]
else:
dataframe = value
dataframe = value # type: ignore[assignment]

# Store to global dict
globals_dict[self._name] = dataframe
Expand Down
16 changes: 16 additions & 0 deletions spotfire/public.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@

_ColumnTypes = dict[str, str]

_POLARS_METADATA_ERROR = (
"Polars DataFrames do not support Spotfire metadata; "
"see https://github.com/pola-rs/polars/issues/5117"
)


def _is_polars_type(obj) -> bool:
"""Return True if obj is a Polars DataFrame or Series."""
return type(obj).__module__.startswith("polars")


# Table and column metadata functions

Expand All @@ -28,6 +38,8 @@ def copy_metadata(source, destination) -> None:
:param destination: the DataFrame or Series to copy metadata to
:raise TypeError: if the types of source and destination do not match
"""
if _is_polars_type(source) or _is_polars_type(destination):
raise TypeError(_POLARS_METADATA_ERROR)
# Verify that types of source and destination match
if isinstance(source, pd.DataFrame) and not isinstance(destination, pd.DataFrame):
raise TypeError("both source and destination must be DataFrames")
Expand Down Expand Up @@ -65,6 +77,8 @@ def get_spotfire_types(dataframe: pd.DataFrame) -> pd.Series:
:param dataframe: the DataFrame to get the Spotfire types of
:returns: a Series containing the Spotfire types of each column of dataframe
"""
if _is_polars_type(dataframe):
raise TypeError(_POLARS_METADATA_ERROR)
if not isinstance(dataframe, pd.DataFrame):
raise TypeError("dataframe is not a DataFrame")
spotfire_types = {}
Expand All @@ -83,6 +97,8 @@ def set_spotfire_types(dataframe: pd.DataFrame, column_types: _ColumnTypes) -> N
:param dataframe: the DataFrame to set the Spotfire types of
:param column_types: dictionary that maps column names to column types
"""
if _is_polars_type(dataframe):
raise TypeError(_POLARS_METADATA_ERROR)
if not isinstance(dataframe, pd.DataFrame):
raise TypeError("dataframe is not a DataFrame")
for col, spotfire_type in column_types.items():
Expand Down
14 changes: 13 additions & 1 deletion spotfire/sbdf.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,29 @@
# This file is subject to the license terms contained
# in the license file that is distributed with this file.

import enum
import typing

import pandas as pd

if typing.TYPE_CHECKING:
import polars as pl


_FilenameLike = typing.Union[str, bytes, int]

class SBDFError(Exception): ...
class SBDFWarning(Warning): ...

class OutputFormat(enum.Enum):
"""Supported output formats for :func:`import_data`."""
PANDAS: str
POLARS: str

def spotfire_typename_to_valuetype_id(typename: str) -> typing.Optional[int]: ...
def import_data(sbdf_file: _FilenameLike): ...
@typing.overload
def import_data(sbdf_file: _FilenameLike, output_format: typing.Literal[OutputFormat.PANDAS] = ...) -> pd.DataFrame: ...
@typing.overload
def import_data(sbdf_file: _FilenameLike, output_format: typing.Literal[OutputFormat.POLARS]) -> "pl.DataFrame": ...
def export_data(obj: typing.Any, sbdf_file: _FilenameLike, default_column_name: str = "x",
rows_per_slice: int = 0, encoding_rle: bool = True) -> None: ...
Loading