apache-hamilton 1.90.0.dev0__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.
Files changed (151) hide show
  1. apache_hamilton-1.90.0.dev0.dist-info/METADATA +407 -0
  2. apache_hamilton-1.90.0.dev0.dist-info/RECORD +151 -0
  3. apache_hamilton-1.90.0.dev0.dist-info/WHEEL +4 -0
  4. apache_hamilton-1.90.0.dev0.dist-info/entry_points.txt +9 -0
  5. apache_hamilton-1.90.0.dev0.dist-info/licenses/DISCLAIMER +10 -0
  6. apache_hamilton-1.90.0.dev0.dist-info/licenses/LICENSE +228 -0
  7. apache_hamilton-1.90.0.dev0.dist-info/licenses/NOTICE +5 -0
  8. hamilton/__init__.py +24 -0
  9. hamilton/ad_hoc_utils.py +132 -0
  10. hamilton/async_driver.py +465 -0
  11. hamilton/base.py +466 -0
  12. hamilton/caching/__init__.py +16 -0
  13. hamilton/caching/adapter.py +1475 -0
  14. hamilton/caching/cache_key.py +70 -0
  15. hamilton/caching/fingerprinting.py +287 -0
  16. hamilton/caching/stores/__init__.py +16 -0
  17. hamilton/caching/stores/base.py +242 -0
  18. hamilton/caching/stores/file.py +140 -0
  19. hamilton/caching/stores/memory.py +297 -0
  20. hamilton/caching/stores/sqlite.py +282 -0
  21. hamilton/caching/stores/utils.py +40 -0
  22. hamilton/cli/__init__.py +16 -0
  23. hamilton/cli/__main__.py +328 -0
  24. hamilton/cli/commands.py +126 -0
  25. hamilton/cli/logic.py +338 -0
  26. hamilton/common/__init__.py +76 -0
  27. hamilton/contrib/__init__.py +41 -0
  28. hamilton/data_quality/__init__.py +16 -0
  29. hamilton/data_quality/base.py +198 -0
  30. hamilton/data_quality/default_validators.py +560 -0
  31. hamilton/data_quality/pandera_validators.py +121 -0
  32. hamilton/dataflows/__init__.py +726 -0
  33. hamilton/dataflows/template/README.md +25 -0
  34. hamilton/dataflows/template/__init__.py +54 -0
  35. hamilton/dataflows/template/author.md +28 -0
  36. hamilton/dataflows/template/requirements.txt +0 -0
  37. hamilton/dataflows/template/tags.json +7 -0
  38. hamilton/dataflows/template/valid_configs.jsonl +1 -0
  39. hamilton/dev_utils/__init__.py +16 -0
  40. hamilton/dev_utils/deprecation.py +204 -0
  41. hamilton/driver.py +2112 -0
  42. hamilton/execution/__init__.py +16 -0
  43. hamilton/execution/debugging_utils.py +56 -0
  44. hamilton/execution/executors.py +502 -0
  45. hamilton/execution/graph_functions.py +421 -0
  46. hamilton/execution/grouping.py +430 -0
  47. hamilton/execution/state.py +539 -0
  48. hamilton/experimental/__init__.py +27 -0
  49. hamilton/experimental/databackend.py +61 -0
  50. hamilton/experimental/decorators/__init__.py +16 -0
  51. hamilton/experimental/decorators/parameterize_frame.py +233 -0
  52. hamilton/experimental/h_async.py +29 -0
  53. hamilton/experimental/h_cache.py +413 -0
  54. hamilton/experimental/h_dask.py +28 -0
  55. hamilton/experimental/h_databackends.py +174 -0
  56. hamilton/experimental/h_ray.py +28 -0
  57. hamilton/experimental/h_spark.py +32 -0
  58. hamilton/function_modifiers/README +40 -0
  59. hamilton/function_modifiers/__init__.py +121 -0
  60. hamilton/function_modifiers/adapters.py +900 -0
  61. hamilton/function_modifiers/base.py +859 -0
  62. hamilton/function_modifiers/configuration.py +310 -0
  63. hamilton/function_modifiers/delayed.py +202 -0
  64. hamilton/function_modifiers/dependencies.py +246 -0
  65. hamilton/function_modifiers/expanders.py +1230 -0
  66. hamilton/function_modifiers/macros.py +1634 -0
  67. hamilton/function_modifiers/metadata.py +434 -0
  68. hamilton/function_modifiers/recursive.py +908 -0
  69. hamilton/function_modifiers/validation.py +289 -0
  70. hamilton/function_modifiers_base.py +31 -0
  71. hamilton/graph.py +1153 -0
  72. hamilton/graph_types.py +264 -0
  73. hamilton/graph_utils.py +41 -0
  74. hamilton/htypes.py +450 -0
  75. hamilton/io/__init__.py +32 -0
  76. hamilton/io/data_adapters.py +216 -0
  77. hamilton/io/default_data_loaders.py +224 -0
  78. hamilton/io/materialization.py +500 -0
  79. hamilton/io/utils.py +158 -0
  80. hamilton/lifecycle/__init__.py +67 -0
  81. hamilton/lifecycle/api.py +833 -0
  82. hamilton/lifecycle/base.py +1130 -0
  83. hamilton/lifecycle/default.py +802 -0
  84. hamilton/log_setup.py +47 -0
  85. hamilton/models.py +92 -0
  86. hamilton/node.py +449 -0
  87. hamilton/plugins/README.md +48 -0
  88. hamilton/plugins/__init__.py +16 -0
  89. hamilton/plugins/dask_extensions.py +47 -0
  90. hamilton/plugins/dlt_extensions.py +161 -0
  91. hamilton/plugins/geopandas_extensions.py +49 -0
  92. hamilton/plugins/h_dask.py +331 -0
  93. hamilton/plugins/h_ddog.py +522 -0
  94. hamilton/plugins/h_diskcache.py +163 -0
  95. hamilton/plugins/h_experiments/__init__.py +22 -0
  96. hamilton/plugins/h_experiments/__main__.py +62 -0
  97. hamilton/plugins/h_experiments/cache.py +39 -0
  98. hamilton/plugins/h_experiments/data_model.py +68 -0
  99. hamilton/plugins/h_experiments/hook.py +219 -0
  100. hamilton/plugins/h_experiments/server.py +375 -0
  101. hamilton/plugins/h_kedro.py +152 -0
  102. hamilton/plugins/h_logging.py +454 -0
  103. hamilton/plugins/h_mcp/__init__.py +28 -0
  104. hamilton/plugins/h_mcp/__main__.py +33 -0
  105. hamilton/plugins/h_mcp/_helpers.py +129 -0
  106. hamilton/plugins/h_mcp/_templates.py +417 -0
  107. hamilton/plugins/h_mcp/server.py +328 -0
  108. hamilton/plugins/h_mlflow.py +335 -0
  109. hamilton/plugins/h_narwhals.py +134 -0
  110. hamilton/plugins/h_openlineage.py +400 -0
  111. hamilton/plugins/h_opentelemetry.py +167 -0
  112. hamilton/plugins/h_pandas.py +257 -0
  113. hamilton/plugins/h_pandera.py +117 -0
  114. hamilton/plugins/h_polars.py +304 -0
  115. hamilton/plugins/h_polars_lazyframe.py +282 -0
  116. hamilton/plugins/h_pyarrow.py +56 -0
  117. hamilton/plugins/h_pydantic.py +127 -0
  118. hamilton/plugins/h_ray.py +242 -0
  119. hamilton/plugins/h_rich.py +142 -0
  120. hamilton/plugins/h_schema.py +493 -0
  121. hamilton/plugins/h_slack.py +100 -0
  122. hamilton/plugins/h_spark.py +1380 -0
  123. hamilton/plugins/h_threadpool.py +125 -0
  124. hamilton/plugins/h_tqdm.py +122 -0
  125. hamilton/plugins/h_vaex.py +129 -0
  126. hamilton/plugins/huggingface_extensions.py +236 -0
  127. hamilton/plugins/ibis_extensions.py +93 -0
  128. hamilton/plugins/jupyter_magic.py +622 -0
  129. hamilton/plugins/kedro_extensions.py +117 -0
  130. hamilton/plugins/lightgbm_extensions.py +99 -0
  131. hamilton/plugins/matplotlib_extensions.py +108 -0
  132. hamilton/plugins/mlflow_extensions.py +216 -0
  133. hamilton/plugins/numpy_extensions.py +105 -0
  134. hamilton/plugins/pandas_extensions.py +1763 -0
  135. hamilton/plugins/plotly_extensions.py +150 -0
  136. hamilton/plugins/polars_extensions.py +71 -0
  137. hamilton/plugins/polars_implementations.py +25 -0
  138. hamilton/plugins/polars_lazyframe_extensions.py +302 -0
  139. hamilton/plugins/polars_post_1_0_0_extensions.py +877 -0
  140. hamilton/plugins/polars_pre_1_0_0_extension.py +836 -0
  141. hamilton/plugins/pydantic_extensions.py +98 -0
  142. hamilton/plugins/pyspark_pandas_extensions.py +47 -0
  143. hamilton/plugins/sklearn_plot_extensions.py +129 -0
  144. hamilton/plugins/spark_extensions.py +105 -0
  145. hamilton/plugins/vaex_extensions.py +51 -0
  146. hamilton/plugins/xgboost_extensions.py +91 -0
  147. hamilton/plugins/yaml_extensions.py +89 -0
  148. hamilton/registry.py +254 -0
  149. hamilton/settings.py +18 -0
  150. hamilton/telemetry.py +50 -0
  151. hamilton/version.py +18 -0
