Repository: pandas-dev/pandas (python)

Issue:
ENH: Add SAS7BDATReader and XportReader to `pandas/api/typing/__init__.py`
### Feature Type

- [X] Adding new functionality to pandas

- [ ] Changing existing functionality in pandas

- [ ] Removing existing functionality in pandas


### Problem Description

These classes can be returned by `read_sas(..., iterator=True/chunksize=...)`

### Feature Description

Adding `from pandas.io.sas.sas_xport import XportReader` and `from pandas.io.sas.sas7bdat import SAS7BDATReader` to `pandas/api/typing/__init__.py`

### Alternative Solutions

None

### Additional Context

_No response_

Pull request description:
TYP: export SASReader in pandas.api.typing
- [x] closes #55689 (Replace xxxx with the GitHub issue number)
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [x] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.


Patch:
diff --git a/ci/code_checks.sh b/ci/code_checks.sh
index d1cdff8f7f..ad12458ad6 100755
--- a/ci/code_checks.sh
+++ b/ci/code_checks.sh
@@ -109,7 +109,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then
         -i "pandas.DataFrame.var PR01,RT03,SA01" \
         -i "pandas.DatetimeIndex.ceil SA01" \
         -i "pandas.DatetimeIndex.date SA01" \
-        -i "pandas.DatetimeIndex.day SA01" \
         -i "pandas.DatetimeIndex.day_of_year SA01" \
         -i "pandas.DatetimeIndex.dayofyear SA01" \
         -i "pandas.DatetimeIndex.floor SA01" \
@@ -118,8 +117,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then
         -i "pandas.DatetimeIndex.indexer_between_time RT03" \
         -i "pandas.DatetimeIndex.inferred_freq SA01" \
         -i "pandas.DatetimeIndex.is_leap_year SA01" \
-        -i "pandas.DatetimeIndex.microsecond SA01" \
-        -i "pandas.DatetimeIndex.nanosecond SA01" \
         -i "pandas.DatetimeIndex.quarter SA01" \
         -i "pandas.DatetimeIndex.round SA01" \
         -i "pandas.DatetimeIndex.snap PR01,RT03,SA01" \
@@ -296,7 +293,6 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then
         -i "pandas.Series.dt.ceil PR01,PR02,SA01" \
         -i "pandas.Series.dt.components SA01" \
         -i "pandas.Series.dt.date SA01" \
-        -i "pandas.Series.dt.day SA01" \
         -i "pandas.Series.dt.day_name PR01,PR02" \
         -i "pandas.Series.dt.day_of_year SA01" \
         -i "pandas.Series.dt.dayofyear SA01" \
@@ -306,10 +302,8 @@ if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then
         -i "pandas.Series.dt.floor PR01,PR02,SA01" \
         -i "pandas.Series.dt.freq GL08" \
         -i "pandas.Series.dt.is_leap_year SA01" \
-        -i "pandas.Series.dt.microsecond SA01" \
         -i "pandas.Series.dt.microseconds SA01" \
         -i "pandas.Series.dt.month_name PR01,PR02" \
-        -i "pandas.Series.dt.nanosecond SA01" \
         -i "pandas.Series.dt.nanoseconds SA01" \
         -i "pandas.Series.dt.normalize PR01" \
         -i "pandas.Series.dt.quarter SA01" \
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 8618d7d525..c817e09b3b 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -29,6 +29,7 @@ enhancement2
 Other enhancements
 ^^^^^^^^^^^^^^^^^^
 - :class:`pandas.api.typing.FrozenList` is available for typing the outputs of :attr:`MultiIndex.names`, :attr:`MultiIndex.codes` and :attr:`MultiIndex.levels` (:issue:`58237`)
+- :class:`pandas.api.typing.SASReader` is available for typing the output of :func:`read_sas` (:issue:`55689`)
 - :func:`DataFrame.to_excel` now raises an ``UserWarning`` when the character count in a cell exceeds Excel's limitation of 32767 characters (:issue:`56954`)
 - :func:`read_stata` now returns ``datetime64`` resolutions better matching those natively stored in the stata format (:issue:`55642`)
 - :meth:`Styler.set_tooltips` provides alternative method to storing tooltips by using title attribute of td elements. (:issue:`56981`)
