Skip to content

vayu (top-level)

The names re-exported from vayu/__init__.py. Most users should import from here:

from vayu import (
    Cache, CacheMissError, FileCache, MemoryCache, Pickler, Jsoner, Serializer,
    Interval, TimeWindow,
    L, configure_logging,
    mem_cache, mem_cached,
    retry, add_jitter, group, camel_or_space_to_snake,
    time_now, timeit, ts_ms, from_timestamp, from_z_string,
    local_datetime, utc_datetime, min_time, max_time, epoch_time,
    to_human_readable_time,
    install_pandas_extensions,
)

Vayu: async-first utilities for caching, time, parallelism, and pandas.

The base install keeps dependencies light. Pandas/plotly/numpy-backed helpers live under vayu.pandas_utils, vayu.plotly_utils, and vayu.statistics and require the vayulib[data] extra.

retry

retry(
    exception_to_check: Union[_ExcType, Tuple[_ExcType]],
    tries: int = 4,
    delay: float = 1,
    backoff: float = 2,
    logger: Logger = None,
)

Retry calling the decorated function using an exponential backoff. Asyncio compatible.

Parameters:

Name Type Description Default
exception_to_check Union[_ExcType, Tuple[_ExcType]]

the exception to check. Maybe a tuple of exceptions to check.

required
tries int

number of times to try (not retry) before giving up

4
delay float

initial delay between retries in seconds

1
backoff float

backoff multiplier e.g. value of 2 will double the delay for each retry

2
logger Logger

logger to use. If None, print.

None
Source code in vayu/common.py
def __init__(
    self,
    exception_to_check: Union[_ExcType, Tuple[_ExcType]],
    tries: int = 4,
    delay: float = 1,
    backoff: float = 2,
    logger: Logger = None,
):
    """
    Args:
        exception_to_check: the exception to check. Maybe a tuple of exceptions to check.
        tries: number of times to try (not retry) before giving up
        delay: initial delay between retries in seconds
        backoff: backoff multiplier e.g. value of 2 will double the delay for each retry
        logger: logger to use. If None, print.
    """
    self._tries = tries
    self._delay = delay
    self._backoff = backoff
    self._ignore_exc_types = exception_to_check
    self._log_fn = logger.warning if logger else print

Interval dataclass

Interval(start: Comparable, end: Comparable)

A closed interval [start, end] with union / intersection / containment algebra.

Works on any comparable type — int, float, date, datetime. Subclasses (e.g. TimeWindow) flow through operators automatically because the dunder methods use self.__class__.

Single-point intervals (start == end) are allowed so that touching endpoints like [1, 3] & [3, 5] produce [3, 3] instead of raising.

Attributes:

Name Type Description
start Comparable

Lower bound (inclusive).

end Comparable

Upper bound (inclusive).

range property

range: Comparable

The span of the interval (end - start).

intersects

intersects(other: Interval) -> bool

Return True if this interval and other share at least one point.

Source code in vayu/interval.py
def intersects(self, other: "Interval") -> bool:
    """Return True if this interval and ``other`` share at least one point."""
    return self.start <= other.end and other.start <= self.end

intersection

intersection(other: Interval) -> Interval

Return the overlapping sub-interval, or raise if there's no overlap.

Raises:

Type Description
ValueError

If the intervals don't overlap.

Source code in vayu/interval.py
def intersection(self, other: "Interval") -> "Interval":
    """Return the overlapping sub-interval, or raise if there's no overlap.

    Raises:
        ValueError: If the intervals don't overlap.
    """
    if not self.intersects(other):
        raise ValueError("No intersection possible")

    return replace(self, start=max(self.start, other.start), end=min(self.end, other.end))

union classmethod

union(
    intervals: Sequence[Interval], allow_gaps: bool = False
) -> Interval

Return the smallest interval that contains all inputs.

Parameters:

Name Type Description Default
intervals Sequence[Interval]

One or more intervals to union.

required
allow_gaps bool

If False (the default), raise ValueError when the inputs don't collectively touch or overlap. If True, return the bounding envelope even when gaps exist.

False

Raises:

Type Description
AssertionError

If intervals is empty.

ValueError

If allow_gaps is False and there is a gap between two inputs.

