Skip to content

Commit 72b23cb

Browse files
Fixed _info bug, added async streaming functionality (#113)
1 parent 364eece commit 72b23cb

13 files changed

Lines changed: 593 additions & 447 deletions

Cargo.lock

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pyfusion"
3-
version = "2.0.10"
3+
version = "2.0.11-dev0"
44
edition = "2021"
55

66

@@ -27,6 +27,17 @@ jsonwebtoken = {version = "9.3.0"}
2727
state = { version = "0.6"}
2828

2929

30+
# Set ceiling on Windows crates due to auth issue on some Windows environments
31+
[target.'cfg(target_arch = "windows")'.dependencies]
32+
windows-targets = { version = "<=0.48.5" }
33+
windows-registry = { version = "<=0.2.0" }
34+
windows-strings = { version = "<=0.1.0" }
35+
windows-link = { version = "<=0.1.0" }
36+
# wasi = { version = "<= 0.13.3+wasi-0.2.2" }
37+
# tokio-util = { version = "<= 0.7.13" }
38+
# End of WINDOWS limit
39+
40+
3041
# Struggling to get OPEN_SSL env vars to point to the installed libs on these archs
3142
# Remove these once we figure out the magic
3243
[target.'cfg(target_arch = "aarch64")'.dependencies]
@@ -40,6 +51,7 @@ openssl = { version = "0.10.64", features = ["vendored"] }
4051
[package.metadata.cargo-llvm-cov]
4152
# End OPEN_SSL
4253

54+
4355
# Ignore directories and files that are not part of the Rust source code
4456
ignore-filename-regex = "(.cargo|py_integ|py_src|py_tests|dist|docs|downloads|runnable|site|target|\\.git|\\.github)/"
4557
ignore-glob = ["py_integ/*", "py_src/*", "py_tests/*", "dist/*", "docs/*", "downloads/*", "runnable/*", "site/*", "target/*"]

docs/fusion_filesystem.ipynb

Lines changed: 448 additions & 387 deletions
Large diffs are not rendered by default.

py_src/fusion/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
__author__ = """Fusion Devs"""
44
__email__ = "fusion_developers@jpmorgan.com"
5-
__version__ = "2.0.10"
5+
__version__ = "2.0.11-dev0"
66

77
from fusion._fusion import FusionCredentials
88
from fusion.fs_sync import fsync

py_src/fusion/embeddings.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ class FusionEmbeddingsConnection(Connection): # type: ignore
9191
credentials (FusionCredentials or str, optional): Credentials for the Fusion API. Can be a `FusionCredentials`
9292
object or a path to a credentials file. Defaults to "config/client_credentials.json".
9393
catalog (str, optional): Catalog name. Defaults to "common".
94-
knowledge_base (str | list[str], optional): Knowledge base name. A dataset identifier. If multiple identifiers
94+
knowledge_base (str | list[str], optional): Knowledge base name. A dataset identifier. If multiple identifiers
9595
are provided, the connection will perform searches across all provided knowledge bases. Any other operations
9696
will not be supported, as a single knowledge base is required for those operations.
9797
index (str, optional): Index name. Defaults to `None`. Used to determine index when the _bulk operation
@@ -200,7 +200,6 @@ def _tidy_url(self, url: str) -> str:
200200
def _remap_endpoints(url: str) -> str:
201201
return url.replace("_bulk", "embeddings").replace("_search", "search")
202202

203-
204203
def _make_url_valid(self, url: str, body: bytes | None = None) -> str:
205204
if url == "/_bulk":
206205
index_name = self.index_name if self.index_name else _retrieve_index_name_from_bulk_body(body)
@@ -400,7 +399,7 @@ def __init__( # noqa: PLR0912, PLR0913, PLR0915
400399
credentials (FusionCredentials or str, optional): Credentials for the Fusion API. Can be a `FusionCredentials`
401400
object or a path to a credentials file. Defaults to "config/client_credentials.json".
402401
catalog (str, optional): Catalog name. Defaults to "common".
403-
knowledge_base (str, optional): Knowledge base name. A dataset identifier. If multiple identifiers
402+
knowledge_base (str, optional): Knowledge base name. A dataset identifier. If multiple identifiers
404403
are provided, the connection will perform searches across all provided knowledge bases. Any other operations
405404
will not be supported, as a single knowledge base is required for those operations.
406405
index (str, optional): Index name. Defaults to `None`. Used to determine index when the _bulk operation

py_src/fusion/embeddings_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
logger = logging.getLogger(__name__)
1515

16+
1617
def _format_full_index_response(response: requests.Response) -> pd.DataFrame:
1718
"""Format get index response.
1819
@@ -77,7 +78,7 @@ def _retrieve_index_name_from_bulk_body(body: bytes | None) -> str:
7778
if "index" in json_obj and "_index" in json_obj["index"]:
7879
index_name = str(json_obj["index"]["_index"])
7980
return index_name
80-
81+
8182
raise ValueError("Index name not found in bulk body")
8283

8384

@@ -115,6 +116,7 @@ def _modify_post_response_langchain(raw_data: str | bytes | bytearray) -> str |
115116

116117
return raw_data
117118

119+
118120
def _modify_post_haystack(knowledge_base: str | list[str] | None, body: bytes | None, method: str) -> bytes | None:
119121
"""Method to modify haystack POST body to match the embeddings API, which expects the embedding field to be
120122
named "vector".

py_src/fusion/fusion.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import copy
56
import json as js
67
import logging
78
import re
@@ -53,6 +54,8 @@
5354
)
5455

5556
if TYPE_CHECKING:
57+
from collections.abc import AsyncGenerator
58+
5659
import fsspec
5760
import requests
5861
from opensearchpy import AsyncOpenSearch, OpenSearch
@@ -770,6 +773,46 @@ def download( # noqa: PLR0912, PLR0913
770773
warnings.warn(f"The download of {r[1]} was not successful", stacklevel=2)
771774
return res if return_paths else None
772775

776+
async def _async_stream_file(self, url: str, chunk_size: int = 100) -> AsyncGenerator[bytes, None]:
777+
"""Return async stream of a file fro mthe given url.
778+
779+
Args:
780+
url (str): File url. Appends Fusion.root_url if http prefix not present.
781+
chunk_size (int, optional): Size for each chunk in async stream. Defaults to 100.
782+
783+
Returns:
784+
AsyncGenerator[bytes, None]: Async generator object.
785+
786+
Yields:
787+
Iterator[AsyncGenerator[bytes, None]]: Next set of bytes read from the file at given url.
788+
"""
789+
dup_credentials = copy.deepcopy(self.credentials)
790+
async_fs = FusionHTTPFileSystem(
791+
client_kwargs={"root_url": self.root_url, "credentials": dup_credentials}, asynchronous=True
792+
)
793+
session = await async_fs.set_session()
794+
async with session:
795+
async for chunk in async_fs._stream_file(url, chunk_size):
796+
yield chunk
797+
798+
async def _async_get_file(self, url: str, chunk_size: int = 1000) -> bytes:
799+
"""Return a file from url as a bytes object, asynchronously.
800+
801+
Under the hood, opens up an async stream downloading file in chunk_size bytes per chunk.
802+
Larger chunk sizes results in shorter execution time for this function.
803+
804+
Args:
805+
url (str): File url. Appends Fusion.root_url if http prefix not present.
806+
chunk_size (int, optional): Size of chunks to get from async stream. Defaults to 1000.
807+
808+
Returns:
809+
bytes: File from url as a bytes object.
810+
"""
811+
async_generator = self._async_stream_file(url, chunk_size)
812+
bytes_list: list[bytes] = [chunk async for chunk in async_generator]
813+
final_bytes: bytes = b"".join(bytes_list)
814+
return final_bytes
815+
773816
def to_df( # noqa: PLR0913
774817
self,
775818
dataset: str,
@@ -2517,7 +2560,7 @@ def get_async_fusion_vector_store_client(self, knowledge_base: str, catalog: str
25172560
root_url=self.root_url,
25182561
credentials=self.credentials,
25192562
)
2520-
2563+
25212564
def list_datasetmembers_distributions(
25222565
self,
25232566
dataset: str,

py_src/fusion/fusion_filesystem.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ def info(self, path: str, **kwargs: Any) -> Any:
207207
kwargs["keep_protocol"] = True
208208
res = super().ls(path, detail=True, **kwargs)
209209
if res[0]["type"] != "file":
210+
kwargs.pop("keep_protocol", None)
210211
res = super().info(path, **kwargs)
211212
if path.split("/")[-2] == "datasets":
212213
target = path.split("/")[-1]
@@ -220,15 +221,20 @@ def info(self, path: str, **kwargs: Any) -> Any:
220221

221222
async def _info(self, path: str, **kwargs: Any) -> Any:
222223
await self._async_startup()
223-
path = await self._decorate_url_a(path)
224+
path = self._decorate_url(path)
224225
kwargs["keep_protocol"] = True
225226
res = await super()._ls(path, detail=True, **kwargs)
226227
if res[0]["type"] != "file":
228+
kwargs.pop("keep_protocol", None)
227229
res = await super()._info(path, **kwargs)
228230
if path.split("/")[-2] == "datasets":
229231
target = path.split("/")[-1]
230232
args = ["/".join(path.split("/")[:-1]) + f"/changes?datasets={quote(target)}"]
231233
res["changes"] = await self._changes(*args)
234+
if res["size"] is None and (res["mimetype"] == "application/json") and (res["type"] == "file"):
235+
res["size"] = 0
236+
res.pop("mimetype", None)
237+
res["type"] = "directory"
232238
split_path = path.split("/")
233239
if len(split_path) > 1 and split_path[-2] == "distributions":
234240
res = res[0]
@@ -325,6 +331,29 @@ async def _cat(self, url: str, start: Optional[int] = None, end: Optional[int] =
325331
out = await super()._cat(url, start=start, end=end, **kwargs)
326332
return out
327333

334+
async def _stream_file(self, url: str, chunk_size: int = 100) -> AsyncGenerator[bytes, None]:
335+
"""Return an async stream to file at the given url.
336+
337+
Args:
338+
url (str): File url. Appends Fusion.root_url if http prefix not present.
339+
chunk_size (int, optional): Size for each chunk in async stream. Defaults to 100.
340+
341+
Returns:
342+
AsyncGenerator[bytes, None]: Async generator object.
343+
344+
Yields:
345+
Iterator[AsyncGenerator[bytes, None]]: Next set of bytes read from the file at given url.
346+
"""
347+
await self._async_startup()
348+
url = self._decorate_url(url)
349+
f = await self.open_async(url, "rb")
350+
async with f:
351+
while True:
352+
chunk = await f.read(chunk_size)
353+
if not chunk:
354+
break
355+
yield chunk
356+
328357
async def _fetch_range(
329358
self,
330359
session: aiohttp.ClientSession,

0 commit comments

Comments
 (0)