Source code for emout.local_data_policy
"""Process-wide policy for local field-data access."""
from __future__ import annotations
import os
from contextlib import contextmanager
from contextvars import ContextVar
from typing import Iterator
LOCAL_DATA_POLICY_ALLOW = "allow"
LOCAL_DATA_POLICY_REMOTE_REQUIRED = "remote_required"
VALID_LOCAL_DATA_POLICIES = (
LOCAL_DATA_POLICY_ALLOW,
LOCAL_DATA_POLICY_REMOTE_REQUIRED,
)
LOCAL_DATA_POLICY_ENV = "EMOUT_LOCAL_DATA_POLICY"
_global_local_data_policy: str | None = None
_context_local_data_policy: ContextVar[str | None] = ContextVar(
"emout_local_data_policy",
default=None,
)
[docs]
class LocalDataAccessDisabledError(RuntimeError):
"""Raised when local field-data access is disabled by policy."""
def normalize_local_data_policy(policy: str | None) -> str | None:
"""Return a canonical local-data policy name."""
if policy is None:
return None
normalized = str(policy).strip().lower().replace("-", "_")
if normalized in {"disabled", "disable", "remote", "strict"}:
normalized = LOCAL_DATA_POLICY_REMOTE_REQUIRED
if normalized in {"enabled", "enable", "local"}:
normalized = LOCAL_DATA_POLICY_ALLOW
if normalized not in VALID_LOCAL_DATA_POLICIES:
allowed = ", ".join(repr(name) for name in VALID_LOCAL_DATA_POLICIES)
raise ValueError(f"Unknown local data policy {policy!r}; expected one of: {allowed}")
return normalized
def get_local_data_policy(override: str | None = None) -> str:
"""Return the effective local-data access policy."""
normalized_override = normalize_local_data_policy(override)
if normalized_override is not None:
return normalized_override
context_policy = _context_local_data_policy.get()
if context_policy is not None:
return context_policy
if _global_local_data_policy is not None:
return _global_local_data_policy
env_policy = os.environ.get(LOCAL_DATA_POLICY_ENV)
normalized_env_policy = normalize_local_data_policy(env_policy)
if normalized_env_policy is not None:
return normalized_env_policy
return LOCAL_DATA_POLICY_ALLOW
def set_local_data_policy(policy: str) -> None:
"""Set the process-wide default local-data access policy."""
global _global_local_data_policy
_global_local_data_policy = normalize_local_data_policy(policy)
def reset_local_data_policy() -> None:
"""Clear the process-wide override and fall back to context/env/defaults."""
global _global_local_data_policy
_global_local_data_policy = None
def disable_local_data_access() -> None:
"""Require field data to be read through remote execution."""
set_local_data_policy(LOCAL_DATA_POLICY_REMOTE_REQUIRED)
def enable_local_data_access() -> None:
"""Allow local field-data reads."""
set_local_data_policy(LOCAL_DATA_POLICY_ALLOW)
def is_local_data_access_disabled(override: str | None = None) -> bool:
"""Return whether local field-data reads are disabled."""
return get_local_data_policy(override) == LOCAL_DATA_POLICY_REMOTE_REQUIRED
@contextmanager
def local_data_policy(policy: str) -> Iterator[None]:
"""Temporarily override local-data access policy in the current context."""
token = _context_local_data_policy.set(normalize_local_data_policy(policy))
try:
yield
finally:
_context_local_data_policy.reset(token)
def local_data_access_error(operation: str, target: str | None = None) -> LocalDataAccessDisabledError:
"""Build a consistent policy error for local field-data access."""
where = f" for {target}" if target else ""
return LocalDataAccessDisabledError(
"Local field data access is disabled by local_data_policy='remote_required'. "
f"Refused to {operation}{where}. "
"Use Emout.remote() with remote_scope()/remote_figure(), or temporarily allow local access with "
"emout.local_data_policy('allow') for small data."
)
def require_local_data_access(policy: str | None, operation: str, target: str | None = None) -> None:
"""Raise if the effective policy disallows local field-data access."""
if is_local_data_access_disabled(policy):
raise local_data_access_error(operation, target)