Source code in vayu/interval.py
@classmethod
def union(cls, intervals: Sequence["Interval"], allow_gaps: bool = False) -> "Interval":
    """Return the smallest interval that contains all inputs.

    Args:
        intervals: One or more intervals to union.
        allow_gaps: If False (the default), raise ``ValueError`` when the
            inputs don't collectively touch or overlap. If True, return the
            bounding envelope even when gaps exist.

    Raises:
        AssertionError: If ``intervals`` is empty.
        ValueError: If ``allow_gaps`` is False and there is a gap between
            two inputs.
    """
    # TODO: Fix this: should have a signature same as intersection.
    if len(intervals) == 0:
        raise AssertionError("Length of intervals should at least be 1")
    if len(intervals) == 1:
        return intervals[0]

    intervals = sorted(intervals, key=attrgetter("start"))
    if not allow_gaps:
        for i in range(len(intervals) - 1):
            first = intervals[i]
            second = intervals[i + 1]
            if first.end < second.start:
                raise ValueError(f"Gaps not supported in interval union")

    return cls(start=intervals[0].start, end=intervals[-1].end)

TimeWindow dataclass

TimeWindow(
    start: Union[datetime, date, None] = None,
    end: Union[datetime, date, None] = None,
)

Bases: Interval

An Interval over datetimes/dates, with convenience constructors.

Inherits the full interval algebra (intersects, intersection, union, & / |, containment, shifting). Both endpoints must be the same type: both date or both datetime. For tz-aware datetimes, both must have tzinfo or both must be naive.

Attributes:

Name Type Description
start Union[datetime, date, None]

Lower bound. Defaults to the Unix epoch when left unset.

end Union[datetime, date, None]

Upper bound. Defaults to time_now() when left unset.

duration property

duration: timedelta

end - start as a timedelta.

start_ms property

start_ms: int

start as a Unix timestamp in milliseconds.

end_ms property

end_ms: int

end as a Unix timestamp in milliseconds.

ahead staticmethod

ahead(
    *,
    t: datetime = None,
    duration: timedelta = None,
    days=0,
    seconds=0,
    microseconds=0,
    milliseconds=0,
    minutes=0,
    hours=0,
    weeks=0
) -> TimeWindow

Return the window [t, t + duration] (looking forward from t).

Pass either duration directly or individual timedelta kwargs (hours=..., minutes=..., etc.). t defaults to time_now().

Source code in vayu/time_utils.py
@staticmethod
def ahead(
    *,
    t: dt.datetime = None,
    duration: dt.timedelta = None,
    days=0,
    seconds=0,
    microseconds=0,
    milliseconds=0,
    minutes=0,
    hours=0,
    weeks=0,
) -> "TimeWindow":
    """Return the window ``[t, t + duration]`` (looking forward from ``t``).

    Pass either ``duration`` directly or individual ``timedelta`` kwargs
    (``hours=...``, ``minutes=...``, etc.). ``t`` defaults to ``time_now()``.
    """
    if not duration:
        duration = dt.timedelta(
            days=days,
            seconds=seconds,
            microseconds=microseconds,
            milliseconds=milliseconds,
            minutes=minutes,
            hours=hours,
            weeks=weeks,
        )
    else:
        assert days == seconds == milliseconds == milliseconds == minutes == hours == weeks == 0
    t = t or time_now()
    return TimeWindow(t, t + duration)

behind staticmethod

behind(
    *,
    t: datetime = None,
    duration: timedelta = None,
    days=0,
    seconds=0,
    microseconds=0,
    milliseconds=0,
    minutes=0,
    hours=0,
    weeks=0
) -> TimeWindow

Return the window [t - duration, t] (looking back from t).

Pass either duration directly or individual timedelta kwargs. t defaults to time_now().

Source code in vayu/time_utils.py
@staticmethod
def behind(
    *,
    t: dt.datetime = None,
    duration: dt.timedelta = None,
    days=0,
    seconds=0,
    microseconds=0,
    milliseconds=0,
    minutes=0,
    hours=0,
    weeks=0,
) -> "TimeWindow":
    """Return the window ``[t - duration, t]`` (looking back from ``t``).

    Pass either ``duration`` directly or individual ``timedelta`` kwargs.
    ``t`` defaults to ``time_now()``.
    """
    if not duration:
        duration = dt.timedelta(
            days=days,
            seconds=seconds,
            microseconds=microseconds,
            milliseconds=milliseconds,
            minutes=minutes,
            hours=hours,
            weeks=weeks,
        )
    else:
        assert days == seconds == milliseconds == milliseconds == minutes == hours == weeks == 0

    t = t or time_now()
    return TimeWindow(t - duration, t)