diff --git a/pandas/api/typing/__init__.py b/pandas/api/typing/__init__.py
index df6392bf69..c58fa0f085 100644
--- a/pandas/api/typing/__init__.py
+++ b/pandas/api/typing/__init__.py
@@ -30,6 +30,7 @@ from pandas.core.window import (
 # TODO: Can't import Styler without importing jinja2
 # from pandas.io.formats.style import Styler
 from pandas.io.json._json import JsonReader
+from pandas.io.sas.sasreader import SASReader
 from pandas.io.stata import StataReader
 
 __all__ = [
@@ -49,6 +50,7 @@ __all__ = [
     "RollingGroupby",
     "SeriesGroupBy",
     "StataReader",
+    "SASReader",
     # See TODO above
     # "Styler",
     "TimedeltaIndexResamplerGroupby",
diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py
index 5d0dfc67bd..7704c99141 100644
--- a/pandas/core/arrays/datetimes.py
+++ b/pandas/core/arrays/datetimes.py
@@ -1597,6 +1597,12 @@ default 'raise'
         """
         The day of the datetime.
 
+        See Also
+        --------
+        DatetimeIndex.year: The year of the datetime.
+        DatetimeIndex.month: The month as January=1, December=12.
+        DatetimeIndex.hour: The hours of the datetime.
+
         Examples
         --------
         >>> datetime_series = pd.Series(
@@ -1706,6 +1712,11 @@ default 'raise'
         """
         The microseconds of the datetime.
 
+        See Also
+        --------
+        DatetimeIndex.second: The seconds of the datetime.
+        DatetimeIndex.nanosecond: The nanoseconds of the datetime.
+
         Examples
         --------
         >>> datetime_series = pd.Series(
@@ -1729,6 +1740,11 @@ default 'raise'
         """
         The nanoseconds of the datetime.
 
+        See Also
+        --------
+        DatetimeIndex.second: The seconds of the datetime.
+        DatetimeIndex.microsecond: The microseconds of the datetime.
+
         Examples
         --------
         >>> datetime_series = pd.Series(
diff --git a/pandas/io/sas/sas7bdat.py b/pandas/io/sas/sas7bdat.py
index 6a392a0f02..25257d5fcc 100644
--- a/pandas/io/sas/sas7bdat.py
+++ b/pandas/io/sas/sas7bdat.py
@@ -16,7 +16,6 @@ Reference for binary data compression:
 
 from __future__ import annotations
 
-from collections import abc
 from datetime import datetime
 import sys
 from typing import TYPE_CHECKING
@@ -45,7 +44,7 @@ from pandas import (
 
 from pandas.io.common import get_handle
 import pandas.io.sas.sas_constants as const
-from pandas.io.sas.sasreader import ReaderBase
+from pandas.io.sas.sasreader import SASReader
 
 if TYPE_CHECKING:
     from pandas._typing import (
@@ -116,7 +115,7 @@ class _Column:
 
 
 # SAS7BDAT represents a SAS data file in SAS7BDAT format.
-class SAS7BDATReader(ReaderBase, abc.Iterator):
+class SAS7BDATReader(SASReader):
     """
     Read SAS files in SAS7BDAT format.
 
diff --git a/pandas/io/sas/sas_xport.py b/pandas/io/sas/sas_xport.py
index adba9bf117..89dbdab64c 100644
--- a/pandas/io/sas/sas_xport.py
+++ b/pandas/io/sas/sas_xport.py
@@ -10,7 +10,6 @@ https://support.sas.com/content/dam/SAS/support/en/technical-papers/record-layou
 
 from __future__ import annotations
 
-from collections import abc
 from datetime import datetime
 import struct
 from typing import TYPE_CHECKING
@@ -24,7 +23,7 @@ from pandas.util._exceptions import find_stack_level
 import pandas as pd
 
 from pandas.io.common import get_handle
-from pandas.io.sas.sasreader import ReaderBase
+from pandas.io.sas.sasreader import SASReader
 
 if TYPE_CHECKING:
     from pandas._typing import (
@@ -252,7 +251,7 @@ def _parse_float_vec(vec):
     return ieee
 
 
-class XportReader(ReaderBase, abc.Iterator):
+class XportReader(SASReader):
     __doc__ = _xport_reader_doc
 
     def __init__(
diff --git a/pandas/io/sas/sasreader.py b/pandas/io/sas/sasreader.py
index 69d9118633..12d698a4f7 100644
--- a/pandas/io/sas/sasreader.py
+++ b/pandas/io/sas/sasreader.py
@@ -8,6 +8,7 @@ from abc import (
     ABC,
     abstractmethod,
 )
+from collections.abc import Iterator
 from typing import (
     TYPE_CHECKING,
     overload,
@@ -33,9 +34,9 @@ if TYPE_CHECKING:
     from pandas import DataFrame
 
 
-class ReaderBase(ABC):
+class SASReader(Iterator["DataFrame"], ABC):
     """
-    Protocol for XportReader and SAS7BDATReader classes.
+    Abstract class for XportReader and SAS7BDATReader.
     """
 
     @abstractmethod
@@ -66,7 +67,7 @@ def read_sas(
     chunksize: int = ...,
     iterator: bool = ...,
     compression: CompressionOptions = ...,
-) -> ReaderBase: ...
+) -> SASReader: ...
 
 
 @overload
@@ -79,7 +80,7 @@ def read_sas(
     chunksize: None = ...,
     iterator: bool = ...,
     compression: CompressionOptions = ...,
-) -> DataFrame | ReaderBase: ...
+) -> DataFrame | SASReader: ...
 
 
 @doc(decompression_options=_shared_docs["decompression_options"] % "filepath_or_buffer")
@@ -92,7 +93,7 @@ def read_sas(
     chunksize: int | None = None,
     iterator: bool = False,
     compression: CompressionOptions = "infer",
-) -> DataFrame | ReaderBase:
+) -> DataFrame | SASReader:
     """
     Read SAS files stored as either XPORT or SAS7BDAT format files.
 
@@ -145,7 +146,7 @@ def read_sas(
                 f"unable to infer format of SAS file from filename: {fname!r}"
             )
 
-    reader: ReaderBase
+    reader: SASReader
     if format.lower() == "xport":
         from pandas.io.sas.sas_xport import XportReader
 


Test patch:
diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py
index 0f2a641d13..b23876d928 100644
--- a/pandas/tests/api/test_api.py
+++ b/pandas/tests/api/test_api.py
@@ -267,6 +267,7 @@ class TestApi(Base):
         "RollingGroupby",
         "SeriesGroupBy",
         "StataReader",
+        "SASReader",
         "TimedeltaIndexResamplerGroupby",
         "TimeGrouper",
         "Window",