Skip to content

Commit b443a17

Browse files
authored
Add DataFrameFileToDocument (#19)
1 parent f9d8b65 commit b443a17

14 files changed

Lines changed: 1091 additions & 195 deletions

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@ The dataframe libraries currently supported are:
2222
- [Polars](https://pola.rs)
2323

2424
The library offers various custom [Converters](https://docs.haystack.deepset.ai/docs/converters) components to transform dataframes into Haystack [`Document`](https://docs.haystack.deepset.ai/docs/data-classes#document) objects:
25+
- `DataFrameFileToDocument` is a main generic converter that reads files using a dataframe backend and converts them into `Document` objects.
2526
- `FileToPandasDataFrame` and `FileToPolarsDataFrame` read files and convert them into dataframes.
2627
- `PandasDataFrameConverter` or `PolarsDataFrameConverter` convert data stored in dataframes into Haystack `Document`objects.
2728

29+
`dataframes-haystack` supports reading files in various formats:
30+
- _csv_, _json_, _parquet_, _excel_, _html_, _xml_, _orc_, _pickle_, _fixed-width format_ for `pandas`. See the [pandas documentation](https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html) for more details.
31+
- _csv_, _json_, _parquet_, _excel_, _avro_, _delta_, _ipc_ for `polars`. See the [polars documentation](https://docs.pola.rs/api/python/stable/reference/io.html) for more details.
32+
2833
## 🛠️ Installation
2934

3035
```sh
@@ -40,8 +45,31 @@ pip install "dataframes-haystack[polars]"
4045
> [!TIP]
4146
> See the [Example Notebooks](./notebooks) for complete examples.
4247
48+
## DataFrameFileToDocument
49+
50+
[Complete example](https://github.com/EdAbati/dataframes-haystack/blob/main/notebooks/dataframe-file-to-doc-example.ipynb)
51+
52+
You can leverage both `pandas` and `polars` backends (thanks to [`narwhals`](https://github.com/narwhals-dev/narwhals)) to read your data!
53+
54+
```python
55+
from dataframes_haystack.components.converters import DataFrameFileToDocument
56+
57+
converter = DataFrameFileToDocument(content_column="text_str")
58+
documents = converter.run(files=["file1.csv", "file2.csv"])
59+
```
60+
61+
```python
62+
>>> documents
63+
{'documents': [
64+
Document(id=0, content: 'Hello world', meta: {}),
65+
Document(id=1, content: 'Hello everyone', meta: {})
66+
]}
67+
```
68+
4369
### Pandas
4470

71+
[Complete example](https://github.com/EdAbati/dataframes-haystack/blob/main/notebooks/pandas-example.ipynb)
72+
4573
#### FileToPandasDataFrame
4674

4775
```python
@@ -87,6 +115,8 @@ Result:
87115

88116
### Polars
89117

118+
[Complete example](https://github.com/EdAbati/dataframes-haystack/blob/main/notebooks/polars-example.ipynb)
119+
90120
#### FileToPolarsDataFrame
91121

92122
```python

notebooks/dataframe-file-to-doc-example.ipynb

Lines changed: 467 additions & 0 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,16 @@ description = "Haystack custom components for your favourite dataframe library."
99
readme = "README.md"
1010
requires-python = ">=3.8"
1111
license = { file = "LICENSE" }
12-
keywords = ["nlp", "machine-learning", "ai", "haystack", "pandas", "dataframe", "polars", "llm"]
12+
keywords = [
13+
"nlp",
14+
"machine-learning",
15+
"ai",
16+
"haystack",
17+
"pandas",
18+
"dataframe",
19+
"polars",
20+
"llm",
21+
]
1322
authors = [{ name = "Edoardo Abati" }]
1423
classifiers = [
1524
"License :: OSI Approved :: MIT License",
@@ -26,6 +35,7 @@ classifiers = [
2635

2736
dependencies = [
2837
"haystack-ai>=2.0.0",
38+
"narwhals>=1.1.0",
2939
"typing_extensions",
3040
]
3141
[project.optional-dependencies]
@@ -42,7 +52,7 @@ path = "src/dataframes_haystack/__about__.py"
4252

4353
# Default environment
4454
[tool.hatch.envs.default]
45-
installer="uv"
55+
installer = "uv"
4656
dependencies = [
4757
"coverage[toml]>=6.5",
4858
"pytest",
@@ -88,8 +98,17 @@ check = "mypy --install-types --non-interactive {args:src/dataframes_haystack te
8898
detached = true
8999
dependencies = ["black>=24.3.0", "nbqa>=1.8.5", "ruff>=0.3.4"]
90100
[tool.hatch.envs.lint.scripts]
91-
style = ["ruff check {args:.}", "black --check --diff {args:.}", "nbqa black --check --diff notebooks/*"]
92-
fmt = ["black {args:.}", "ruff check --fix {args:.}", "nbqa black notebooks/*", "style"]
101+
style = [
102+
"ruff check {args:.}",
103+
"black --check --diff {args:.}",
104+
"nbqa black --check --diff notebooks/*",
105+
]
106+
fmt = [
107+
"black {args:.}",
108+
"ruff check --fix {args:.}",
109+
"nbqa black notebooks/*",
110+
"style",
111+
]
93112

94113
[tool.black]
95114
target-version = ["py38"]
@@ -102,60 +121,45 @@ line-length = 120
102121
extend-include = ["*.ipynb"]
103122

104123
[tool.ruff.lint]
105-
select = [
106-
"A",
107-
"ARG",
108-
"B",
109-
"C",
110-
"DTZ",
111-
"E",
112-
"EM",
113-
"F",
114-
"I",
115-
"ICN",
116-
"ISC",
117-
"N",
118-
"PLC",
119-
"PLE",
120-
"PLR",
121-
"PLW",
122-
"Q",
123-
"RUF",
124-
"S",
125-
"T",
126-
"TID",
127-
"UP",
128-
"W",
129-
"YTT",
130-
]
124+
select = ["ALL"]
131125
ignore = [
132126
# Allow non-abstract empty methods in abstract base classes
133127
"B027",
128+
# No required doctstring for modules, packages
129+
"D100",
130+
"D104",
131+
# No future annotations
132+
"FA100",
134133
# Ignore checks for possible passwords
135134
"S105",
136135
"S106",
137136
"S107",
138137
# Ignore complexity
139138
"C901",
139+
# Generic veriable name df is ok
140+
"PD901",
140141
"PLR0911",
141142
"PLR0912",
142143
"PLR0913",
143144
"PLR0915",
144145
]
145-
unfixable = [
146-
# Don't touch unused imports
147-
"F401",
148-
]
149146

150147
[tool.ruff.lint.isort]
151148
known-first-party = ["dataframes_haystack"]
152149

153150
[tool.ruff.lint.flake8-tidy-imports]
154151
ban-relative-imports = "all"
155152

153+
[tool.ruff.lint.pydocstyle]
154+
convention = "google"
155+
156+
[tool.ruff.format]
157+
docstring-code-format = true
158+
156159
[tool.ruff.lint.per-file-ignores]
157160
# Tests can use magic values, assertions, and relative imports
158-
"tests/**/*" = ["PLR2004", "S101", "TID252"]
161+
"tests/*" = ["PLR2004", "S101", "TID252", "D100", "D103"]
162+
"notebooks/*" = ["PTH123", "SIM115"]
159163

160164

161165
# Test coverage
@@ -168,7 +172,10 @@ omit = [
168172
]
169173

170174
[tool.coverage.paths]
171-
dataframes_haystack = ["src/dataframes_haystack", "*/dataframes-haystack/src/dataframes_haystack"]
175+
dataframes_haystack = [
176+
"src/dataframes_haystack",
177+
"*/dataframes-haystack/src/dataframes_haystack",
178+
]
172179
tests = ["tests", "*/dataframes-haystack/tests"]
173180

174181
[tool.coverage.report]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from dataframes_haystack.components.converters._common import DataFrameFileToDocument
2+
3+
__all__ = [
4+
"DataFrameFileToDocument",
5+
]
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import logging
2+
from functools import partial
3+
from typing import Any, Dict, List, Literal, Optional, Union
4+
5+
import narwhals.stable.v1 as nw
6+
from haystack import Document, component
7+
8+
from dataframes_haystack.components.converters._utils import (
9+
FileFormat,
10+
ReaderFunc,
11+
frame_to_documents,
12+
get_pandas_readers_map,
13+
get_polars_readers_map,
14+
read_with_select,
15+
)
16+
17+
logger = logging.getLogger(__name__)
18+
19+
Backends = Literal["pandas", "polars"]
20+
21+
22+
@component
23+
class DataFrameFileToDocument:
24+
"""Reads files and converts their data in Documents.
25+
26+
Usage example:
27+
```python
28+
from dataframes_haystack.components.converters import DataFrameFileToDocument
29+
30+
converter = DataFrameFileToDocument(content_column="text_str")
31+
results = converter.run(files=["file1.csv", "file2.csv"])
32+
documents = results["documents"]
33+
print(documents[0].content)
34+
```
35+
"""
36+
37+
def __init__(
38+
self,
39+
content_column: str,
40+
meta_columns: Union[List[str], None] = None,
41+
index_column: Union[str, None] = None,
42+
file_format: FileFormat = "csv",
43+
read_kwargs: Optional[Dict[str, Any]] = None,
44+
backend: Backends = "polars",
45+
) -> None:
46+
"""Create a DataFrameFileToDocument component.
47+
48+
Args:
49+
content_column: The name of the DataFrame column that contains the text content.
50+
meta_columns: Optional list of names of the DataFrame columns that contain metadata.
51+
index_column: The name of the DataFrame column that contains the index.
52+
file_format: The format of the files to read.
53+
read_kwargs: Optional keyword arguments to pass to the file reader function.
54+
backend: The backend to use for reading the files.
55+
"""
56+
self.content_column = content_column
57+
self.meta_columns = meta_columns or []
58+
self.index_column = index_column
59+
self.file_format = file_format
60+
self.read_kwargs = read_kwargs or {}
61+
self.backend = backend
62+
if self.backend not in ["pandas", "polars"]:
63+
msg = f"Unsupported backend: {self.backend}"
64+
raise ValueError(msg)
65+
self._reader_function = self._get_reader_function()
66+
67+
def _get_reader_function(self) -> ReaderFunc:
68+
file_format_mapping = get_pandas_readers_map() if self.backend == "pandas" else get_polars_readers_map()
69+
reader_function = file_format_mapping.get(self.file_format)
70+
if reader_function:
71+
return reader_function
72+
msg = f"Unsupported file format for {self.backend} backend: {self.file_format}"
73+
raise ValueError(msg)
74+
75+
def _run_read(self, file_paths: List[str]) -> nw.DataFrame:
76+
selected_columns = [self.index_column, self.content_column, *self.meta_columns]
77+
selected_columns = [col for col in selected_columns if col is not None]
78+
read_func = partial(self._reader_function, **self.read_kwargs)
79+
df_list = [read_with_select(read_func, file_path=path, columns_subset=selected_columns) for path in file_paths]
80+
return nw.concat(df_list, how="vertical")
81+
82+
@component.output_types(documents=List[Document])
83+
def run(
84+
self,
85+
file_paths: List[str],
86+
meta: Union[Dict[str, Any], List[Dict[str, Any]], None] = None,
87+
) -> Dict[str, List[Document]]:
88+
"""Reads files and converts their data in Documents.
89+
90+
Args:
91+
file_paths: List of file paths to read.
92+
meta:
93+
Optional metadata to attach to the Documents.
94+
This value can be either a dictionary or a list of dictionaries.
95+
If it's a dictionary, its content is added to the metadata of all produced Documents.
96+
If it's a list, the length of the list must match the number of rows in the DataFrame,
97+
because the two lists will be zipped.
98+
99+
Returns:
100+
A dictionary with the following keys:
101+
- `documents`: Created Documents
102+
"""
103+
df = self._run_read(file_paths)
104+
documents = frame_to_documents(
105+
df,
106+
content_column=self.content_column,
107+
meta_columns=self.meta_columns,
108+
index_column=self.index_column,
109+
extra_metadata=meta,
110+
)
111+
112+
return {"documents": documents}

0 commit comments

Comments
 (0)