around staticmethod

around(
    *,
    t: datetime = None,
    duration: timedelta = None,
    days=0,
    seconds=0,
    microseconds=0,
    milliseconds=0,
    minutes=0,
    hours=0,
    weeks=0
)

Return [t - duration, t + duration] (symmetric window around t).

Pass either duration directly or individual timedelta kwargs. t defaults to time_now().

Source code in vayu/time_utils.py
@staticmethod
def around(
    *,
    t: dt.datetime = None,
    duration: dt.timedelta = None,
    days=0,
    seconds=0,
    microseconds=0,
    milliseconds=0,
    minutes=0,
    hours=0,
    weeks=0,
):
    """Return ``[t - duration, t + duration]`` (symmetric window around ``t``).

    Pass either ``duration`` directly or individual ``timedelta`` kwargs.
    ``t`` defaults to ``time_now()``.
    """
    if not duration:
        duration = dt.timedelta(
            days=days,
            seconds=seconds,
            microseconds=microseconds,
            milliseconds=milliseconds,
            minutes=minutes,
            hours=hours,
            weeks=weeks,
        )
    else:
        assert days == seconds == milliseconds == milliseconds == minutes == hours == weeks == 0

    t = t or time_now()
    return TimeWindow(t - duration, t + duration)

from_date staticmethod

from_date(
    year: int, month: int, day: int, tz=None
) -> TimeWindow

Return the window spanning a full calendar day (midnight → end of day).

tz defaults to UTC.

Source code in vayu/time_utils.py
@staticmethod
def from_date(year: int, month: int, day: int, tz=None) -> "TimeWindow":
    """Return the window spanning a full calendar day (midnight → end of day).

    ``tz`` defaults to UTC.
    """
    tz = tz or ZoneInfo("UTC")
    date = dt.date(year, month, day)
    return TimeWindow(start=min_time(date, tz=tz), end=max_time(date, tz=tz))

from_timestamp staticmethod

from_timestamp(
    start_ts: [int, float], end_ts: [int, float], tz=None
) -> TimeWindow

Return the window built from two Unix timestamps.

Each timestamp may be seconds or milliseconds (see from_timestamp).

Source code in vayu/time_utils.py
@staticmethod
def from_timestamp(start_ts: [int, float], end_ts: [int, float], tz=None) -> "TimeWindow":
    """Return the window built from two Unix timestamps.

    Each timestamp may be seconds or milliseconds (see ``from_timestamp``).
    """
    start = from_timestamp(start_ts, tz=tz)
    end = from_timestamp(end_ts, tz=tz)
    return TimeWindow(start, end)

Cache

Cache(serializer: Serializer)

Bases: ABC

Abstract TTL cache with sync and async @cached decorators.

Subclasses implement the backend (_read / _write / _delete, each in sync and async form). Concrete implementations include FileCache and MemoryCache.

Source code in vayu/cache.py
def __init__(self, serializer: Serializer):
    self._serializer = serializer
    self._locks = dict()
    self._sync_locks = dict()

cached

cached(
    ttl: Union[timedelta, float],
    prefix: str = None,
    key: Union[str, Callable] = None,
    serializer: Serializer = None,
    log: bool = True,
)

Used as decorator to cache function calls.

Parameters:

Name Type Description Default
ttl Union[timedelta, float]

cache expiry ttl

required
prefix str

If set, gets prepended to the rest of the cache key, otherwise uses function, class, and module name to create prefix.

None
key Union[str, Callable]
  • None: Use SHA256 digest of args & *kwargs to construct cache key
  • str: Use the specified string and ignore function arguments
  • Callable: Calls it with args * *kwargs which returns a str key.
None
serializer Serializer

If not specified, default pickler serializer is used

None
log bool

If True, logs reports such as cache hit, miss etc.

