yggdrasil.io.io_stats¶
io_stats ¶
Unified I/O stats — backend stat quad plus content-level media_type.
Every :class:yggdrasil.io.holder.Holder (concretely :class:Memory
and :class:yggdrasil.io.fs.Path) exposes :meth:stats returning an
:class:IOStats. It's the single shape downstream code reads when it
needs "what kind, how big, how fresh, what is it" without caring
whether the backing is a local file, an S3 object, an in-memory
buffer, or something else.
The size / mtime / kind / mode quad mirrors
:class:os.stat_result so existing callers can keep using the
familiar st_size / st_mtime / st_mode aliases or the
positional tuple shape. media_type extends the picture with the
single most useful piece for content-level dispatch — codec
selection, format inference, content-negotiation.
IOKind ¶
Bases: IntEnum
What a backend reports a path/holder entry is.
IOStats
dataclass
¶
IOStats(
size: int = 0,
mtime: float = 0.0,
kind: IOKind = IOKind.MISSING,
mode: int = 0,
media_type: "Optional[MediaType]" = None,
metadata: "Optional[dict[str, str]]" = None,
field: "Optional[Field]" = None,
)
Stat-like quad (size / mtime / kind / mode) + media_type.
All fields are best-effort:
size— visible byte count.0is a legitimate value for a freshly-created empty holder or a directory.mtime— last modification time as a Unix timestamp.0.0when the backing has no meaningful mtime (memory holders, newly-minted spills) — callers that need "now" should use :attr:mtime_or_now.kind— :class:IOKindenum classifying the entry. Defaults to :attr:IOKind.MISSINGso an "empty" stats object reads as "nothing here".mode— POSIX permission bits,0when the backend has no meaningful concept of mode (S3, Databricks REST).media_type— :class:MediaTypeinferred from the holder's identity (URL extension, registered mime, sniffed magic bytes).Nonewhen no honest answer is available; never guess :class:MimeTypes.OCTET_STREAMhere — let the caller decide.metadata— free-form backend metadata as a flatdict[str, str](S3 response +x-amz-meta-*headers, Databricks Files headers, ETag, version id, …).Nonewhen the backend exposes nothing extra; a single home for "everything else the backend told us" without subclassing :class:IOStats.
Backends with richer metadata (ETag, content-type, owner…) populate
:attr:metadata rather than cramming extras into mode.
mtime_or_now
property
¶
mtime if set, else :func:time.time — for callers that
always want a non-zero timestamp without sprinkling fallbacks.
has_mtime
property
¶
Whether the backing reported a real modification time.
False for memory holders, freshly-minted spills, and any
backend that doesn't expose mtime — the sentinel 0.0
means "unknown", not "epoch".
copy ¶
copy(
*,
size: Any = ...,
mtime: Any = ...,
kind: Any = ...,
mode: Any = ...,
media_type: Any = ...,
metadata: Any = ...,
field: Any = ...
) -> "IOStats"
Return a fresh :class:IOStats with selected fields overridden.
Each kwarg uses the ... sentinel so the caller can pass
None (or any other value) to override without colliding
with "leave unchanged". Any field left at ... carries the
value over from self.
with_ ¶
with_(
*,
size: Any = ...,
mtime: Any = ...,
kind: Any = ...,
mode: Any = ...,
media_type: Any = ...,
metadata: Any = ...,
field: Any = ...,
inplace: bool = False
) -> "IOStats"
Return a stats object with the given fields overridden.
inplace=False (default) returns a fresh :class:IOStats
via :meth:copy. inplace=True mutates self and
returns self for chaining. Each field kwarg uses the
... sentinel — pass any other value (including None)
to override.
normalize_timestamp
staticmethod
¶
Resolve a flexible time spec to a Unix timestamp.
None propagates as None (caller uses it as "no
bound"). Numbers pass through as raw Unix timestamps.
:class:datetime.timedelta is resolved as
now(UTC) - delta — a past wall-clock moment. This
matches how callers naturally phrase mtime filters: stats
.is_fresher_than(timedelta(hours=1)) reads as "modified in
the last hour", and modified_between(start=timedelta(days
=7)) as "modified in the last week". A zero/negative
timedelta is allowed and resolves to "now" or a future
instant respectively — useful as a sentinel but uncommon in
practice.
Everything else (datetime, ISO string) goes through
:func:any_to_datetime for consistent parsing.
is_fresher_than ¶
Whether this holder was modified strictly after mtime.
Unknown mtime (self.mtime == 0.0) returns False.
is_older_than ¶
Whether this holder was modified strictly before mtime.
Unknown mtime (self.mtime == 0.0) returns False.
modified_between ¶
Whether mtime falls in the half-open window [start, end).
Either bound may be None for "unbounded on that side", so
modified_between(start=yesterday) reads "modified since
yesterday" and modified_between(end=cutoff) reads
"modified before cutoff". Both None is allowed and
accepts any known mtime (still rejects unknown).
Bounds accept anything :func:any_to_datetime understands —
datetime, ISO strings, raw timestamps, or a
:class:datetime.timedelta interpreted relative to now.
Unknown mtime (self.mtime == 0.0) always returns
False — there's no honest answer.
is_between_timestamp ¶
Raw-timestamp variant of :meth:modified_between.
Half-open [start, end). None on either side means
unbounded. Unknown mtime returns False.
format_bytes ¶
Human-readable byte count for logs / messages — 1536 → '1.5 KiB'.
Binary (IEC, 1024-step KiB/MiB/…) by default, matching how the
codebase sizes its buffers and limits; pass binary=False for SI
(1000-step kB/MB/…). None renders as 'unknown' and sub-step
counts stay exact bytes ('512 B'); larger values get one decimal.
Cheap (a handful of divisions), but callers that log on a hot path should
still gate the call behind logger.isEnabledFor(...) so it's skipped
when the level is disabled.