@@ -0,0 +1,1763 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ import abc
19
+ import csv
20
+ import dataclasses
21
+ from collections.abc import Callable, Collection, Hashable, Iterator
22
+ from datetime import datetime
23
+ from io import BufferedReader, BytesIO, StringIO
24
+ from pathlib import Path
25
+ from typing import Any, TypeAlias
26
+
27
+ from packaging.version import Version
28
+
29
+ try:
30
+ import pandas as pd
31
+ except ImportError as e:
32
+ raise NotImplementedError("Pandas is not installed.") from e
33
+
34
+ from collections.abc import Iterable, Mapping, Sequence
35
+ from typing import Literal
36
+
37
+ try:
38
+ import fsspec
39
+ import pyarrow.fs
40
+
41
+ FILESYSTEM_TYPE = pyarrow.fs.FileSystem | fsspec.spec.AbstractFileSystem | None
42
+ except ImportError:
43
+ FILESYSTEM_TYPE = type | None
44
+
45
+ from sqlite3 import Connection
46
+
47
+ from pandas._typing import NpDtype
48
+ from pandas.core.dtypes.dtypes import ExtensionDtype
49
+
50
+ from hamilton import registry
51
+ from hamilton.io import utils
52
+ from hamilton.io.data_adapters import DataLoader, DataSaver
53
+
54
+ DATAFRAME_TYPE = pd.DataFrame
55
+ COLUMN_TYPE = pd.Series
56
+
57
+ JSONSerializable: TypeAlias = str | float | bool | list | dict | None
58
+ IndexLabel: TypeAlias = Hashable | Iterator[Hashable] | None
59
+ Dtype: TypeAlias = ExtensionDtype | NpDtype
60
+
61
+
62
+ @registry.get_column.register(pd.DataFrame)
63
+ def get_column_pandas(df: pd.DataFrame, column_name: str) -> pd.Series:
64
+ return df[column_name]
65
+
66
+
67
+ @registry.fill_with_scalar.register(pd.DataFrame)
68
+ def fill_with_scalar_pandas(df: pd.DataFrame, column_name: str, value: Any) -> pd.DataFrame:
69
+ df[column_name] = value
70
+ return df
71
+
72
+
73
+ def register_types():
74
+ """Function to register the types for this extension."""
75
+ registry.register_types("pandas", DATAFRAME_TYPE, COLUMN_TYPE)
76
+
77
+
78
+ register_types()
79
+
80
+
81
+ class DataFrameDataLoader(DataLoader, DataSaver, abc.ABC):
82
+ """Base class for data loaders that saves/loads pandas dataframes.
83
+ Note that these are currently grouped together, but this could change!
84
+ We can change this as these are not part of the publicly exposed APIs.
85
+ Rather, the fixed component is the keys (E.G. csv, feather, etc...) , which,
86
+ when combined with types, correspond to a group of specific parameter. As such,
87
+ the backwards-compatible invariance enables us to change the implementation
88
+ (which classes), and so long as the set of parameters/load targets are compatible,
89
+ we are good to go."""
90
+
91
+ @classmethod
92
+ def applicable_types(cls) -> Collection[type]:
93
+ return [DATAFRAME_TYPE]
94
+
95
+ @abc.abstractmethod
96
+ def load_data(self, type_: type[DATAFRAME_TYPE]) -> tuple[DATAFRAME_TYPE, dict[str, Any]]:
97
+ pass
98
+
99
+ @abc.abstractmethod
100
+ def save_data(self, data: DATAFRAME_TYPE) -> dict[str, Any]:
101
+ pass
102
+
103
+
104
+ @dataclasses.dataclass
105
+ class PandasCSVReader(DataLoader):
106
+ """
107
+ Class that handles saving CSV files with pandas.
108
+ Maps to https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html
109
+ """
110
+
111
+ # the filepath_or_buffer param will be changed to path for backwards compatibility
112
+ path: str | Path | BytesIO | BufferedReader
113
+ # kwargs
114
+ sep: str | None = ","
115
+ delimiter: str | None = None
116
+ header: Sequence | int | Literal["infer"] | None = "infer"
117
+ names: Sequence | None = None
118
+ index_col: Hashable | Sequence | Literal[False] | None = None
119
+ usecols: list[Hashable] | Callable | tuple | None = None
120
+ dtype: Dtype | dict[Hashable, Dtype] | None = None
121
+ engine: Literal["c", "python", "pyarrow", "python-fwf"] | None = None
122
+ converters: Mapping | None = None
123
+ true_values: list | None = None
124
+ false_values: list | None = None
125
+ skipinitialspace: bool | None = False
126
+ skiprows: list[int] | int | Callable[[Hashable], bool] | None = None
127
+ skipfooter: int = 0
128
+ nrows: int | None = None
129
+ na_values: Hashable | Iterable | Mapping | None = None
130
+ keep_default_na: bool = True
131
+ na_filter: bool = True
132
+ verbose: bool | None = None
133
+ skip_blank_lines: bool = True
134
+ parse_dates: bool | Sequence | None | None = False
135
+ keep_date_col: bool | None = None
136
+ date_format: str | None = None
137
+ dayfirst: bool = False
138
+ cache_dates: bool = True
139
+ iterator: bool = False
140
+ chunksize: int | None = None
141
+ compression: (
142
+ Literal["infer", "gzip", "bz2", "zip", "xz", "zstd", "tar"] | dict[str, Any] | None
143
+ ) = "infer"
144
+ thousands: str | None = None
145
+ decimal: str = "."
146
+ lineterminator: str | None = None
147
+ quotechar: str | None = None
148
+ quoting: int = 0
149
+ doublequote: bool = True
150
+ escapechar: str | None = None
151
+ comment: str | None = None
152
+ encoding: str = "utf-8"
153
+ encoding_errors: (
154
+ Literal["strict", "ignore", "replace", "backslashreplace", "surrogateescape"] | str
155
+ ) = "strict"
156
+ dialect: str | csv.Dialect | None = None
157
+ on_bad_lines: Literal["error", "warn", "skip"] | Callable = "error"
158
+ delim_whitespace: bool | None = None
159
+ low_memory: bool = True
160
+ memory_map: bool = False
161
+ float_precision: Literal["high", "legacy", "round_trip"] | None = None
162
+ storage_options: dict[str, Any] | None = None
163
+ dtype_backend: Literal["pyarrow", "numpy_nullable"] = "numpy_nullable"
164
+
165
+ @classmethod
166
+ def applicable_types(cls) -> Collection[type]:
167
+ return [DATAFRAME_TYPE]
168
+
169
+ def _get_loading_kwargs(self) -> dict[str, Any]:
170
+ kwargs = {}
171
+ if self.sep is not None:
172
+ kwargs["sep"] = self.sep
173
+ if self.delimiter is not None:
174
+ kwargs["delimiter"] = self.delimiter
175
+ if self.header is not None:
176
+ kwargs["header"] = self.header
177
+ if self.names is not None:
178
+ kwargs["names"] = self.names
179
+ if self.index_col is not None:
180
+ kwargs["index_col"] = self.index_col
181
+ if self.usecols is not None:
182
+ kwargs["usecols"] = self.usecols
183
+ if self.dtype is not None:
184
+ kwargs["dtype"] = self.dtype
185
+ if self.engine is not None:
186
+ kwargs["engine"] = self.engine
187
+ if self.converters is not None:
188
+ kwargs["converters"] = self.converters
189
+ if self.true_values is not None:
190
+ kwargs["true_values"] = self.true_values
191
+ if self.false_values is not None:
192
+ kwargs["false_values"] = self.false_values
193
+ if self.skipinitialspace is not None:
194
+ kwargs["skipinitialspace"] = self.skipinitialspace
195
+ if self.skiprows is not None:
196
+ kwargs["skiprows"] = self.skiprows
197
+ if self.nrows is not None:
198
+ kwargs["nrows"] = self.nrows
199
+ if self.na_values is not None:
200
+ kwargs["na_values"] = self.na_values
201
+ if self.keep_default_na is not None:
202
+ kwargs["keep_default_na"] = self.keep_default_na
203
+ if self.na_filter is not None:
204
+ kwargs["na_filter"] = self.na_filter
205
+ if Version(pd.__version__) < Version("2.2") and self.verbose is not None:
206
+ kwargs["verbose"] = self.verbose
207
+ if self.skip_blank_lines is not None:
208
+ kwargs["skip_blank_lines"] = self.skip_blank_lines
209
+ if self.parse_dates is not None:
210
+ kwargs["parse_dates"] = self.parse_dates
211
+ if Version(pd.__version__) < Version("2.2") and self.keep_date_col is not None:
212
+ kwargs["keep_date_col"] = self.keep_date_col
213
+ if self.date_format is not None:
214
+ kwargs["date_format"] = self.date_format
215
+ if self.dayfirst is not None:
216
+ kwargs["dayfirst"] = self.dayfirst
217
+ if self.cache_dates is not None:
218
+ kwargs["cache_dates"] = self.cache_dates
219
+ if self.iterator is not None:
220
+ kwargs["iterator"] = self.iterator
221
+ if self.chunksize is not None:
222
+ kwargs["chunksize"] = self.chunksize
223
+ if self.compression is not None:
224
+ kwargs["compression"] = self.compression
225
+ if self.thousands is not None:
226
+ kwargs["thousands"] = self.thousands
227
+ if self.lineterminator is not None:
228
+ kwargs["lineterminator"] = self.lineterminator
229
+ if self.quotechar is not None:
230
+ kwargs["quotechar"] = self.quotechar
231
+ if self.quoting is not None:
232
+ kwargs["quoting"] = self.quoting
233
+ if self.doublequote is not None:
234
+ kwargs["doublequote"] = self.doublequote
235
+ if self.escapechar is not None:
236
+ kwargs["escapechar"] = self.escapechar
237
+ if self.comment is not None:
238
+ kwargs["comment"] = self.comment
239
+ if self.encoding is not None:
240
+ kwargs["encoding"] = self.encoding
241
+ if self.encoding_errors is not None:
242
+ kwargs["encoding_errors"] = self.encoding_errors
243
+ if self.dialect is not None:
244
+ kwargs["dialect"] = self.dialect
245
+ if self.on_bad_lines is not None:
246
+ kwargs["on_bad_lines"] = self.on_bad_lines
247
+ if Version(pd.__version__) < Version("2.2") and self.delim_whitespace is not None:
248
+ kwargs["delim_whitespace"] = self.delim_whitespace
249
+ if self.low_memory is not None:
250
+ kwargs["low_memory"] = self.low_memory
251
+ if self.memory_map is not None:
252
+ kwargs["memory_map"] = self.memory_map
253
+ if self.float_precision is not None:
254
+ kwargs["float_precision"] = self.float_precision
255
+ if self.storage_options is not None:
256
+ kwargs["storage_options"] = self.storage_options
257
+ if Version(pd.__version__) >= Version("2.0") and self.dtype_backend is not None:
258
+ kwargs["dtype_backend"] = self.dtype_backend
259
+
260
+ return kwargs
261
+
262
+ def load_data(self, type_: type) -> tuple[DATAFRAME_TYPE, dict[str, Any]]:
263
+ df = pd.read_csv(self.path, **self._get_loading_kwargs())
264
+ metadata = utils.get_file_and_dataframe_metadata(self.path, df)
265
+ return df, metadata
266
+
267
+ @classmethod
268
+ def name(cls) -> str:
269
+ return "csv"
270
+
271
+
272
+ @dataclasses.dataclass
273
+ class PandasCSVWriter(DataSaver):
274
+ """Class that handles saving CSV files with pandas.
275
+ Maps to https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_csv.html
276
+ """
277
+
278
+ path: str | Path | BytesIO | BufferedReader
279
+ # kwargs
280
+ sep: str | None = ","
281
+ na_rep: str = ""
282
+ float_format: str | Callable | None = None
283
+ columns: Sequence | None = None
284
+ header: bool | list[str] | None = True
285
+ index: bool | None = False
286
+ index_label: IndexLabel | None = None
287
+ mode: str = "w"
288
+ encoding: str | None = None
289
+ compression: (
290
+ Literal["infer", "gzip", "bz2", "zip", "xz", "zstd", "tar"] | dict[str, Any] | None
291
+ ) = "infer"
292
+ quoting: int | None = None
293
+ quotechar: str | None = '"'
294
+ lineterminator: str | None = None
295
+ chunksize: int | None = None
296
+ date_format: str | None = None
297
+ doublequote: bool = True
298
+ escapechar: str | None = None
299
+ decimal: str = "."
300
+ errors: str = "strict"
301
+ storage_options: dict[str, Any] | None = None
302
+
303
+ @classmethod
304
+ def applicable_types(cls) -> Collection[type]:
305
+ return [DATAFRAME_TYPE]
306
+
307
+ def _get_saving_kwargs(self) -> dict[str, Any]:
308
+ # Puts kwargs in a dict
309
+ kwargs = {}
310
+ if self.sep is not None:
311
+ kwargs["sep"] = self.sep
312
+ if self.na_rep is not None:
313
+ kwargs["na_rep"] = self.na_rep
314
+ if self.float_format is not None:
315
+ kwargs["float_format"] = self.float_format
316
+ if self.columns is not None:
317
+ kwargs["columns"] = self.columns
318
+ if self.header is not None:
319
+ kwargs["header"] = self.header
320
+ if self.index is not None:
321
+ kwargs["index"] = self.index
322
+ if self.index_label is not None:
323
+ kwargs["index_label"] = self.index_label
324
+ if self.mode is not None:
325
+ kwargs["mode"] = self.mode
326
+ if self.encoding is not None:
327
+ kwargs["encoding"] = self.encoding
328
+ if self.compression is not None:
329
+ kwargs["compression"] = self.compression
330
+ if self.quoting is not None:
331
+ kwargs["quoting"] = self.quoting
332
+ if self.quotechar is not None:
333
+ kwargs["quotechar"] = self.quotechar
334
+ if self.lineterminator is not None:
335
+ kwargs["lineterminator"] = self.lineterminator
336
+ if self.chunksize is not None:
337
+ kwargs["chunksize"] = self.chunksize
338
+ if self.date_format is not None:
339
+ kwargs["date_format"] = self.date_format
340
+ if self.doublequote is not None:
341
+ kwargs["doublequote"] = self.doublequote
342
+ if self.escapechar is not None:
343
+ kwargs["escapechar"] = self.escapechar
344
+ if self.decimal is not None:
345
+ kwargs["decimal"] = self.decimal
346
+ if self.errors is not None:
347
+ kwargs["errors"] = self.errors
348
+ if self.storage_options is not None:
349
+ kwargs["storage_options"] = self.storage_options
350
+
351
+ return kwargs
352
+
353
+ def save_data(self, data: DATAFRAME_TYPE) -> dict[str, Any]:
354
+ data.to_csv(self.path, **self._get_saving_kwargs())
355
+ return utils.get_file_and_dataframe_metadata(self.path, data)
356
+
357
+ @classmethod
358
+ def name(cls) -> str:
359
+ return "csv"
360
+
361
+
362
+ @dataclasses.dataclass
363
+ class PandasParquetReader(DataLoader):
364
+ """Class that handles saving parquet files with pandas.
365
+ Maps to https://pandas.pydata.org/docs/reference/api/pandas.read_parquet.html#pandas.read_parquet
366
+ """
367
+
368
+ path: str | Path | BytesIO | BufferedReader
369
+ # kwargs
370
+ engine: Literal["auto", "pyarrow", "fastparquet"] = "auto"
371
+ columns: list[str] | None = None
372
+ storage_options: dict[str, Any] | None = None
373
+ use_nullable_dtypes: bool = False
374
+ dtype_backend: Literal["numpy_nullable", "pyarrow"] = "numpy_nullable"
375
+ filesystem: str | None = None
376
+ filters: list[tuple] | list[list[tuple]] | None = None
377
+
378
+ @classmethod
379
+ def applicable_types(cls) -> Collection[type]:
380
+ return [DATAFRAME_TYPE]
381
+
382
+ def _get_loading_kwargs(self):
383
+ kwargs = {}
384
+ if self.engine is not None:
385
+ kwargs["engine"] = self.engine
386
+ if self.columns is not None:
387
+ kwargs["columns"] = self.columns
388
+ if self.storage_options is not None:
389
+ kwargs["storage_options"] = self.storage_options
390
+ if Version(pd.__version__) < Version("2.0") and self.use_nullable_dtypes is not None:
391
+ kwargs["use_nullable_dtypes"] = self.use_nullable_dtypes
392
+ if Version(pd.__version__) >= Version("2.0") and self.dtype_backend is not None:
393
+ kwargs["dtype_backend"] = self.dtype_backend
394
+ if self.filesystem is not None:
395
+ kwargs["filesystem"] = self.filesystem
396
+ if self.filters is not None:
397
+ kwargs["filters"] = self.filters
398
+
399
+ return kwargs
400
+
401
+ def load_data(self, type_: type) -> tuple[DATAFRAME_TYPE, dict[str, Any]]:
402
+ # Loads the data and returns the df and metadata of the pickle
403
+ df = pd.read_parquet(self.path, **self._get_loading_kwargs())
404
+ metadata = utils.get_file_and_dataframe_metadata(self.path, df)
405
+ return df, metadata
406
+
407
+ @classmethod
408
+ def name(cls) -> str:
409
+ return "parquet"
410
+
411
+
412
+ @dataclasses.dataclass
413
+ class PandasParquetWriter(DataSaver):
414
+ """Class that handles saving parquet files with pandas.
415
+ Maps to https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_parquet.html#pandas.DataFrame.to_parquet
416
+ """
417
+
418
+ path: str | Path | BytesIO | BufferedReader
419
+ # kwargs
420
+ engine: Literal["auto", "pyarrow", "fastparquet"] = "auto"
421
+ compression: str | None = "snappy"
422
+ index: bool | None = None
423
+ partition_cols: list[str] | None = None
424
+ storage_options: dict[str, Any] | None = None
425
+ extra_kwargs: dict[str, Any] | None = None
426
+
427
+ @classmethod
428
+ def applicable_types(cls) -> Collection[type]:
429
+ return [DATAFRAME_TYPE]
430
+
431
+ def _get_saving_kwargs(self) -> dict[str, Any]:
432
+ # Puts kwargs in a dict
433
+ kwargs = {}
434
+ if self.engine is not None:
435
+ kwargs["engine"] = self.engine
436
+ if self.compression is not None:
437
+ kwargs["compression"] = self.compression
438
+ if self.index is not None:
439
+ kwargs["index"] = self.index
440
+ if self.partition_cols is not None:
441
+ kwargs["partition_cols"] = self.partition_cols
442
+ if self.storage_options is not None:
443
+ kwargs["storage_options"] = self.storage_options
444
+ if self.extra_kwargs is not None:
445
+ kwargs.update(self.extra_kwargs)
446
+ return kwargs
447
+
448
+ def save_data(self, data: DATAFRAME_TYPE) -> dict[str, Any]:
449
+ data.to_parquet(self.path, **self._get_saving_kwargs())
450
+ return utils.get_file_and_dataframe_metadata(self.path, data)
451
+
452
+ @classmethod
453
+ def name(cls) -> str:
454
+ return "parquet"
455
+
456
+
457
+ @dataclasses.dataclass
458
+ class PandasPickleReader(DataLoader):
459
+ """Class for loading/reading pickle files with Pandas.
460
+ Maps to https://pandas.pydata.org/docs/reference/api/pandas.read_pickle.html#pandas.read_pickle
461
+ """
462
+
463
+ filepath_or_buffer: str | Path | BytesIO | BufferedReader = None
464
+ path: str | Path | BytesIO | BufferedReader = (
465
+ None # alias for `filepath_or_buffer` to keep reading/writing args symmetric.
466
+ )
467
+ # kwargs:
468
+ compression: str | dict[str, Any] | None = "infer"
469
+ storage_options: dict[str, Any] | None = None
470
+
471
+ @classmethod
472
+ def applicable_types(cls) -> Collection[type]:
473
+ # Returns type for which data loader is available
474
+ return [DATAFRAME_TYPE]
475
+
476
+ def _get_loading_kwargs(self) -> dict[str, Any]:
477
+ # Puts kwargs in a dict
478
+ kwargs = {}
479
+ if self.compression is not None:
480
+ kwargs["compression"] = self.compression
481
+ if self.storage_options is not None:
482
+ kwargs["storage_options"] = self.storage_options
483
+ return kwargs
484
+
485
+ def load_data(self, type_: type) -> tuple[DATAFRAME_TYPE, dict[str, Any]]:
486
+ # Loads the data and returns the df and metadata of the pickle
487
+ df = pd.read_pickle(self.filepath_or_buffer, **self._get_loading_kwargs())
488
+ metadata = utils.get_file_and_dataframe_metadata(self.filepath_or_buffer, df)
489
+ return df, metadata
490
+
491
+ @classmethod
492
+ def name(cls) -> str:
493
+ return "pickle"
494
+
495
+ def __post_init__(self):
496
+ """As we're adding in a path alias for filepath_or_buffer, we need to ensure that
497
+ we have backwards compatibility with the old parameter. That means that:
498
+ 1. Either filepath_or_buffer or path must be specified, not both
499
+ 2. If path is specified, filepath_or_buffer is set to path
500
+ """
501
+ if self.filepath_or_buffer is None and self.path is None:
502
+ raise ValueError("Either filepath_or_buffer or path must be specified")
503
+ elif self.filepath_or_buffer is not None and self.path is not None:
504
+ raise ValueError("Only one of filepath_or_buffer or path must be specified")
505
+ elif self.filepath_or_buffer is None:
506
+ self.filepath_or_buffer = self.path
507
+
508
+
509
+ pickle_protocol_default = 5
510
+
511
+
512
+ @dataclasses.dataclass
513
+ class PandasPickleWriter(DataSaver):
514
+ """Class that handles saving pickle files with pandas.
515
+ Maps to https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_pickle.html#pandas.DataFrame.to_pickle
516
+ """
517
+
518
+ path: str | Path | BytesIO | BufferedReader
519
+ # kwargs:
520
+ compression: str | dict[str, Any] | None = "infer"
521
+ protocol: int = pickle_protocol_default
522
+ storage_options: dict[str, Any] | None = None
523
+
524
+ @classmethod
525
+ def applicable_types(cls) -> Collection[type]:
526
+ return [DATAFRAME_TYPE]
527
+
528
+ def _get_saving_kwargs(self) -> dict[str, Any]:
529
+ # Puts kwargs in a dict
530
+ kwargs = {}
531
+ if self.compression is not None:
532
+ kwargs["compression"] = self.compression
533
+ if self.protocol is not None:
534
+ kwargs["protocol"] = self.protocol
535
+ if self.storage_options is not None:
536
+ kwargs["storage_options"] = self.storage_options
537
+ return kwargs
538
+
539
+ def save_data(self, data: DATAFRAME_TYPE) -> dict[str, Any]:
540
+ data.to_pickle(self.path, **self._get_saving_kwargs())
541
+ return utils.get_file_and_dataframe_metadata(self.path, data)
542
+
543
+ @classmethod
544
+ def name(cls) -> str:
545
+ return "pickle"
546
+
547
+
548
+ @dataclasses.dataclass
549
+ class PandasJsonReader(DataLoader):
550
+ """Class specifically to handle loading JSON files/buffers with Pandas.
551
+
552
+ Disclaimer: We're exposing all the *current* params from the Pandas read_json method.
553
+ Some of these params may get deprecated or new params may be introduced. In the event that
554
+ the params/kwargs below become outdated, please raise an issue or submit a pull request.
555
+
556
+ Should map to https://pandas.pydata.org/docs/reference/api/pandas.read_json.html
557
+ """
558
+
559
+ filepath_or_buffer: str | Path | BytesIO | BufferedReader
560
+ # kwargs
561
+ chunksize: int | None = None
562
+ compression: str | dict[str, Any] | None = "infer"
563
+ convert_axes: bool | None = None
564
+ convert_dates: bool | list[str] = True
565
+ date_unit: str | None = None
566
+ dtype: Dtype | dict[Hashable, Dtype] | None = None
567
+ dtype_backend: str | None = None
568
+ encoding: str | None = None
569
+ encoding_errors: str | None = "strict"
570
+ engine: str = "ujson"
571
+ keep_default_dates: bool = True
572
+ lines: bool = False
573
+ nrows: int | None = None
574
+ orient: str | None = None
575
+ precise_float: bool = False
576
+ storage_options: dict[str, Any] | None = None
577
+ typ: str = "frame"
578
+
579
+ @classmethod
580
+ def applicable_types(cls) -> Collection[type]:
581
+ return [DATAFRAME_TYPE]
582
+
583
+ def _get_loading_kwargs(self) -> dict[str, Any]:
584
+ kwargs = {}
585
+ if self.chunksize is not None:
586
+ kwargs["chunksize"] = self.chunksize
587
+ if self.compression is not None:
588
+ kwargs["compression"] = self.compression
589
+ if self.convert_axes is not None:
590
+ kwargs["convert_axes"] = self.convert_axes
591
+ if self.convert_dates is not None:
592
+ kwargs["convert_dates"] = self.convert_dates
593
+ if self.date_unit is not None:
594
+ kwargs["date_unit"] = self.date_unit
595
+ if self.dtype is not None:
596
+ kwargs["dtype"] = self.dtype
597
+ if Version(pd.__version__) >= Version("2.0") and self.dtype_backend is not None:
598
+ kwargs["dtype_backend"] = self.dtype_backend
599
+ if self.encoding is not None:
600
+ kwargs["encoding"] = self.encoding
601
+ if self.encoding_errors is not None:
602
+ kwargs["encoding_errors"] = self.encoding_errors
603
+ if self.engine is not None:
604
+ kwargs["engine"] = self.engine
605
+ if self.keep_default_dates is not None:
606
+ kwargs["keep_default_dates"] = self.keep_default_dates
607
+ if self.lines is not None:
608
+ kwargs["lines"] = self.lines
609
+ if self.nrows is not None:
610
+ kwargs["nrows"] = self.nrows
611
+ if self.orient is not None:
612
+ kwargs["orient"] = self.orient
613
+ if self.precise_float is not None:
614
+ kwargs["precise_float"] = self.precise_float
615
+ if self.storage_options is not None:
616
+ kwargs["storage_options"] = self.storage_options
617
+ if self.typ is not None:
618
+ kwargs["typ"] = self.typ
619
+ return kwargs
620
+
621
+ def load_data(self, type_: type) -> tuple[DATAFRAME_TYPE, dict[str, Any]]:
622
+ df = pd.read_json(self.filepath_or_buffer, **self._get_loading_kwargs())
623
+ metadata = utils.get_file_and_dataframe_metadata(self.filepath_or_buffer, df)
624
+ return df, metadata
625
+
626
+ @classmethod
627
+ def name(cls) -> str:
628
+ return "json"
629
+
630
+
631
+ @dataclasses.dataclass
632
+ class PandasJsonWriter(DataSaver):
633
+ """Class specifically to handle saving JSON files/buffers with Pandas.
634
+
635
+ Disclaimer: We're exposing all the *current* params from the Pandas DataFrame.to_json method.
636
+ Some of these params may get deprecated or new params may be introduced. In the event that
637
+ the params/kwargs below become outdated, please raise an issue or submit a pull request.
638
+
639
+ Should map to https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_json.html
640
+ """
641
+
642
+ filepath_or_buffer: str | Path | BytesIO | BufferedReader
643
+ # kwargs
644
+ compression: str = "infer"
645
+ date_format: str = "epoch"
646
+ date_unit: str = "ms"
647
+ default_handler: Callable[[Any], JSONSerializable] | None = None
648
+ double_precision: int = 10
649
+ force_ascii: bool = True
650
+ index: bool | None = None
651
+ indent: int = 0
652
+ lines: bool = False
653
+ mode: str = "w"
654
+ orient: str | None = None
655
+ storage_options: dict[str, Any] | None = None
656
+
657
+ @classmethod
658
+ def applicable_types(cls) -> Collection[type]:
659
+ return [DATAFRAME_TYPE]
660
+
661
+ def _get_saving_kwargs(self):
662
+ kwargs = {}
663
+ if self.compression is not None:
664
+ kwargs["compression"] = self.compression
665
+ if self.date_format is not None:
666
+ kwargs["date_format"] = self.date_format
667
+ if self.date_unit is not None:
668
+ kwargs["date_unit"] = self.date_unit
669
+ if self.default_handler is not None:
670
+ kwargs["default_handler"] = self.default_handler
671
+ if self.double_precision is not None:
672
+ kwargs["double_precision"] = self.double_precision
673
+ if self.force_ascii is not None:
674
+ kwargs["force_ascii"] = self.force_ascii
675
+ if self.index is not None:
676
+ kwargs["index"] = self.index
677
+ if self.indent is not None:
678
+ kwargs["indent"] = self.indent
679
+ if self.lines is not False:
680
+ kwargs["lines"] = self.lines
681
+ if self.mode is not None:
682
+ kwargs["mode"] = self.mode
683
+ if self.orient is not None:
684
+ kwargs["orient"] = self.orient
685
+ if self.storage_options is not None:
686
+ kwargs["storage_options"] = self.storage_options
687
+ return kwargs
688
+
689
+ def save_data(self, data: DATAFRAME_TYPE) -> dict[str, Any]:
690
+ data.to_json(self.filepath_or_buffer, **self._get_saving_kwargs())
691
+ return utils.get_file_and_dataframe_metadata(self.filepath_or_buffer, data)
692
+
693
+ @classmethod
694
+ def name(cls) -> str:
695
+ return "json"
696
+
697
+
698
+ @dataclasses.dataclass
699
+ class PandasSqlReader(DataLoader):
700
+ """Class specifically to handle loading SQL data using Pandas.
701
+
702
+ Disclaimer: We're exposing all the *current* params from the Pandas read_sql method.
703
+ Some of these params may get deprecated or new params may be introduced. In the event that
704
+ the params/kwargs below become outdated, please raise an issue or submit a pull request.
705
+
706
+ Should map to https://pandas.pydata.org/docs/reference/api/pandas.read_sql.html
707
+ Requires optional Pandas dependencies. See https://pandas.pydata.org/docs/getting_started/install.html#sql-databases.
708
+ """
709
+
710
+ query_or_table: str
711
+ db_connection: str | Connection # can pass in SQLAlchemy engine/connection
712
+ # kwarg
713
+ chunksize: int | None = None
714
+ coerce_float: bool = True
715
+ columns: list[str] | None = None
716
+ dtype: Dtype | dict[Hashable, Dtype] | None = None
717
+ dtype_backend: str | None = None
718
+ index_col: str | list[str] | None = None
719
+ params: list | tuple | dict | None = None
720
+ parse_dates: list | dict | None = None
721
+
722
+ @classmethod
723
+ def applicable_types(cls) -> Collection[type]:
724
+ return [DATAFRAME_TYPE]
725
+
726
+ def _get_loading_kwargs(self) -> dict[str, Any]:
727
+ kwargs = {}
728
+ if self.chunksize is not None:
729
+ kwargs["chunksize"] = self.chunksize
730
+ if self.coerce_float is not None:
731
+ kwargs["coerce_float"] = self.coerce_float
732
+ if self.columns is not None:
733
+ kwargs["columns"] = self.columns
734
+ if self.dtype is not None:
735
+ kwargs["dtype"] = self.dtype
736
+ if Version(pd.__version__) >= Version("2.0") and self.dtype_backend is not None:
737
+ kwargs["dtype_backend"] = self.dtype_backend
738
+ if self.index_col is not None:
739
+ kwargs["index_col"] = self.index_col
740
+ if self.params is not None:
741
+ kwargs["params"] = self.params
742
+ if self.parse_dates is not None:
743
+ kwargs["parse_dates"] = self.parse_dates
744
+ return kwargs
745
+
746
+ def load_data(self, type_: type) -> tuple[DATAFRAME_TYPE, dict[str, Any]]:
747
+ df = pd.read_sql(self.query_or_table, self.db_connection, **self._get_loading_kwargs())
748
+ sql_metadata = utils.get_sql_metadata(self.query_or_table, df)
749
+ df_metadata = utils.get_dataframe_metadata(df)
750
+ return df, {**sql_metadata, **df_metadata}
751
+
752
+ @classmethod
753
+ def name(cls) -> str:
754
+ return "sql"
755
+
756
+
757
+ @dataclasses.dataclass
758
+ class PandasSqlWriter(DataSaver):
759
+ """Class specifically to handle saving DataFrames to SQL databases using Pandas.
760
+
761
+ Disclaimer: We're exposing all the *current* params from the Pandas DataFrame.to_sql method.
762
+ Some of these params may get deprecated or new params may be introduced. In the event that
763
+ the params/kwargs below become outdated, please raise an issue or submit a pull request.
764
+
765
+ Should map to https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_sql.html
766
+ Requires optional Pandas dependencies. See https://pandas.pydata.org/docs/getting_started/install.html#sql-databases.
767
+ """
768
+
769
+ table_name: str
770
+ db_connection: Any # can pass in SQLAlchemy engine/connection
771
+ # kwargs
772
+ chunksize: int | None = None
773
+ dtype: Dtype | dict[Hashable, Dtype] | None = None
774
+ if_exists: str = "fail"
775
+ index: bool = True
776
+ index_label: IndexLabel | None = None
777
+ method: str | Callable | None = None
778
+ schema: str | None = None
779
+
780
+ @classmethod
781
+ def applicable_types(cls) -> Collection[type]:
782
+ return [DATAFRAME_TYPE]
783
+
784
+ def _get_saving_kwargs(self) -> dict[str, Any]:
785
+ kwargs = {}
786
+ if self.chunksize is not None:
787
+ kwargs["chunksize"] = self.chunksize
788
+ if self.dtype is not None:
789
+ kwargs["dtype"] = self.dtype
790
+ if self.if_exists is not None:
791
+ kwargs["if_exists"] = self.if_exists
792
+ if self.index is not None:
793
+ kwargs["index"] = self.index
794
+ if self.index_label is not None:
795
+ kwargs["index_label"] = self.index_label
796
+ if self.method is not None:
797
+ kwargs["method"] = self.method
798
+ if self.schema is not None:
799
+ kwargs["schema"] = self.schema
800
+ return kwargs
801
+
802
+ def save_data(self, data: DATAFRAME_TYPE) -> dict[str, Any]:
803
+ results = data.to_sql(self.table_name, self.db_connection, **self._get_saving_kwargs())
804
+ sql_metadata = utils.get_sql_metadata(self.table_name, results)
805
+ df_metadata = utils.get_dataframe_metadata(data)
806
+ return {**sql_metadata, **df_metadata}
807
+
808
+ @classmethod
809
+ def name(cls) -> str:
810
+ return "sql"
811
+
812
+
813
+ @dataclasses.dataclass
814
+ class PandasXmlReader(DataLoader):
815
+ """Class for loading/reading xml files with Pandas.
816
+ Maps to https://pandas.pydata.org/docs/reference/api/pandas.read_xml.html
817
+
818
+ Requires `lxml`. See https://pandas.pydata.org/docs/getting_started/install.html#xml
819
+ """
820
+
821
+ path_or_buffer: str | Path | BytesIO | BufferedReader
822
+ # kwargs
823
+ xpath: str | None = "./*"
824
+ namespace: dict[str, str] | None = None
825
+ elems_only: bool | None = False
826
+ attrs_only: bool | None = False
827
+ names: list[str] | None = None
828
+ dtype: dict[str, Any] | None = None
829
+ converters: dict[int | str, Any] | None = None
830
+ parse_dates: bool | list[int | str | list[list] | dict[str, list[int]]] = False
831
+ encoding: str | None = "utf-8"
832
+ parser: str = "lxml"
833
+ stylesheet: str | Path | BytesIO | BufferedReader = None
834
+ iterparse: dict[str, list[str]] | None = None
835
+ compression: str | dict[str, Any] | None = "infer"
836
+ storage_options: dict[str, Any] | None = None
837
+ dtype_backend: str = "numpy_nullable"
838
+
839
+ @classmethod
840
+ def applicable_types(cls) -> Collection[type]:
841
+ return [DATAFRAME_TYPE]
842
+
843
+ def _get_loading_kwargs(self) -> dict[str, Any]:
844
+ kwargs = {}
845
+ if self.xpath is not None:
846
+ kwargs["xpath"] = self.xpath
847
+ if self.namespace is not None:
848
+ kwargs["namespace"] = self.namespace
849
+ if self.elems_only is not None:
850
+ kwargs["elems_only"] = self.elems_only
851
+ if self.attrs_only is not None:
852
+ kwargs["attrs_only"] = self.attrs_only
853
+ if self.names is not None:
854
+ kwargs["names"] = self.names
855
+ if self.dtype is not None:
856
+ kwargs["dtype"] = self.dtype
857
+ if self.converters is not None:
858
+ kwargs["converters"] = self.converters
859
+ if self.parse_dates is not None:
860
+ kwargs["parse_dates"] = self.parse_dates
861
+ if self.encoding is not None:
862
+ kwargs["encoding"] = self.encoding
863
+ if self.parser is not None:
864
+ kwargs["parser"] = self.parser
865
+ if self.encoding is not None:
866
+ kwargs["encoding"] = self.encoding
867
+ if self.parser is not None:
868
+ kwargs["parser"] = self.parser
869
+ if self.stylesheet is not None:
870
+ kwargs["stylesheet"] = self.stylesheet
871
+ if self.iterparse is not None:
872
+ kwargs["iterparse"] = self.iterparse
873
+ if self.compression is not None:
874
+ kwargs["compression"] = self.compression
875
+ if self.storage_options is not None:
876
+ kwargs["storage_options"] = self.storage_options
877
+ if Version(pd.__version__) >= Version("2.0") and self.dtype_backend is not None:
878
+ kwargs["dtype_backend"] = self.dtype_backend
879
+ return kwargs
880
+
881
+ def load_data(self, type: type) -> tuple[DATAFRAME_TYPE, dict[str, Any]]:
882
+ # Loads the data and returns the df and metadata of the xml
883
+ df = pd.read_xml(self.path_or_buffer, **self._get_loading_kwargs())
884
+ metadata = utils.get_file_and_dataframe_metadata(self.path_or_buffer, df)
885
+ return df, metadata
886
+
887
+ @classmethod
888
+ def name(cls) -> str:
889
+ return "xml"
890
+
891
+
892
+ @dataclasses.dataclass
893
+ class PandasXmlWriter(DataSaver):
894
+ """Class specifically to handle saving xml files/buffers with Pandas.
895
+ Should map to https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_xml.html
896
+
897
+ Requires `lxml`. See https://pandas.pydata.org/docs/getting_started/install.html#xml.
898
+ """
899
+
900
+ path_or_buffer: str | Path | BytesIO | BufferedReader
901
+ # kwargs
902
+ index: bool = True
903
+ root_name: str = "data"
904
+ row_name: str = "row"
905
+ na_rep: str | None = None
906
+ attr_cols: list[str] | None = None
907
+ elems_cols: list[str] | None = None
908
+ namespaces: dict[str, str] | None = None
909
+ prefix: str | None = None
910
+ encoding: str = "utf-8"
911
+ xml_declaration: bool = True
912
+ pretty_print: bool = True
913
+ parser: str = "lxml"
914
+ stylesheet: str | Path | BytesIO | BufferedReader | None = None
915
+ compression: str | dict[str, Any] | None = "infer"
916
+ storage_options: dict[str, Any] | None = None
917
+
918
+ @classmethod
919
+ def applicable_types(cls) -> Collection[type]:
920
+ return [DATAFRAME_TYPE]
921
+
922
+ def _get_saving_kwargs(self):
923
+ kwargs = {}
924
+ if self.index is not None:
925
+ kwargs["index"] = self.index
926
+ if self.root_name is not None:
927
+ kwargs["root_name"] = self.root_name
928
+ if self.row_name is not None:
929
+ kwargs["row_name"] = self.row_name
930
+ if self.na_rep is not None:
931
+ kwargs["na_rep"] = self.na_rep
932
+ if self.attr_cols is not None:
933
+ kwargs["attr_cols"] = self.attr_cols
934
+ if self.elems_cols is not None:
935
+ kwargs["elems_cols"] = self.elems_cols
936
+ if self.namespaces is not None:
937
+ kwargs["namespaces"] = self.namespaces
938
+ if self.prefix is not None:
939
+ kwargs["prefix"] = self.prefix
940
+ if self.encoding is not None:
941
+ kwargs["encoding"] = self.encoding
942
+ if self.xml_declaration is not None:
943
+ kwargs["xml_declaration"] = self.xml_declaration
944
+ if self.pretty_print is not None:
945
+ kwargs["pretty_print"] = self.pretty_print
946
+ if self.parser is not None:
947
+ kwargs["parser"] = self.parser
948
+ if self.stylesheet is not None:
949
+ kwargs["stylesheet"] = self.stylesheet
950
+ if self.compression is not None:
951
+ kwargs["compression"] = self.compression
952
+ if self.storage_options is not None:
953
+ kwargs["storage_options"] = self.storage_options
954
+ return kwargs
955
+
956
+ def save_data(self, data: DATAFRAME_TYPE) -> dict[str, Any]:
957
+ data.to_xml(self.path_or_buffer, **self._get_saving_kwargs())
958
+ return utils.get_file_and_dataframe_metadata(self.path_or_buffer, data)
959
+
960
+ @classmethod
961
+ def name(cls) -> str:
962
+ return "xml"
963
+
964
+
965
+ @dataclasses.dataclass
966
+ class PandasHtmlReader(DataLoader):
967
+ """Class for loading/reading xml files with Pandas.
968
+ Maps to https://pandas.pydata.org/docs/reference/api/pandas.read_html.html
969
+ """
970
+
971
+ io: str | Path | BytesIO | BufferedReader
972
+ # kwargs
973
+ match: str | None = ".+"
974
+ flavor: str | Sequence | None = None
975
+ header: int | Sequence | None = None
976
+ index_col: int | Sequence | None = None
977
+ skiprows: int | Sequence | slice | None = None
978
+ attrs: dict[str, str] | None = None
979
+ parse_dates: bool | None = None
980
+ thousands: str | None = ","
981
+ encoding: str | None = None
982
+ decimal: str = "."
983
+ converters: dict[Any, Any] | None = None
984
+ na_values: Iterable = None
985
+ keep_default_na: bool = True
986
+ displayed_only: bool = True
987
+ extract_links: Literal["header", "footer", "body", "all"] | None = None
988
+ dtype_backend: Literal["pyarrow", "numpy_nullable"] = "numpy_nullable"
989
+ storage_options: dict[str, Any] | None = None
990
+
991
+ @classmethod
992
+ def applicable_types(cls) -> Collection[type]:
993
+ return [DATAFRAME_TYPE]
994
+
995
+ def _get_loading_kwargs(self) -> dict[str, Any]:
996
+ kwargs = {}
997
+ if self.match is not None:
998
+ kwargs["match"] = self.match
999
+ if self.flavor is not None:
1000
+ kwargs["flavor"] = self.flavor
1001
+ if self.header is not None:
1002
+ kwargs["header"] = self.header
1003
+ if self.index_col is not None:
1004
+ kwargs["index_col"] = self.index_col
1005
+ if self.skiprows is not None:
1006
+ kwargs["skiprows"] = self.skiprows
1007
+ if self.attrs is not None:
1008
+ kwargs["attrs"] = self.attrs
1009
+ if self.parse_dates is not None:
1010
+ kwargs["parse_dates"] = self.parse_dates
1011
+ if self.thousands is not None:
1012
+ kwargs["thousands"] = self.thousands
1013
+ if self.encoding is not None:
1014
+ kwargs["encoding"] = self.encoding
1015
+ if self.decimal is not None:
1016
+ kwargs["decimal"] = self.decimal
1017
+ if self.converters is not None:
1018
+ kwargs["converters"] = self.converters
1019
+ if self.na_values is not None:
1020
+ kwargs["na_values"] = self.na_values
1021
+ if self.keep_default_na is not None:
1022
+ kwargs["keep_default_na"] = self.keep_default_na
1023
+ if self.displayed_only is not None:
1024
+ kwargs["displayed_only"] = self.displayed_only
1025
+ if self.extract_links is not None:
1026
+ kwargs["extract_links"] = self.extract_links
1027
+ if Version(pd.__version__) >= Version("2.0") and self.dtype_backend is not None:
1028
+ kwargs["dtype_backend"] = self.dtype_backend
1029
+ if self.storage_options is not None:
1030
+ kwargs["storage_options"] = self.storage_options
1031
+
1032
+ return kwargs
1033
+
1034
+ def load_data(self, type: type) -> tuple[list[DATAFRAME_TYPE], dict[str, Any]]:
1035
+ # Loads the data and returns the df and metadata of the xml
1036
+ df = pd.read_html(self.io, **self._get_loading_kwargs())
1037
+ metadata = utils.get_file_and_dataframe_metadata(self.io, df[0])
1038
+ return df, metadata
1039
+
1040
+ @classmethod
1041
+ def name(cls) -> str:
1042
+ return "html"
1043
+
1044
+
1045
+ @dataclasses.dataclass
1046
+ class PandasHtmlWriter(DataSaver):
1047
+ """Class specifically to handle saving xml files/buffers with Pandas.
1048
+ Should map to https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_html.html#pandas.DataFrame.to_html
1049
+ """
1050
+
1051
+ buf: str | Path | StringIO | None = None
1052
+ # kwargs
1053
+ columns: list[str] | None = None
1054
+ col_space: str | int | list | dict | None = None
1055
+ header: bool | None = True
1056
+ index: bool | None = True
1057
+ na_rep: str | None = "NaN"
1058
+ formatters: list | tuple | dict | None = None
1059
+ float_format: str | None = None
1060
+ sparsify: bool | None = True
1061
+ index_names: bool | None = True
1062
+ justify: str = None
1063
+ max_rows: int | None = None
1064
+ max_cols: int | None = None
1065
+ show_dimensions: bool = False
1066
+ decimal: str = "."
1067
+ bold_rows: bool = True
1068
+ classes: str | list[str] | tuple | None = None
1069
+ escape: bool | None = True
1070
+ notebook: Literal[True, False] = False
1071
+ border: int = None
1072
+ table_id: str | None = None
1073
+ render_links: bool = False
1074
+ encoding: str | None = "utf-8"
1075
+
1076
+ @classmethod
1077
+ def applicable_types(cls) -> Collection[type]:
1078
+ return [DATAFRAME_TYPE]
1079
+
1080
+ def _get_saving_kwargs(self):
1081
+ kwargs = {}
1082
+ if self.columns is not None:
1083
+ kwargs["columns"] = self.columns
1084
+ if self.col_space is not None:
1085
+ kwargs["col_space"] = self.col_space
1086
+ if self.header is not None:
1087
+ kwargs["header"] = self.header
1088
+ if self.index is not None:
1089
+ kwargs["index"] = self.index
1090
+ if self.na_rep is not None:
1091
+ kwargs["na_rep"] = self.na_rep
1092
+ if self.formatters is not None:
1093
+ kwargs["formatters"] = self.formatters
1094
+ if self.float_format is not None:
1095
+ kwargs["float_format"] = self.float_format
1096
+ if self.sparsify is not None:
1097
+ kwargs["sparsify"] = self.sparsify
1098
+ if self.index_names is not None:
1099
+ kwargs["index_names"] = self.index_names
1100
+ if self.justify is not None:
1101
+ kwargs["justify"] = self.justify
1102
+ if self.max_rows is not None:
1103
+ kwargs["max_rows"] = self.max_rows
1104
+ if self.max_cols is not None:
1105
+ kwargs["max_cols"] = self.max_cols
1106
+ if self.show_dimensions is not None:
1107
+ kwargs["show_dimensions"] = self.show_dimensions
1108
+ if self.decimal is not None:
1109
+ kwargs["decimal"] = self.decimal
1110
+ if self.bold_rows is not None:
1111
+ kwargs["bold_rows"] = self.bold_rows
1112
+ if self.classes is not None:
1113
+ kwargs["classes"] = self.classes
1114
+ if self.escape is not None:
1115
+ kwargs["escape"] = self.escape
1116
+ if self.notebook is not None:
1117
+ kwargs["notebook"] = self.notebook
1118
+ if self.border is not None:
1119
+ kwargs["border"] = self.border
1120
+ if self.table_id is not None:
1121
+ kwargs["table_id"] = self.table_id
1122
+ if self.render_links is not None:
1123
+ kwargs["render_links"] = self.render_links
1124
+ if self.encoding is not None:
1125
+ kwargs["encoding"] = self.encoding
1126
+
1127
+ return kwargs
1128
+
1129
+ def save_data(self, data: DATAFRAME_TYPE) -> dict[str, Any]:
1130
+ data.to_html(self.buf, **self._get_saving_kwargs())
1131
+ return utils.get_file_and_dataframe_metadata(self.buf, data)
1132
+
1133
+ @classmethod
1134
+ def name(cls) -> str:
1135
+ return "html"
1136
+
1137
+
1138
+ @dataclasses.dataclass
1139
+ class PandasStataReader(DataLoader):
1140
+ """Class for loading/reading xml files with Pandas.
1141
+ Maps to https://pandas.pydata.org/docs/reference/api/pandas.read_stata.html#pandas.read_stata
1142
+ """
1143
+
1144
+ filepath_or_buffer: str | Path | BytesIO | BufferedReader
1145
+ # kwargs
1146
+ convert_dates: bool = True
1147
+ convert_categoricals: bool = True
1148
+ index_col: str | None = None
1149
+ convert_missing: bool = False
1150
+ preserve_dtypes: bool = True
1151
+ columns: Sequence | None = None
1152
+ order_categoricals: bool = True
1153
+ chunksize: int | None = None
1154
+ iterator: bool = False
1155
+ compression: dict[str, Any] | Literal["infer", "gzip", "bz2", "zip", "xz", "zstd", "tar"] = (
1156
+ "infer"
1157
+ )
1158
+ storage_options: dict[str, Any] | None = None
1159
+
1160
+ @classmethod
1161
+ def applicable_types(cls) -> Collection[type]:
1162
+ return [DATAFRAME_TYPE]
1163
+
1164
+ def _get_loading_kwargs(self) -> dict[str, Any]:
1165
+ kwargs = {}
1166
+ if self.convert_dates is not None:
1167
+ kwargs["convert_dates"] = self.convert_dates
1168
+ if self.convert_categoricals is not None:
1169
+ kwargs["convert_categoricals"] = self.convert_categoricals
1170
+ if self.index_col is not None:
1171
+ kwargs["index_col"] = self.index_col
1172
+ if self.convert_missing is not None:
1173
+ kwargs["convert_missing"] = self.convert_missing
1174
+ if self.preserve_dtypes is not None:
1175
+ kwargs["preserve_dtypes"] = self.preserve_dtypes
1176
+ if self.columns is not None:
1177
+ kwargs["columns"] = self.columns
1178
+ if self.order_categoricals is not None:
1179
+ kwargs["order_categoricals"] = self.order_categoricals
1180
+ if self.chunksize is not None:
1181
+ kwargs["chunksize"] = self.chunksize
1182
+ if self.iterator is not None:
1183
+ kwargs["iterator"] = self.iterator
1184
+ if self.compression is not None:
1185
+ kwargs["compression"] = self.compression
1186
+ if self.storage_options is not None:
1187
+ kwargs["storage_options"] = self.storage_options
1188
+
1189
+ return kwargs
1190
+
1191
+ def load_data(self, type: type) -> tuple[DATAFRAME_TYPE, dict[str, Any]]:
1192
+ # Loads the data and returns the df and metadata of the xml
1193
+ df = pd.read_stata(self.filepath_or_buffer, **self._get_loading_kwargs())
1194
+ metadata = utils.get_file_and_dataframe_metadata(self.filepath_or_buffer, df)
1195
+ return df, metadata
1196
+
1197
+ @classmethod
1198
+ def name(cls) -> str:
1199
+ return "stata"
1200
+
1201
+
1202
+ @dataclasses.dataclass
1203
+ class PandasStataWriter(DataSaver):
1204
+ """Class specifically to handle saving xml files/buffers with Pandas.
1205
+ Should map to https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_stata.html
1206
+ """
1207
+
1208
+ path: str | Path | BufferedReader = None
1209
+ # kwargs
1210
+ convert_dates: dict[Hashable, str] | None = None
1211
+ write_index: bool = True
1212
+ byteorder: str | None = None
1213
+ time_stamp: datetime | None = None
1214
+ data_label: str | None = None
1215
+ variable_labels: dict[Hashable, str] | None = None
1216
+ version: Literal[114, 117, 118, 119] = 114
1217
+ convert_strl: str | None = None
1218
+ compression: dict[str, Any] | Literal["infer", "gzip", "bz2", "zip", "xz", "zstd", "tar"] = (
1219
+ "infer"
1220
+ )
1221
+ storage_options: dict[str, Any] | None = None
1222
+ value_labels: dict[Hashable, str] | None = None
1223
+
1224
+ @classmethod
1225
+ def applicable_types(cls) -> Collection[type]:
1226
+ return [DATAFRAME_TYPE]
1227
+
1228
+ def _get_saving_kwargs(self):
1229
+ kwargs = {}
1230
+ if self.convert_dates is not None:
1231
+ kwargs["convert_dates"] = self.convert_dates
1232
+ if self.write_index is not None:
1233
+ kwargs["write_index"] = self.write_index
1234
+ if self.byteorder is not None:
1235
+ kwargs["byteorder"] = self.byteorder
1236
+ if self.time_stamp is not None:
1237
+ kwargs["time_stamp"] = self.time_stamp
1238
+ if self.data_label is not None:
1239
+ kwargs["data_label"] = self.data_label
1240
+ if self.variable_labels is not None:
1241
+ kwargs["variable_labels"] = self.variable_labels
1242
+ if self.version is not None:
1243
+ kwargs["version"] = self.version
1244
+ if self.convert_strl is not None and self.version == 117:
1245
+ kwargs["convert_strl"] = self.convert_strl
1246
+ if self.compression is not None:
1247
+ kwargs["compression"] = self.compression
1248
+ if self.storage_options is not None:
1249
+ kwargs["storage_options"] = self.storage_options
1250
+ if self.value_labels is not None:
1251
+ kwargs["value_labels"] = self.value_labels
1252
+
1253
+ return kwargs
1254
+
1255
+ def save_data(self, data: DATAFRAME_TYPE) -> dict[str, Any]:
1256
+ data.to_stata(self.path, **self._get_saving_kwargs())
1257
+ return utils.get_file_and_dataframe_metadata(self.path, data)
1258
+
1259
+ @classmethod
1260
+ def name(cls) -> str:
1261
+ return "stata"
1262
+
1263
+
1264
+ @dataclasses.dataclass
1265
+ class PandasFeatherReader(DataLoader):
1266
+ """Class for loading/reading feather files with Pandas.
1267
+ Maps to https://pandas.pydata.org/docs/reference/api/pandas.read_feather.html
1268
+ """
1269
+
1270
+ path: str | Path | BytesIO | BufferedReader
1271
+ # kwargs
1272
+ columns: Sequence | None = None
1273
+ use_threads: bool = True
1274
+ storage_options: dict[str, Any] | None = None
1275
+ dtype_backend: Literal["pyarrow", "numpy_nullable"] = "numpy_nullable"
1276
+
1277
+ @classmethod
1278
+ def applicable_types(cls) -> Collection[type]:
1279
+ return [DATAFRAME_TYPE]
1280
+
1281
+ def _get_loading_kwargs(self) -> dict[str, Any]:
1282
+ kwargs = {}
1283
+ if self.columns is not None:
1284
+ kwargs["columns"] = self.columns
1285
+ if self.use_threads is not None:
1286
+ kwargs["use_threads"] = self.use_threads
1287
+ if self.storage_options is not None:
1288
+ kwargs["storage_options"] = self.storage_options
1289
+ if Version(pd.__version__) >= Version("2.0") and self.dtype_backend is not None:
1290
+ kwargs["dtype_backend"] = self.dtype_backend
1291
+
1292
+ return kwargs
1293
+
1294
+ def load_data(self, type: type) -> tuple[DATAFRAME_TYPE, dict[str, Any]]:
1295
+ # Loads the data and returns the df and metadata of the xml
1296
+ df = pd.read_feather(self.path, **self._get_loading_kwargs())
1297
+ metadata = utils.get_file_and_dataframe_metadata(self.path, df)
1298
+ return df, metadata
1299
+
1300
+ @classmethod
1301
+ def name(cls) -> str:
1302
+ return "feather"
1303
+
1304
+
1305
+ @dataclasses.dataclass
1306
+ class PandasFeatherWriter(DataSaver):
1307
+ """Class specifically to handle saving xml files/buffers with Pandas.
1308
+ Should map to https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_feather.html
1309
+ Additional Parameters passed to: https://arrow.apache.org/docs/python/generated/pyarrow.feather.write_feather.html#pyarrow.feather.write_feather
1310
+
1311
+ Requires `lz4` https://pypi.org/project/lz4/
1312
+ """
1313
+
1314
+ path: str | Path | BytesIO | BufferedReader
1315
+ # kwargs
1316
+ dest: str | None = None
1317
+ compression: Literal["zstd", "lz4", "uncompressed"] = None
1318
+ compression_level: int | None = None
1319
+ chunksize: int | None = None
1320
+ version: int | None = 2
1321
+
1322
+ @classmethod
1323
+ def applicable_types(cls) -> Collection[type]:
1324
+ return [DATAFRAME_TYPE]
1325
+
1326
+ def _get_saving_kwargs(self):
1327
+ kwargs = {}
1328
+ if self.dest is not None:
1329
+ kwargs["dest"] = self.dest
1330
+ if self.compression is not None:
1331
+ kwargs["compression"] = self.compression
1332
+ if self.compression_level is not None:
1333
+ kwargs["compression_level"] = self.compression_level
1334
+ if self.chunksize is not None:
1335
+ kwargs["chunksize"] = self.chunksize
1336
+ if self.version is not None:
1337
+ kwargs["version"] = self.version
1338
+
1339
+ return kwargs
1340
+
1341
+ def save_data(self, data: DATAFRAME_TYPE) -> dict[str, Any]:
1342
+ data.to_feather(self.path, **self._get_saving_kwargs())
1343
+ return utils.get_file_and_dataframe_metadata(self.path, data)
1344
+
1345
+ @classmethod
1346
+ def name(cls) -> str:
1347
+ return "feather"
1348
+
1349
+
1350
+ @dataclasses.dataclass
1351
+ class PandasORCReader(DataLoader):
1352
+ """
1353
+ Class that handles reading ORC files and output a pandas DataFrame
1354
+ Maps to: https://pandas.pydata.org/docs/reference/api/pandas.read_orc.html#pandas.read_orc
1355
+ """
1356
+
1357
+ path: str | Path | BytesIO | BufferedReader
1358
+ # kwargs
1359
+ columns: list[str] | None = None
1360
+ dtype_backend: Literal["pyarrow", "numpy_nullable"] = "numpy_nullable"
1361
+ filesystem: FILESYSTEM_TYPE | None = None
1362
+
1363
+ @classmethod
1364
+ def applicable_types(cls) -> Collection[type]:
1365
+ return [DATAFRAME_TYPE]
1366
+
1367
+ def _get_loading_kwargs(self) -> dict[str, Any]:
1368
+ kwargs = {}
1369
+ if self.columns is not None:
1370
+ kwargs["columns"] = self.columns
1371
+ if self.dtype_backend is not None:
1372
+ kwargs["dtype_backend"] = self.dtype_backend
1373
+ if self.filesystem is not None:
1374
+ kwargs["filesystem"] = self.filesystem
1375
+
1376
+ return kwargs
1377
+
1378
+ def load_data(self, type: type) -> tuple[DATAFRAME_TYPE, dict[str, Any]]:
1379
+ # Loads the data and returns the df and metadata of the orc
1380
+ df = pd.read_orc(self.path, **self._get_loading_kwargs())
1381
+ metadata = utils.get_file_and_dataframe_metadata(self.path, df)
1382
+ return df, metadata
1383
+
1384
+ @classmethod
1385
+ def name(cls) -> str:
1386
+ return "orc"
1387
+
1388
+
1389
+ @dataclasses.dataclass
1390
+ class PandasORCWriter(DataSaver):
1391
+ """
1392
+ Class that handles writing DataFrames to ORC files.
1393
+ Maps to: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_orc.html
1394
+ """
1395
+
1396
+ path: str | Path | BytesIO | BufferedReader
1397
+ # kwargs
1398
+ engine: Literal["pyarrow"] = "pyarrow"
1399
+ index: bool | None = None
1400
+ engine_kwargs: dict[str, Any] | None | None = None
1401
+
1402
+ @classmethod
1403
+ def applicable_types(cls) -> Collection[type]:
1404
+ return [DATAFRAME_TYPE]
1405
+
1406
+ def _get_saving_kwargs(self):
1407
+ kwargs = {}
1408
+ if self.engine is not None:
1409
+ kwargs["engine"] = self.engine
1410
+ if self.index is not None:
1411
+ kwargs["index"] = self.index
1412
+ if self.engine_kwargs is not None:
1413
+ kwargs["engine_kwargs"] = self.engine_kwargs
1414
+
1415
+ return kwargs
1416
+
1417
+ def save_data(self, data: DATAFRAME_TYPE) -> dict[str, Any]:
1418
+ data.to_orc(self.path, **self._get_saving_kwargs())
1419
+ return utils.get_file_and_dataframe_metadata(self.path, data)
1420
+
1421
+ @classmethod
1422
+ def name(cls) -> str:
1423
+ return "orc"
1424
+
1425
+
1426
+ @dataclasses.dataclass
1427
+ class PandasExcelReader(DataLoader):
1428
+ """Class for reading Excel files and output a pandas DataFrame.
1429
+ Maps to https://pandas.pydata.org/docs/reference/api/pandas.read_excel.html
1430
+ """
1431
+
1432
+ path: str | Path | BytesIO | BufferedReader = None
1433
+ # kwargs:
1434
+ # inspect.get_type_hints doesn't work with type aliases,
1435
+ # which are used in pandas.read_excel.
1436
+ # So we have to list all the arguments in plain code.
1437
+ sheet_name: str | int | list[int | str] | None = 0
1438
+ header: int | Sequence | None = 0
1439
+ names: Sequence | None = None
1440
+ index_col: int | str | Sequence | None = None
1441
+ usecols: int | str | Sequence | Sequence | Callable[[str], bool] | None = None
1442
+ dtype: Dtype | dict[Hashable, Dtype] | None = None
1443
+ engine: Literal["xlrd", "openpyxl", "odf", "pyxlsb", "calamine"] | None = None
1444
+ converters: dict[str, Callable] | dict[int, Callable] | None = None
1445
+ true_values: Iterable | None = None
1446
+ false_values: Iterable | None = None
1447
+ skiprows: Sequence | int | Callable[[int], object] | None = None
1448
+ nrows: int | None = None
1449
+ na_values = None # in pandas.read_excel there are not type hints for na_values
1450
+ keep_default_na: bool = True
1451
+ na_filter: bool = True
1452
+ verbose: bool | None = None
1453
+ parse_dates: list[int | str] | dict[str, list[int | str]] | bool = False
1454
+ # date_parser: Optional[Callable] # date_parser is deprecated since pandas=2.0.0
1455
+ date_format: dict[Hashable, str] | str | None = None
1456
+ thousands: str | None = None
1457
+ decimal: str = "."
1458
+ comment: str | None = None
1459
+ skipfooter: int = 0
1460
+ storage_options: dict[str, Any] | None = None
1461
+ dtype_backend: Literal["pyarrow", "numpy_nullable"] = "numpy_nullable"
1462
+ engine_kwargs: dict[str, Any] | None = None
1463
+
1464
+ @classmethod
1465
+ def applicable_types(cls) -> Collection[type]:
1466
+ # Returns type for which data loader is available
1467
+ return [DATAFRAME_TYPE]
1468
+
1469
+ def _get_loading_kwargs(self) -> dict[str, Any]:
1470
+ # Puts kwargs in a dict
1471
+ kwargs = dataclasses.asdict(self)
1472
+
1473
+ # path corresponds to 'io' argument of pandas.read_excel,
1474
+ # but we send it separately
1475
+ del kwargs["path"]
1476
+
1477
+ # engine_kwargs appeared only in pandas >= 2.1
1478
+ # For compatibility with pandas 2.0 we remove engine_kwargs from kwargs if it's empty.
1479
+ if kwargs["engine_kwargs"] is None:
1480
+ del kwargs["engine_kwargs"]
1481
+
1482
+ return kwargs
1483
+
1484
+ def load_data(self, type_: type) -> tuple[DATAFRAME_TYPE, dict[str, Any]]:
1485
+ # Loads the data and returns the df and metadata of the excel file
1486
+ df = pd.read_excel(self.path, **self._get_loading_kwargs())
1487
+ metadata = utils.get_file_and_dataframe_metadata(self.path, df)
1488
+ return df, metadata
1489
+
1490
+ @classmethod
1491
+ def name(cls) -> str:
1492
+ return "excel"
1493
+
1494
+
1495
+ @dataclasses.dataclass
1496
+ class PandasExcelWriter(DataSaver):
1497
+ """Class that handles saving Excel files with pandas.
1498
+ Maps to https://pandas.pydata.org/docs/reference/api/pandas.ExcelWriter.html
1499
+ Additional parameters passed to https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_excel.html
1500
+ """
1501
+
1502
+ path: str | Path | BytesIO
1503
+ # kwargs:
1504
+ # inspect.get_type_hints doesn't work with type aliases,
1505
+ # which are used in pandas.DataFrame.to_excel.
1506
+ # So we have to list all the arguments in plain code
1507
+ sheet_name: str = "Sheet1"
1508
+ na_rep: str = ""
1509
+ float_format: str | None = None
1510
+ columns: Sequence | None = None
1511
+ header: Sequence | bool = True
1512
+ index: bool = True
1513
+ index_label: IndexLabel | None = None
1514
+ startrow: int = 0
1515
+ startcol: int = 0
1516
+ engine: Literal["openpyxl", "xlsxwriter"] | None = None
1517
+ merge_cells: bool = True
1518
+ inf_rep: str = "inf"
1519
+ freeze_panes: tuple[int, int] | None = None
1520
+ storage_options: dict[str, Any] | None = None
1521
+ engine_kwargs: dict[str, Any] | None = None
1522
+ mode: Literal["w", "a"] | None = "w"
1523
+ if_sheet_exists: Literal["error", "new", "replace", "overlay"] | None = None
1524
+ datetime_format: str = None
1525
+ date_format: str = None
1526
+
1527
+ @classmethod
1528
+ def applicable_types(cls) -> Collection[type]:
1529
+ return [DATAFRAME_TYPE]
1530
+
1531
+ def _get_saving_kwargs(self) -> dict[str, Any]:
1532
+ # Puts kwargs in a dict
1533
+ kwargs = dataclasses.asdict(self)
1534
+
1535
+ # Pass kwargs to ExcelWriter ONLY for kwargs which appear in both ExcelWriter and .to_excel()
1536
+ writer_kwarg_names = [
1537
+ "date_format",
1538
+ "datetime_format",
1539
+ "if_sheet_exists",
1540
+ "mode",
1541
+ "engine_kwargs",
1542
+ "engine",
1543
+ "storage_options",
1544
+ ]
1545
+
1546
+ # path corresponds to 'excel_writer' argument of pandas.DataFrame.to_excel,
1547
+ # but we send it separately
1548
+ del kwargs["path"]
1549
+
1550
+ # engine_kwargs appeared only in pandas >= 2.1
1551
+ # For compatibility with pandas 2.0 we remove engine_kwargs from kwargs if it's empty.
1552
+ if kwargs["engine_kwargs"] is None:
1553
+ del kwargs["engine_kwargs"]
1554
+ writer_kwarg_names.remove("engine_kwargs")
1555
+
1556
+ # seperate kwargs for ExcelWriter and to_excel() invocation
1557
+ writer_kwargs = {k: kwargs[k] for k in writer_kwarg_names}
1558
+ to_excel_kwargs = {k: kwargs[k] for k in (kwargs.keys() - set(writer_kwarg_names))}
1559
+
1560
+ return writer_kwargs, to_excel_kwargs
1561
+
1562
+ def save_data(self, data: DATAFRAME_TYPE) -> dict[str, Any]:
1563
+ writer_kwargs, to_excel_kwargs = self._get_saving_kwargs()
1564
+
1565
+ with pd.ExcelWriter(self.path, **writer_kwargs) as writer:
1566
+ data.to_excel(writer, **to_excel_kwargs)
1567
+ return utils.get_file_and_dataframe_metadata(self.path, data)
1568
+
1569
+ @classmethod
1570
+ def name(cls) -> str:
1571
+ return "excel"
1572
+
1573
+
1574
+ @dataclasses.dataclass
1575
+ class PandasTableReader(DataLoader):
1576
+ """Class for loading/reading table files with Pandas.
1577
+ Maps to https://pandas.pydata.org/docs/reference/api/pandas.read_table.html
1578
+ """
1579
+
1580
+ filepath_or_buffer: str | Path | BytesIO | BufferedReader
1581
+ # kwargs
1582
+ sep: str | None = None
1583
+ delimiter: str | None = None
1584
+ header: int | Sequence | str | None = "infer"
1585
+ names: Sequence | None = None
1586
+ index_col: int | str | Sequence | None = None
1587
+ usecols: Sequence | None = None
1588
+ dtype: Dtype | dict[Hashable, Dtype] | None = None
1589
+ engine: Literal["c", "python", "pyarrow"] | None = None
1590
+ converters: dict[Hashable, Callable] | None = None
1591
+ true_values: Iterable | None = None
1592
+ false_values: Iterable | None = None
1593
+ skipinitialspace: bool = False
1594
+ skiprows: list[int] | int | list[Callable] | None = None
1595
+ skipfooter: int = 0
1596
+ nrows: int | None = None
1597
+ na_values: Hashable | Iterable | dict[Hashable, Iterable] | None = None
1598
+ keep_default_na: bool = True
1599
+ na_filter: bool = True
1600
+ verbose: bool | None = None
1601
+ skip_blank_lines: bool = True
1602
+ parse_dates: list[int | str] | dict[str, list[int | str]] | bool = False
1603
+ infer_datetime_format: bool = False
1604
+ keep_date_col: bool | None = None
1605
+ date_parser: Callable | None = None
1606
+ date_format: str | str | None = None
1607
+ dayfirst: bool = False
1608
+ cache_dates: bool = True
1609
+ iterator: bool = False
1610
+ chunksize: int | None = None
1611
+ compression: str | dict = "infer"
1612
+ thousands: str | None = None
1613
+ decimal: str = "."
1614
+ lineterminator: str | None = None
1615
+ quotechar: str | None = '"'
1616
+ quoting: int = 0
1617
+ doublequote: bool = True
1618
+ escapechar: str | None = None
1619
+ comment: str | None = None
1620
+ encoding: str | None = None
1621
+ encoding_errors: str | None = "strict"
1622
+ dialect: str | None = None
1623
+ on_bad_lines: Literal["error", "warn", "skip"] | Callable = "error"
1624
+ delim_whitespace: bool | None = None
1625
+ low_memory: bool = True
1626
+ memory_map: bool = False
1627
+ float_precision: Literal["high", "legacy", "round_trip"] | None = None
1628
+ storage_options: dict | None = None
1629
+ dtype_backend: Literal["numpy_nullable", "pyarrow"] = "numpy_nullable"
1630
+
1631
+ @classmethod
1632
+ def applicable_types(cls) -> Collection[type]:
1633
+ return [DATAFRAME_TYPE]
1634
+
1635
+ def _get_loading_kwargs(self) -> dict[str, Any]:
1636
+ # Puts kwargs in a dict
1637
+ kwargs = dataclasses.asdict(self)
1638
+
1639
+ # filepath_or_buffer corresponds to 'filepath_or_buffer' argument of pandas.read_table,
1640
+ # but we send it separately
1641
+ del kwargs["filepath_or_buffer"]
1642
+
1643
+ return kwargs
1644
+
1645
+ def load_data(self, type_: type) -> tuple[DATAFRAME_TYPE, dict[str, Any]]:
1646
+ # Loads the data and returns the df and metadata of the table
1647
+ df = pd.read_table(self.filepath_or_buffer, **self._get_loading_kwargs())
1648
+ metadata = utils.get_file_and_dataframe_metadata(self.filepath_or_buffer, df)
1649
+ return df, metadata
1650
+
1651
+ @classmethod
1652
+ def name(cls) -> str:
1653
+ return "table"
1654
+
1655
+
1656
+ @dataclasses.dataclass
1657
+ class PandasFWFReader(DataLoader):
1658
+ """Class for loading/reading fixed-width formatted files with Pandas.
1659
+ Maps to https://pandas.pydata.org/docs/reference/api/pandas.read_fwf.html
1660
+ """
1661
+
1662
+ filepath_or_buffer: str | Path | BytesIO | BufferedReader
1663
+ # kwargs
1664
+ colspecs: str | list[tuple[int, int]] | tuple[int, int] = "infer"
1665
+ widths: list[int] | None = None
1666
+ infer_nrows: int = 100
1667
+ dtype_backend: Literal["numpy_nullable", "pyarrow"] = "numpy_nullable"
1668
+
1669
+ @classmethod
1670
+ def applicable_types(cls) -> Collection[type]:
1671
+ return [DATAFRAME_TYPE]
1672
+
1673
+ def _get_loading_kwargs(self) -> dict[str, Any]:
1674
+ # Puts kwargs in a dict
1675
+ kwargs = dataclasses.asdict(self)
1676
+
1677
+ # filepath_or_buffer corresponds to 'filepath_or_buffer' argument of pandas.read_fwf,
1678
+ # but we send it separately
1679
+ del kwargs["filepath_or_buffer"]
1680
+
1681
+ return kwargs
1682
+
1683
+ def load_data(self, type_: type) -> tuple[DATAFRAME_TYPE, dict[str, Any]]:
1684
+ # Loads the data and returns the df and metadata of the fwf file
1685
+ df = pd.read_fwf(self.filepath_or_buffer, **self._get_loading_kwargs())
1686
+ metadata = utils.get_file_and_dataframe_metadata(self.filepath_or_buffer, df)
1687
+ return df, metadata
1688
+
1689
+ @classmethod
1690
+ def name(cls) -> str:
1691
+ return "fwf"
1692
+
1693
+
1694
+ @dataclasses.dataclass
1695
+ class PandasSPSSReader(DataLoader):
1696
+ """Class for loading/reading spss files with Pandas.
1697
+ Maps to https://pandas.pydata.org/docs/reference/api/pandas.read_spss.html
1698
+ """
1699
+
1700
+ path: str | Path
1701
+ # kwargs
1702
+ usecols: list[Hashable] | Callable[[str], bool] | None = None
1703
+ convert_categoricals: bool = True
1704
+ dtype_backend: Literal["pyarrow", "numpy_nullable"] = "numpy_nullable"
1705
+
1706
+ @classmethod
1707
+ def applicable_types(cls) -> Collection[type]:
1708
+ return [DATAFRAME_TYPE]
1709
+
1710
+ def _get_loading_kwargs(self) -> dict[str, Any]:
1711
+ # Puts kwargs in a dict
1712
+ kwargs = dataclasses.asdict(self)
1713
+
1714
+ # path corresponds to 'io' argument of pandas.read_spss,
1715
+ # but we send it separately
1716
+ del kwargs["path"]
1717
+
1718
+ return kwargs
1719
+
1720
+ def load_data(self, type_: type) -> tuple[DATAFRAME_TYPE, dict[str, Any]]:
1721
+ # Loads the data and returns the df and metadata of the spss file
1722
+ df = pd.read_spss(self.path, **self._get_loading_kwargs())
1723
+ metadata = utils.get_file_and_dataframe_metadata(self.path, df)
1724
+ return df, metadata
1725
+
1726
+ @classmethod
1727
+ def name(cls) -> str:
1728
+ return "spss"
1729
+
1730
+
1731
+ def register_data_loaders():
1732
+ """Function to register the data loaders for this extension."""
1733
+ for loader in [
1734
+ PandasCSVReader,
1735
+ PandasCSVWriter,
1736
+ PandasParquetReader,
1737
+ PandasParquetWriter,
1738
+ PandasPickleReader,
1739
+ PandasPickleWriter,
1740
+ PandasJsonReader,
1741
+ PandasJsonWriter,
1742
+ PandasSqlReader,
1743
+ PandasSqlWriter,
1744
+ PandasXmlReader,
1745
+ PandasXmlWriter,
1746
+ PandasHtmlReader,
1747
+ PandasHtmlWriter,
1748
+ PandasStataReader,
1749
+ PandasStataWriter,
1750
+ PandasFeatherReader,
1751
+ PandasFeatherWriter,
1752
+ PandasORCWriter,
1753
+ PandasORCReader,
1754
+ PandasExcelWriter,
1755
+ PandasExcelReader,
1756
+ PandasTableReader,
1757
+ PandasFWFReader,
1758
+ PandasSPSSReader,
1759
+ ]:
1760
+ registry.register_adapter(loader)
1761
+
1762
+
1763
+ register_data_loaders()