True
Source code in vayu/cache.py
def cached(
    self,
    ttl: Union[timedelta, float],
    prefix: str = None,
    key: Union[str, Callable] = None,
    serializer: Serializer = None,
    log: bool = True,
):
    """Used as decorator to cache function calls.

    Args:
        ttl: cache expiry ttl
        prefix: If set, gets prepended to the rest of the cache key, otherwise uses function,
                    class, and module name to create prefix.
        key:
            - None: Use SHA256 digest of *args & **kwargs to construct cache key
            - str: Use the specified string and ignore function arguments
            - Callable: Calls it with *args * **kwargs which returns a str key.
        serializer: If not specified, default pickler serializer is used
        log: If True, logs reports such as cache hit, miss etc.

    """
    serializer = serializer or self._serializer
    if ttl is None:
        raise ValueError("ttl must be specified for cached decorator")
    if not isinstance(ttl, timedelta):
        ttl = timedelta(seconds=ttl)
    if ttl.total_seconds() <= 0:
        raise ValueError("ttl must be positive")

    @auto_adapt_to_methods
    def decorator(func):
        if asyncio.iscoroutinefunction(func):
            return self._make_async_wrapper(func, serializer, ttl, prefix, key, log)
        else:
            return self._make_sync_wrapper(func, serializer, ttl, prefix, key, log)

    return decorator

CacheMissError

Bases: Exception

Raised on cache miss or when a cached entry has expired.

FileCache

FileCache(path: str, serializer: Serializer = None)

Bases: Cache

Persistent cache backed by files under a directory.

One file per key. Writes are atomic (write to a temp file in the same directory, then replace). Note: cache keys become filenames, so custom key= callables must return filesystem-legal strings (avoid /, etc.).

Parameters:

Name Type Description Default
path str

Directory to store cache files. Created if it doesn't exist.

required
serializer Serializer

How to encode values. Defaults to Pickler.

None
Source code in vayu/cache.py
def __init__(self, path: str, serializer: Serializer = None):
    super().__init__(serializer or Pickler())
    self._path = Path(path)
    self._ttl_bytes_length = 8
    self._path.mkdir(parents=True, exist_ok=True)

Jsoner

Bases: Serializer

Serializer that uses JSON.

Uses orjson when installed (faster), otherwise falls back to the standard-library json. Only handles JSON-serializable values.

MemoryCache

MemoryCache()

Bases: Cache

In-process cache backed by a plain dict.

Uses a no-op serializer — values are stored by reference, not copied. Volatile: everything is lost when the process exits. The module-level mem_cache is an instance of this class, and mem_cached is a shortcut to mem_cache.cached.

Source code in vayu/cache.py
def __init__(self):
    super().__init__(self.NoOpSerializer())
    self._cache = {}

Pickler

Bases: Serializer

Serializer that uses pickle with the highest protocol.

Supports arbitrary Python objects but is not safe to read from untrusted sources.

Serializer

Bases: ABC

Strategy for converting cached values to/from bytes.

add_jitter

add_jitter(
    n: JitterT, jitter_percentage: int = 20
) -> JitterT

Add jitter to seconds.

Source code in vayu/common.py
def add_jitter(n: JitterT, jitter_percentage: int = 20) -> JitterT:
    """Add jitter to seconds."""
    return n * (1 + random.randint(-jitter_percentage, jitter_percentage) / 100)

group

group(iterable: Iterable, key: Callable) -> Dict

Group elements of an iterable by a key function.

Source code in vayu/common.py
def group(iterable: Iterable, key: Callable) -> Dict:
    """Group elements of an iterable by a key function."""
    groups = {}
    for item in iterable:
        k = key(item)
        if k not in groups:
            groups[k] = []
        groups[k].append(item)
    return groups

epoch_time

epoch_time() -> datetime

Return the Unix epoch (1970-01-01 00:00:00) as a tz-aware datetime.

Source code in vayu/time_utils.py
def epoch_time() -> dt.datetime:
    """Return the Unix epoch (1970-01-01 00:00:00) as a tz-aware datetime."""
    return from_timestamp(0)

from_timestamp

from_timestamp(ts: float, tz=TZ) -> datetime

Convert a Unix timestamp to a tz-aware datetime.

Timestamps greater than 1e12 are treated as milliseconds (divided by 1000); smaller values are treated as seconds. This lets you pass either unit without worrying about the difference.

Source code in vayu/time_utils.py
def from_timestamp(ts: float, tz=TZ) -> dt.datetime:
    """Convert a Unix timestamp to a tz-aware datetime.

    Timestamps greater than ``1e12`` are treated as milliseconds (divided by 1000);
    smaller values are treated as seconds. This lets you pass either unit without
    worrying about the difference.
    """
    if ts > 1e12:
        # ts is in ms.
        ts = ts / 1000
    return dt.datetime.fromtimestamp(ts, tz=tz or dt.UTC)

