from abc import ABC, abstractmethod
from dataclasses import dataclass
import getpass
import os
import platform
import socket
import subprocess
import time
from typing import Any, TypeAlias
from .call import Call
try:
from ._version import __version__ as _fleche_version
except ImportError:
[docs]
_fleche_version = "unknown"
# Values produced by MetaData.pre/post must be JSON-serializable.
# This alias documents the expected shape and helps static type checkers.
[docs]
JSONValue: TypeAlias = str | int | float | bool | None | list["JSONValue"] | dict[str, "JSONValue"]
[docs]
class Runtime(MetaData):
"""Metadata type for capturing runtime information.
Keys:
timestart (float): The timestamp when the execution started.
timestop (float): The timestamp when the execution stopped.
walltime (float): The total execution time in seconds.
Notes:
Values are JSON-serializable.
"""
[docs]
def pre(self, call: Call) -> dict[str, Any]:
"""
Records the start time before function execution.
"""
return {'timestart': time.time()}
[docs]
def post(self, pre: dict[str, Any], call: Call) -> dict[str, Any]:
"""
Records the stop time and calculates the wall time after function execution.
"""
return {
'timestop': (t := time.time()),
'walltime': t - pre['timestart'],
}
[docs]
keys: dict[str, type] = {
'timestart': float,
'timestop': float,
'walltime': float,
}
[docs]
class Environment(MetaData):
"""Metadata type for capturing the execution environment.
Keys:
hostname (str): The machine hostname (``socket.gethostname()``).
username (str): The current user (``getpass.getuser()``).
cwd (str): The working directory at call time (``os.getcwd()``).
fleche_version (str): The fleche package version (``fleche.__version__``);
``"unknown"`` when the package was imported without an installed
``_version.py`` (e.g. an editable checkout without a build).
python_version (str): The CPython runtime version (``platform.python_version()``).
"""
[docs]
def pre(self, call: Call) -> dict[str, Any]:
return {
'hostname': socket.gethostname(),
'username': getpass.getuser(),
'cwd': os.getcwd(),
'fleche_version': _fleche_version,
'python_version': platform.python_version(),
}
[docs]
name: str = 'environment'
[docs]
keys: dict[str, type] = {
'hostname': str,
'username': str,
'cwd': str,
'fleche_version': str,
'python_version': str,
}
[docs]
def _git(*args: str) -> str | None:
"""Run ``git`` with *args* and return stripped stdout, or ``None`` on failure."""
try:
result = subprocess.run(
('git', *args),
capture_output=True, text=True, check=False, timeout=2,
)
except (FileNotFoundError, subprocess.TimeoutExpired):
return None
if result.returncode != 0:
return None
return result.stdout.strip()
[docs]
class Git(MetaData):
"""Metadata type for capturing the git state of the working directory.
Keys:
root (str | None): Repository top level (``git rev-parse --show-toplevel``).
commit (str | None): HEAD commit SHA (``git rev-parse HEAD``).
branch (str | None): Current branch name (``git rev-parse --abbrev-ref HEAD``);
``"HEAD"`` when in detached-HEAD state.
dirty (bool | None): ``True`` if there are uncommitted changes
(tracked or untracked), ``False`` otherwise; ``None`` when not
inside a repository or git is unavailable.
All keys are ``None`` when not inside a git repository or when the ``git``
executable is missing.
"""
[docs]
def pre(self, call: Call) -> dict[str, Any]:
root = _git('rev-parse', '--show-toplevel')
if root is None:
return {'root': None, 'commit': None, 'branch': None, 'dirty': None}
status = _git('status', '--porcelain')
return {
'root': root,
'commit': _git('rev-parse', 'HEAD'),
'branch': _git('rev-parse', '--abbrev-ref', 'HEAD'),
'dirty': bool(status) if status is not None else None,
}
[docs]
keys: dict[str, type] = {
'root': str,
'commit': str,
'branch': str,
'dirty': bool,
}
@dataclass