1"""
2Read SAS sas7bdat or xport files.
3"""
4
5from __future__ import annotations
6
7from abc import (
8 ABC,
9 abstractmethod,
10)
11from collections.abc import Iterator
12from typing import (
13 TYPE_CHECKING,
14 Self,
15 overload,
16)
17
18from pandas._libs import lib
19from pandas.util._decorators import set_module
20
21from pandas.io.common import stringify_path
22
23if TYPE_CHECKING:
24 from collections.abc import Hashable
25 from types import TracebackType
26
27 from pandas._typing import (
28 CompressionOptions,
29 FilePath,
30 ReadBuffer,
31 )
32
33 from pandas import DataFrame
34
35
36@set_module("pandas.api.typing")
37class SASReader(Iterator["DataFrame"], ABC):
38 """
39 Abstract class for XportReader and SAS7BDATReader.
40 """
41
42 @abstractmethod
43 def read(self, nrows: int | None = None) -> DataFrame: ...
44
45 @abstractmethod
46 def close(self) -> None: ...
47
48 def __enter__(self) -> Self:
49 return self
50
51 def __exit__(
52 self,
53 exc_type: type[BaseException] | None,
54 exc_value: BaseException | None,
55 traceback: TracebackType | None,
56 ) -> None:
57 self.close()
58
59
60@overload
61def read_sas(
62 filepath_or_buffer: FilePath | ReadBuffer[bytes],
63 *,
64 format: str | None = ...,
65 index: Hashable | None = ...,
66 encoding: str | lib.NoDefault | None = ...,
67 chunksize: int = ...,
68 iterator: bool = ...,
69 compression: CompressionOptions = ...,
70) -> SASReader: ...
71
72
73@overload
74def read_sas(
75 filepath_or_buffer: FilePath | ReadBuffer[bytes],
76 *,
77 format: str | None = ...,
78 index: Hashable | None = ...,
79 encoding: str | lib.NoDefault | None = ...,
80 chunksize: None = ...,
81 iterator: bool = ...,
82 compression: CompressionOptions = ...,
83) -> DataFrame | SASReader: ...
84
85
86@set_module("pandas")
87def read_sas(
88 filepath_or_buffer: FilePath | ReadBuffer[bytes],
89 *,
90 format: str | None = None,
91 index: Hashable | None = None,
92 encoding: str | lib.NoDefault | None = lib.no_default,
93 chunksize: int | None = None,
94 iterator: bool = False,
95 compression: CompressionOptions = "infer",
96) -> DataFrame | SASReader:
97 """
98 Read SAS files stored as either XPORT or SAS7BDAT format files.
99
100 The file format is inferred from the file extension if not explicitly
101 specified. Supports both full reads and chunked iteration.
102
103 Parameters
104 ----------
105 filepath_or_buffer : str, path object, or file-like object
106 String, path object (implementing ``os.PathLike[str]``), or file-like
107 object implementing a binary ``read()`` function. The string could be
108 a URL. Valid URL schemes include http, ftp, s3, and file. For file
109 URLs, a host is expected. A local file could be:
110 ``file://localhost/path/to/table.sas7bdat``.
111
112 Certain URL schemes may require additional packages. For example, S3
113 URLs require the ``s3fs`` library. See
114 :ref:`install.optional_dependencies` for a full list.
115 format : str {'xport', 'sas7bdat'} or None
116 If None, file format is inferred from file extension. If 'xport' or
117 'sas7bdat', uses the corresponding format.
118 index : identifier of index column, defaults to None
119 Identifier of column that should be used as index of the DataFrame.
120 encoding : str, 'infer' or None, default is None
121 Encoding for text data. If None, text data are stored as raw bytes.
122 ``'infer'`` decodes SAS7BDAT files using the encoding recorded in the
123 file header, falling back to latin-1 if the header does not name one
124 pandas recognizes; XPORT files record no encoding, so ``'infer'``
125 means ISO-8859-1 there.
126
127 .. deprecated:: 3.1.0
128 The default will change from ``None`` to ``'infer'``, so text data
129 will be decoded rather than returned as bytes. Pass ``None``
130 explicitly to keep the current behavior.
131 chunksize : int
132 Read file `chunksize` lines at a time, returns iterator.
133 iterator : bool, defaults to False
134 If True, returns an iterator for reading the file incrementally.
135 compression : str or dict, default 'infer'
136 For on-the-fly decompression of on-disk data. If 'infer' and
137 'filepath_or_buffer' is path-like, then detect compression from the
138 following extensions: '.gz', '.bz2', '.zip', '.xz', '.zst', '.tar',
139 '.tar.gz', '.tar.xz' or '.tar.bz2' (otherwise no compression).
140 Set to ``None`` for no decompression.
141 Can also be a dict with key ``'method'`` set to one of {``'zip'``,
142 ``'gzip'``, ``'bz2'``, ``'zstd'``, ``'xz'``, ``'tar'``} and other
143 key-value pairs are forwarded to ``zipfile.ZipFile``,
144 ``gzip.GzipFile``, ``bz2.BZ2File``, ``zstandard.ZstdCompressor``,
145 ``lzma.LZMAFile`` or ``tarfile.TarFile``, respectively.
146 As an example, the following could be passed for faster compression
147 and to create a reproducible gzip archive:
148 ``compression={'method': 'gzip', 'compresslevel': 1, 'mtime': 1}``.
149
150 Returns
151 -------
152 DataFrame, SAS7BDATReader, or XportReader
153 DataFrame if iterator=False and chunksize=None, else SAS7BDATReader
154 or XportReader, file format is inferred from file extension.
155
156 See Also
157 --------
158 read_csv : Read a comma-separated values (csv) file into a DataFrame.
159 read_excel : Read an Excel file into a pandas DataFrame.
160 read_spss : Read an SPSS file into a pandas DataFrame.
161 read_orc : Load an ORC object into a pandas DataFrame.
162 read_feather : Load a feather-format object into a pandas DataFrame.
163
164 Examples
165 --------
166 >>> df = pd.read_sas("sas_data.sas7bdat") # doctest: +SKIP
167 """
168 if format is None:
169 buffer_error_msg = (
170 "If this is a buffer object rather "
171 "than a string name, you must specify a format string"
172 )
173 filepath_or_buffer = stringify_path(filepath_or_buffer)
174 if not isinstance(filepath_or_buffer, str):
175 raise ValueError(buffer_error_msg)
176 fname = filepath_or_buffer.lower()
177 if ".xpt" in fname:
178 format = "xport"
179 elif ".sas7bdat" in fname:
180 format = "sas7bdat"
181 else:
182 raise ValueError(
183 f"unable to infer format of SAS file from filename: {fname!r}"
184 )
185
186 reader: SASReader
187 if format.lower() == "xport":
188 from pandas.io.sas.sas_xport import XportReader
189
190 reader = XportReader(
191 filepath_or_buffer,
192 index=index,
193 encoding=encoding,
194 chunksize=chunksize,
195 compression=compression,
196 )
197 elif format.lower() == "sas7bdat":
198 from pandas.io.sas.sas7bdat import SAS7BDATReader
199
200 reader = SAS7BDATReader(
201 filepath_or_buffer,
202 index=index,
203 encoding=encoding,
204 chunksize=chunksize,
205 compression=compression,
206 )
207 else:
208 raise ValueError("unknown SAS format")
209
210 if iterator or chunksize:
211 return reader
212
213 with reader:
214 return reader.read()