from_z_string

from_z_string(t: str) -> datetime

Parse an ISO-8601 string with a trailing Z (Zulu / UTC) suffix.

Source code in vayu/time_utils.py
def from_z_string(t: str) -> dt.datetime:
    """Parse an ISO-8601 string with a trailing ``Z`` (Zulu / UTC) suffix."""
    return dt.datetime.fromisoformat(t.upper().replace("Z", "+00:00"))

local_datetime

local_datetime(
    year: int,
    month: int,
    day: int,
    hour: int = 0,
    minute: int = 0,
    second: int = 0,
    microsecond: int = 0,
    tz=TZ,
) -> datetime

Construct a datetime in the module TZ (or the given tz).

Source code in vayu/time_utils.py
def local_datetime(
    year: int,
    month: int,
    day: int,
    hour: int = 0,
    minute: int = 0,
    second: int = 0,
    microsecond: int = 0,
    tz=TZ,
) -> datetime.datetime:
    """Construct a datetime in the module ``TZ`` (or the given ``tz``)."""
    t = datetime.datetime(
        year=year,
        month=month,
        day=day,
        hour=hour,
        minute=minute,
        second=second,
        microsecond=microsecond,
    )
    return t.replace(tzinfo=tz)

max_time

max_time(date: date, tz=None) -> datetime

Return the latest datetime on the given date (23:59:59.999999). UTC by default.

Source code in vayu/time_utils.py
def max_time(date: dt.date, tz=None) -> dt.datetime:
    """Return the latest datetime on the given date (``23:59:59.999999``). UTC by default."""
    return dt.datetime.combine(date, dt.time.max, tzinfo=tz or dt.UTC)

min_time

min_time(date: date, tz=None) -> datetime

Return the earliest datetime on the given date (00:00:00). UTC by default.

Source code in vayu/time_utils.py
def min_time(date: dt.date, tz=None) -> dt.datetime:
    """Return the earliest datetime on the given date (``00:00:00``). UTC by default."""
    return dt.datetime.combine(date, dt.time.min, tzinfo=tz or dt.UTC)

time_now

time_now(
    with_ms: bool = False, local: bool = True
) -> datetime

Return the current time as a tz-aware datetime.

Parameters:

Name Type Description Default
with_ms bool

Keep microseconds. Default is to truncate to whole seconds.

False
local bool

If True, use the module TZ. If False, return UTC.

True
Source code in vayu/time_utils.py
def time_now(with_ms: bool = False, local: bool = True) -> dt.datetime:
    """Return the current time as a tz-aware datetime.

    Args:
        with_ms: Keep microseconds. Default is to truncate to whole seconds.
        local: If True, use the module ``TZ``. If False, return UTC.
    """
    t = dt.datetime.now(tz=TZ if local else datetime.UTC)
    if not with_ms:
        t = t.replace(microsecond=0)

    return t

timeit

timeit(f)

Decorator that prints how long the wrapped function took.

Useful for quick scripts. For library-level instrumentation, wire your own logging around the call instead — this helper uses print.

Source code in vayu/time_utils.py
def timeit(f):
    """Decorator that prints how long the wrapped function took.

    Useful for quick scripts. For library-level instrumentation, wire your own
    logging around the call instead — this helper uses ``print``.
    """

    @wraps(f)
    def timed(*args, **kw):

        t = -time.time()
        result = f(*args, **kw)
        t += time.time()

        print(f"`{f.__name__}` took: {round(t, 3)}s")
        return result

    return timed

to_human_readable_time

to_human_readable_time(t: Union[timedelta, float])

Format a duration as a compact human string (e.g. 1h2m3s).

Accepts a timedelta or a number of seconds. Returns "0s" for zero and "invalid-time" for negative values.

Source code in vayu/time_utils.py
def to_human_readable_time(t: Union[dt.timedelta, float]):
    """Format a duration as a compact human string (e.g. ``1h2m3s``).

    Accepts a ``timedelta`` or a number of seconds. Returns ``"0s"`` for zero
    and ``"invalid-time"`` for negative values.
    """
    if isinstance(t, dt.timedelta):
        t = int(t.total_seconds())
    if t < 0:
        return "invalid-time"

    h, remainder = divmod(t, 3600)
    m, s = divmod(remainder, 60)

    parts = []
    if h > 0:
        parts.append(f"{h}h")
    if m > 0:
        parts.append(f"{m}m")
    if s > 0:
        parts.append(f"{s}s")

    return "".join(parts) if parts else "0s"

