flyteplugins-lance 2.5.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from flyteplugins.lance.df_transformer import register_lance_df_transformers
2
+
3
+ __all__ = ["register_lance_df_transformers"]
@@ -0,0 +1,239 @@
1
+ import functools
2
+ import typing
3
+ from pathlib import Path
4
+
5
+ import flyte.storage as storage
6
+ from flyte._utils import lazy_module
7
+ from flyte.io._dataframe.dataframe import DataFrame
8
+ from flyte.io.extend import (
9
+ DataFrameDecoder,
10
+ DataFrameEncoder,
11
+ DataFrameTransformerEngine,
12
+ )
13
+ from flyteidl2.core import literals_pb2, types_pb2
14
+
15
+ if typing.TYPE_CHECKING:
16
+ import pyarrow as pa
17
+
18
+ import lance
19
+ else:
20
+ lance = lazy_module("lance")
21
+ pa = lazy_module("pyarrow")
22
+
23
+ # Format string registered with the DataFrame type system. A DataFrame with this
24
+ # format is backed by a Lance dataset directory on (possibly remote) storage.
25
+ LANCE = "lance"
26
+
27
+
28
+ def get_lance_storage_options(protocol: typing.Optional[str]) -> typing.Dict[str, str]:
29
+ """
30
+ Build a flat string storage_options dict for Lance from Flyte's storage config.
31
+
32
+ Lance is backed by the same object_store crate as Polars, so the keys mirror the
33
+ Polars ones closely. The one difference for S3 is the endpoint key: Lance uses
34
+ "aws_endpoint" (Polars uses "aws_endpoint_url").
35
+ """
36
+ from flyte._initialize import get_storage
37
+ from flyte.errors import InitializationError
38
+
39
+ if not protocol:
40
+ return {}
41
+
42
+ try:
43
+ storage_config = get_storage()
44
+ except InitializationError:
45
+ storage_config = None
46
+
47
+ match protocol:
48
+ case "s3":
49
+ from flyte.storage import S3
50
+
51
+ if storage_config and isinstance(storage_config, S3):
52
+ s3_config = storage_config
53
+ else:
54
+ s3_config = S3.auto()
55
+
56
+ opts: typing.Dict[str, str] = {}
57
+ if s3_config.access_key_id:
58
+ opts["aws_access_key_id"] = s3_config.access_key_id
59
+ if s3_config.secret_access_key:
60
+ opts["aws_secret_access_key"] = s3_config.secret_access_key
61
+ if s3_config.region:
62
+ opts["aws_region"] = s3_config.region
63
+ if s3_config.endpoint:
64
+ opts["aws_endpoint"] = s3_config.endpoint
65
+ # Custom endpoints (e.g. a local MinIO) are usually plain HTTP; the
66
+ # object_store client refuses HTTP unless explicitly allowed.
67
+ if s3_config.endpoint.startswith("http://"):
68
+ opts["aws_allow_http"] = "true"
69
+ return opts
70
+
71
+ case "gs" | "gcs":
72
+ # GCS typically uses application default credentials, which the
73
+ # object_store client picks up automatically.
74
+ return {}
75
+
76
+ case "abfs" | "abfss":
77
+ from flyte.storage import ABFS
78
+
79
+ if storage_config and isinstance(storage_config, ABFS):
80
+ abfs_config = storage_config
81
+ else:
82
+ abfs_config = ABFS.auto()
83
+
84
+ opts = {}
85
+ if abfs_config.account_name:
86
+ opts["azure_storage_account_name"] = abfs_config.account_name
87
+ if abfs_config.account_key:
88
+ opts["azure_storage_account_key"] = abfs_config.account_key
89
+ if abfs_config.tenant_id:
90
+ opts["azure_storage_tenant_id"] = abfs_config.tenant_id
91
+ if abfs_config.client_id:
92
+ opts["azure_storage_client_id"] = abfs_config.client_id
93
+ if abfs_config.client_secret:
94
+ opts["azure_storage_client_secret"] = abfs_config.client_secret
95
+ return opts
96
+
97
+ case _:
98
+ return {}
99
+
100
+
101
+ def _resolve_write_uri(dataframe: DataFrame) -> str:
102
+ """Pick the URI to write to: an explicit one on the DataFrame, or a fresh
103
+ Flyte-managed path from the task's raw-data prefix (local temp or blob store)."""
104
+ if dataframe.uri:
105
+ return typing.cast(str, dataframe.uri)
106
+
107
+ from flyte._context import internal_ctx
108
+
109
+ return str(internal_ctx().raw_data.get_random_remote_path())
110
+
111
+
112
+ def _storage_options_for(uri: str) -> typing.Optional[typing.Dict[str, str]]:
113
+ """Storage options for a URI, or None for local paths (Lance wants None locally)."""
114
+ if not storage.is_remote(uri):
115
+ return None
116
+ filesystem = storage.get_underlying_filesystem(path=uri)
117
+ protocol = filesystem.protocol
118
+ if isinstance(protocol, (list, tuple)):
119
+ protocol = protocol[0]
120
+ return get_lance_storage_options(protocol=protocol) or None
121
+
122
+
123
+ class LanceToLanceDatasetDecodingHandler(DataFrameDecoder):
124
+ """Decode a "lance" DataFrame into a live lance.LanceDataset handle.
125
+
126
+ This is the streaming path: the dataset is opened lazily (manifest only) and
127
+ the caller streams from it via scanner()/take()/to_batches(). Column subsetting
128
+ is intentionally left to the caller's scanner(columns=...), which is more
129
+ flexible for streaming than eagerly subsetting here.
130
+ """
131
+
132
+ def __init__(self):
133
+ super().__init__(lance.LanceDataset, None, LANCE)
134
+
135
+ async def decode(
136
+ self,
137
+ flyte_value: literals_pb2.StructuredDataset,
138
+ current_task_metadata: literals_pb2.StructuredDatasetMetadata,
139
+ ) -> "lance.LanceDataset":
140
+ uri = flyte_value.uri
141
+ return lance.dataset(uri, storage_options=_storage_options_for(uri))
142
+
143
+
144
+ class LanceDatasetToLanceEncodingHandler(DataFrameEncoder):
145
+ """Encode a lance.LanceDataset by copying it into Flyte-managed storage.
146
+
147
+ The source dataset is streamed fragment-by-fragment via its RecordBatchReader,
148
+ so this does not materialize the whole dataset in memory.
149
+ """
150
+
151
+ def __init__(self):
152
+ super().__init__(lance.LanceDataset, None, LANCE)
153
+
154
+ async def encode(
155
+ self,
156
+ dataframe: DataFrame,
157
+ structured_dataset_type: types_pb2.StructuredDatasetType,
158
+ ) -> literals_pb2.StructuredDataset:
159
+ uri = _resolve_write_uri(dataframe)
160
+ if not storage.is_remote(uri):
161
+ Path(uri).parent.mkdir(parents=True, exist_ok=True)
162
+
163
+ source = typing.cast("lance.LanceDataset", dataframe.val)
164
+ reader = source.scanner().to_reader()
165
+ lance.write_dataset(reader, uri, storage_options=_storage_options_for(uri))
166
+
167
+ structured_dataset_type.format = LANCE
168
+ return literals_pb2.StructuredDataset(
169
+ uri=uri,
170
+ metadata=literals_pb2.StructuredDatasetMetadata(structured_dataset_type=structured_dataset_type),
171
+ )
172
+
173
+
174
+ class ArrowToLanceEncodingHandler(DataFrameEncoder):
175
+ """Encode an in-memory pyarrow.Table as a Lance dataset.
176
+
177
+ Not registered as the default for pyarrow.Table (that stays Parquet); opt in
178
+ with an explicit "lance" format, e.g. Annotated[DataFrame, "lance"].
179
+ """
180
+
181
+ def __init__(self):
182
+ super().__init__(pa.Table, None, LANCE)
183
+
184
+ async def encode(
185
+ self,
186
+ dataframe: DataFrame,
187
+ structured_dataset_type: types_pb2.StructuredDatasetType,
188
+ ) -> literals_pb2.StructuredDataset:
189
+ uri = _resolve_write_uri(dataframe)
190
+ if not storage.is_remote(uri):
191
+ Path(uri).parent.mkdir(parents=True, exist_ok=True)
192
+
193
+ table = typing.cast("pa.Table", dataframe.val)
194
+ lance.write_dataset(table, uri, storage_options=_storage_options_for(uri))
195
+
196
+ structured_dataset_type.format = LANCE
197
+ return literals_pb2.StructuredDataset(
198
+ uri=uri,
199
+ metadata=literals_pb2.StructuredDatasetMetadata(structured_dataset_type=structured_dataset_type),
200
+ )
201
+
202
+
203
+ class LanceToArrowDecodingHandler(DataFrameDecoder):
204
+ """Decode a "lance" DataFrame eagerly into a pyarrow.Table.
205
+
206
+ This materializes the whole dataset in memory. For large or multimodal datasets
207
+ (e.g. image bytes) decode to lance.LanceDataset and stream instead.
208
+ """
209
+
210
+ def __init__(self):
211
+ super().__init__(pa.Table, None, LANCE)
212
+
213
+ async def decode(
214
+ self,
215
+ flyte_value: literals_pb2.StructuredDataset,
216
+ current_task_metadata: literals_pb2.StructuredDatasetMetadata,
217
+ ) -> "pa.Table":
218
+ uri = flyte_value.uri
219
+ columns = None
220
+ if current_task_metadata.structured_dataset_type and current_task_metadata.structured_dataset_type.columns:
221
+ columns = [c.name for c in current_task_metadata.structured_dataset_type.columns]
222
+ return lance.dataset(uri, storage_options=_storage_options_for(uri)).to_table(columns=columns)
223
+
224
+
225
+ @functools.lru_cache(maxsize=None)
226
+ def register_lance_df_transformers():
227
+ """Register Lance DataFrame encoders and decoders with the DataFrameTransformerEngine.
228
+
229
+ This function is called automatically via the flyte.plugins.types entry point
230
+ when flyte.init() is called with load_plugin_type_transformers=True (the default).
231
+ """
232
+ DataFrameTransformerEngine.register(LanceDatasetToLanceEncodingHandler(), default_format_for_type=True)
233
+ DataFrameTransformerEngine.register(LanceToLanceDatasetDecodingHandler(), default_format_for_type=True)
234
+ DataFrameTransformerEngine.register(ArrowToLanceEncodingHandler())
235
+ DataFrameTransformerEngine.register(LanceToArrowDecodingHandler())
236
+
237
+
238
+ # Also register at module import time for backwards compatibility
239
+ register_lance_df_transformers()
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: flyteplugins-lance
3
+ Version: 2.5.15
4
+ Summary: Lance Plugin for Flyte
5
+ Author-email: Samhita Alla <samhita@union.ai>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: pylance
9
+ Requires-Dist: pyarrow
10
+ Requires-Dist: flyte
11
+
12
+ # Lance Plugin
13
+
14
+ This plugin adds a "lance" format to the Flyte DataFrame, so a Lance dataset can
15
+ be passed between tasks as a typed `flyte.io.DataFrame`.
16
+
17
+ Lance is a columnar, multimodal, streaming-optimized format. Its central property
18
+ is that a dataset is opened lazily and streamed on demand — sequentially for a
19
+ scan or by random access for shuffled training — without materializing the whole
20
+ thing in memory. This plugin preserves that: the primary decoder hands back a live
21
+ `lance.LanceDataset` handle you can stream from, not a materialized table.
22
+
23
+ The plugin registers:
24
+
25
+ - `lance.LanceDataset` as the default in-memory type for the "lance" format —
26
+ encoded by copying the dataset to Flyte-managed storage, decoded lazily via
27
+ `lance.dataset(uri)`. This is the streaming path.
28
+ - `pyarrow.Table` for the "lance" format, for handing off an in-memory table.
29
+ Because `pyarrow.Table` already defaults to Parquet, this is the one case where
30
+ you opt into Lance explicitly, with `Annotated[DataFrame, "lance"]`. Encoded with
31
+ `lance.write_dataset` and decoded eagerly with `dataset.to_table()`, which
32
+ materializes the whole dataset — prefer `lance.LanceDataset` for large or
33
+ multimodal data.
34
+
35
+ Object-store credentials are threaded through Lance's `storage_options` from
36
+ Flyte's storage configuration, so remote reads and writes go through the same
37
+ credentials as the rest of Flyte.
38
+
39
+ To install the plugin, run the following command:
40
+
41
+ ```bash
42
+ pip install flyteplugins-lance
43
+ ```
44
+
45
+ Usage:
46
+
47
+ ```python
48
+ import tempfile
49
+
50
+ import flyte
51
+ import lance
52
+ import pyarrow as pa
53
+
54
+ # Installing the plugin in the task image is all that is needed. Flyte discovers
55
+ # it through the flyte.plugins.types entry point and registers the "lance" format
56
+ # automatically, so there is nothing to import in your task code.
57
+ env = flyte.TaskEnvironment(
58
+ name="lance-example",
59
+ image=flyte.Image.from_debian_base().with_pip_packages("flyteplugins-lance"),
60
+ )
61
+
62
+
63
+ @env.task
64
+ async def make() -> lance.LanceDataset:
65
+ uri = f"{tempfile.mkdtemp()}/example.lance"
66
+ lance.write_dataset(pa.table({"id": [1, 2, 3]}), uri)
67
+ return lance.dataset(uri) # encoded as "lance" — the default format for a LanceDataset
68
+
69
+
70
+ @env.task
71
+ async def consume(ds: lance.LanceDataset) -> int:
72
+ return ds.count_rows() # a live, streaming handle — no wrapper, no .open()
73
+
74
+
75
+ @env.task
76
+ async def main() -> int:
77
+ return await consume(await make())
78
+ ```
@@ -0,0 +1,7 @@
1
+ flyteplugins/lance/__init__.py,sha256=2ZUkBBLhiz82qMldYo3cQlrgk2Mr801RXo1ck4tVq2w,123
2
+ flyteplugins/lance/df_transformer.py,sha256=GdH3xmWT2QLSOSjVAOMwasZqMz8PLCZ1CsrfCSm_Tvc,8977
3
+ flyteplugins_lance-2.5.15.dist-info/METADATA,sha256=gv52lMBAXVrBou7dniJTtGLmR3XM1jXvWxZ9mDGCeuk,2697
4
+ flyteplugins_lance-2.5.15.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
5
+ flyteplugins_lance-2.5.15.dist-info/entry_points.txt,sha256=jhQy7HRvLrTyKds7iW0Ax_hHqamPyZTgN72nj4FMUNg,95
6
+ flyteplugins_lance-2.5.15.dist-info/top_level.txt,sha256=cgd779rPu9EsvdtuYgUxNHHgElaQvPn74KhB5XSeMBE,13
7
+ flyteplugins_lance-2.5.15.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [flyte.plugins.types]
2
+ lance = flyteplugins.lance.df_transformer:register_lance_df_transformers
@@ -0,0 +1 @@
1
+ flyteplugins