Skip to content

Commit 3febdce

Browse files
committed
feat(protocol): set up dataframe interchange scaffolding and add initial XarrayBuffer
Set up the dataframe interchange protocol scaffolding based on earlier guidance and implemented the initial XarrayBuffer component. Introduced a dedicated xarray_sql.dataframe_protocol subpackage so protocol-specific logic has its own organized namespace rather than crowding existing modules. Open to restructuring this if you prefer the classes to live directly under the main package. Currently placed both the abstract Buffer interface and the concrete XarrayBuffer implementation in core.py; happy to split these into separate files if that aligns better with the project structure. Once the layout is settled, I’ll proceed with implementing the remaining two protocol classes as discussed.
1 parent 599f86f commit 3febdce

3 files changed

Lines changed: 250 additions & 0 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""DataFrame interchange protocol support for xarray SQL integration.
2+
3+
This package exposes helpers to register a Dataset accessor providing
4+
`__dataframe__` so we can interoperate with other dataframe libraries.
5+
6+
The actual protocol implementation lives in `core.py`, while
7+
`accessor.py` exposes user-facing registration utilities.
8+
"""
9+
10+
from .accessor import register_dataset_dataframe_accessor
11+
12+
__all__ = ["register_dataset_dataframe_accessor"]
13+
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Dataset accessor for the DataFrame interchange protocol."""
2+
3+
from __future__ import annotations
4+
5+
import typing as t
6+
7+
import xarray as xr
8+
9+
from . import core
10+
11+
12+
AccessorReturn = t.TypeVar("AccessorReturn")
13+
14+
15+
@xr.register_dataset_accessor("df_protocol")
16+
class DataFrameProtocolAccessor:
17+
"""Access DataFrame interchange utilities from an xarray Dataset."""
18+
19+
def __init__(self, dataset: xr.Dataset) -> None:
20+
self._dataset = dataset
21+
22+
def __dataframe__(self, allow_copy: bool = False):
23+
"""Expose the dataset via the DataFrame interchange protocol."""
24+
return core.dataset_to_protocol(self._dataset, allow_copy=allow_copy)
25+
26+
27+
def register_dataset_dataframe_accessor() -> None:
28+
"""Ensure the dataset accessor is registered (import side-effect)."""
29+
# Accessing the class ensures module import registers the accessor.
30+
_ = DataFrameProtocolAccessor
31+
32+
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"""Core DataFrame interchange protocol objects for xarray datasets."""
2+
3+
from __future__ import annotations
4+
5+
import enum
6+
import typing as t
7+
from abc import ABC, abstractmethod
8+
9+
import numpy as np
10+
import xarray as xr
11+
12+
13+
class DlpackDeviceType(enum.IntEnum):
14+
"""Integer enum for device type codes matching DLPack."""
15+
16+
CPU = 1
17+
CUDA = 2
18+
CPU_PINNED = 3
19+
OPENCL = 4
20+
VULKAN = 7
21+
METAL = 8
22+
VPI = 9
23+
ROCM = 10
24+
25+
26+
class DtypeKind(enum.IntEnum):
27+
"""
28+
Integer enum for data types.
29+
30+
Attributes
31+
----------
32+
INT : int
33+
Matches to signed integer data type.
34+
UINT : int
35+
Matches to unsigned integer data type.
36+
FLOAT : int
37+
Matches to floating point data type.
38+
BOOL : int
39+
Matches to boolean data type.
40+
STRING : int
41+
Matches to string data type (UTF-8 encoded).
42+
DATETIME : int
43+
Matches to datetime data type.
44+
CATEGORICAL : int
45+
Matches to categorical data type.
46+
"""
47+
48+
INT = 0
49+
UINT = 1
50+
FLOAT = 2
51+
BOOL = 20
52+
STRING = 21 # UTF-8
53+
DATETIME = 22
54+
CATEGORICAL = 23
55+
56+
57+
Dtype = t.Tuple["DtypeKind", int, str, str] # see Column.dtype
58+
59+
60+
class ColumnNullType(enum.IntEnum):
61+
"""
62+
Integer enum for null type representation.
63+
64+
Attributes
65+
----------
66+
NON_NULLABLE : int
67+
Non-nullable column.
68+
USE_NAN : int
69+
Use explicit float NaN value.
70+
USE_SENTINEL : int
71+
Sentinel value besides NaN.
72+
USE_BITMASK : int
73+
The bit is set/unset representing a null on a certain position.
74+
USE_BYTEMASK : int
75+
The byte is set/unset representing a null on a certain position.
76+
"""
77+
78+
NON_NULLABLE = 0
79+
USE_NAN = 1
80+
USE_SENTINEL = 2
81+
USE_BITMASK = 3
82+
USE_BYTEMASK = 4
83+
84+
85+
class ColumnBuffers(t.TypedDict):
86+
# first element is a buffer containing the column data;
87+
# second element is the data buffer's associated dtype
88+
data: t.Tuple["Buffer", Dtype]
89+
90+
# first element is a buffer containing mask values indicating missing data;
91+
# second element is the mask value buffer's associated dtype.
92+
# None if the null representation is not a bit or byte mask
93+
validity: t.Optional[t.Tuple["Buffer", Dtype]]
94+
95+
# first element is a buffer containing the offset values for
96+
# variable-size binary data (e.g., variable-length strings);
97+
# second element is the offsets buffer's associated dtype.
98+
# None if the data buffer does not have an associated offsets buffer
99+
offsets: t.Optional[t.Tuple["Buffer", Dtype]]
100+
101+
102+
class CategoricalDescription(t.TypedDict):
103+
# whether the ordering of dictionary indices is semantically meaningful
104+
is_ordered: bool
105+
# whether a dictionary-style mapping of categorical values to other objects exists
106+
is_dictionary: bool
107+
# Python-level only (e.g. ``{int: str}``).
108+
# None if not a dictionary-style categorical.
109+
categories: t.Optional["Column"]
110+
111+
112+
class Buffer(ABC):
113+
"""
114+
Data in the buffer is guaranteed to be contiguous in memory.
115+
116+
Note that there is no dtype attribute present, a buffer can be thought of
117+
as simply a block of memory. However, if the column that the buffer is
118+
attached to has a dtype that's supported by DLPack and ``__dlpack__`` is
119+
implemented, then that dtype information will be contained in the return
120+
value from ``__dlpack__``.
121+
122+
This distinction is useful to support both data exchange via DLPack on a
123+
buffer and (b) dtypes like variable-length strings which do not have a
124+
fixed number of bytes per element.
125+
"""
126+
127+
@property
128+
@abstractmethod
129+
def bufsize(self) -> int:
130+
"""
131+
Buffer size in bytes.
132+
"""
133+
...
134+
135+
@property
136+
@abstractmethod
137+
def ptr(self) -> int:
138+
"""
139+
Pointer to start of the buffer as an integer.
140+
"""
141+
...
142+
143+
@abstractmethod
144+
def __dlpack__(self):
145+
"""
146+
Produce DLPack capsule (see array API standard).
147+
148+
Raises:
149+
150+
- TypeError : if the buffer contains unsupported dtypes.
151+
- NotImplementedError : if DLPack support is not implemented
152+
153+
Useful to have to connect to array libraries. Support optional because
154+
it's not completely trivial to implement for a Python-only library.
155+
"""
156+
raise NotImplementedError("__dlpack__")
157+
158+
@abstractmethod
159+
def __dlpack_device__(self) -> t.Tuple[DlpackDeviceType, t.Optional[int]]:
160+
"""
161+
Device type and device ID for where the data in the buffer resides.
162+
Uses device type codes matching DLPack.
163+
Note: must be implemented even if ``__dlpack__`` is not.
164+
"""
165+
...
166+
167+
168+
class XarrayBuffer(Buffer):
169+
"""Buffer implementation wrapping a NumPy ndarray without copying."""
170+
171+
def __init__(self, array: np.ndarray) -> None:
172+
if not array.flags["C_CONTIGUOUS"]:
173+
raise ValueError(
174+
"Dataset backing array must be C-contiguous for zero-copy exchange."
175+
)
176+
self._array = array
177+
178+
@property
179+
def bufsize(self) -> int:
180+
return int(self._array.nbytes)
181+
182+
@property
183+
def ptr(self) -> int:
184+
return int(self._array.__array_interface__["data"][0])
185+
186+
def __dlpack__(self):
187+
if hasattr(self._array, "__dlpack__"):
188+
return self._array.__dlpack__()
189+
raise NotImplementedError("__dlpack__")
190+
191+
def __dlpack_device__(self) -> t.Tuple[DlpackDeviceType, t.Optional[int]]:
192+
return (DlpackDeviceType.CPU, 0)
193+
194+
195+
196+
def dataset_to_protocol(
197+
dataset: xr.Dataset, *, allow_copy: bool
198+
) -> t.Any: # pragma: no cover - placeholder
199+
"""Return a DataFrame Interchange protocol object for the dataset.
200+
201+
This is a stub that will be expanded with the actual implementation.
202+
"""
203+
raise NotImplementedError
204+
205+

0 commit comments

Comments
 (0)