ts_ms

ts_ms(t: datetime) -> int

Return a Unix timestamp in milliseconds.

Source code in vayu/time_utils.py
def ts_ms(t: datetime.datetime) -> int:
    """Return a Unix timestamp in milliseconds."""
    return int(t.timestamp() * 1000)

utc_datetime

utc_datetime(
    year: int,
    month: int,
    day: int,
    hour: int = 0,
    minute: int = 0,
    second: int = 0,
    microsecond: int = 0,
) -> datetime

Construct a UTC datetime. Convenience wrapper over datetime(..., tzinfo=UTC).

Source code in vayu/time_utils.py
def utc_datetime(
    year: int,
    month: int,
    day: int,
    hour: int = 0,
    minute: int = 0,
    second: int = 0,
    microsecond: int = 0,
) -> dt.datetime:
    """Construct a UTC datetime. Convenience wrapper over ``datetime(..., tzinfo=UTC)``."""
    return dt.datetime(
        year=year,
        month=month,
        day=day,
        hour=hour,
        minute=minute,
        second=second,
        microsecond=microsecond,
        tzinfo=datetime.UTC,
    )

configure_logging

configure_logging(
    level: Optional[str] = None,
    tz: Optional[str] = None,
    stream: IO = stdout,
    fmt: str = "[%(asctime)s] %(levelname)s> %(message)s",
) -> None

Configure the vayu logger with a stdout handler and timezone-aware timestamps.

Off by default so that importing the library does not mutate global logging state. Call once at application startup to opt in.

Parameters:

Name Type Description Default
level Optional[str]

log level (default: env var LOG_LEVEL, else "INFO").

None
tz Optional[str]

IANA timezone for timestamps (default: env var TZ, else "UTC").

None
stream IO

handler output stream.

stdout
fmt str

log record format.

'[%(asctime)s] %(levelname)s> %(message)s'
Source code in vayu/log.py
def configure(
    level: Optional[str] = None,
    tz: Optional[str] = None,
    stream: IO = sys.stdout,
    fmt: str = "[%(asctime)s] %(levelname)s> %(message)s",
) -> None:
    """Configure the vayu logger with a stdout handler and timezone-aware timestamps.

    Off by default so that importing the library does not mutate global logging
    state. Call once at application startup to opt in.

    Args:
        level: log level (default: env var LOG_LEVEL, else "INFO").
        tz: IANA timezone for timestamps (default: env var TZ, else "UTC").
        stream: handler output stream.
        fmt: log record format.
    """
    level = (level or os.getenv("LOG_LEVEL", "INFO")).upper()
    zone = ZoneInfo(tz or os.getenv("TZ", "UTC"))

    class _TzFormatter(logging.Formatter):
        def formatTime(self, record, datefmt=None):
            return (
                datetime.datetime.fromtimestamp(record.created, datetime.timezone.utc)
                .replace(microsecond=0)
                .astimezone(zone)
                .isoformat(sep="T")
            )

    for existing in list(L.handlers):
        if not isinstance(existing, logging.NullHandler):
            L.removeHandler(existing)

    handler = logging.StreamHandler(stream)
    handler.setFormatter(_TzFormatter(fmt))
    L.addHandler(handler)
    L.setLevel(level)
    L.propagate = False
    logging.getLogger("asyncio").setLevel(logging.WARNING)

install_pandas_extensions

install_pandas_extensions() -> None

Attach select convenience methods to pandas.DataFrame and pandas.Series.

Opt-in because it monkey-patches pandas globally. Call once at application startup if you want df.select(...) and series.select(...). Requires the vayulib[data] extra.

Source code in vayu/__init__.py
def install_pandas_extensions() -> None:
    """Attach `select` convenience methods to `pandas.DataFrame` and `pandas.Series`.

    Opt-in because it monkey-patches pandas globally. Call once at application
    startup if you want `df.select(...)` and `series.select(...)`. Requires the
    `vayulib[data]` extra.
    """
    import pandas as pd

    from vayu.pandas_utils import select_frame, select_series

    if not hasattr(pd.DataFrame, "select"):
        pd.DataFrame.select = select_frame
    if not hasattr(pd.Series, "select"):
        pd.Series.select = select_series