Comprehensive cleanup across 80 files based on automated (ruff, pyflakes, vulture)
and manual analysis of the entire codebase.
Changes by category:
Unused imports removed (~95 across 55 files):
- Removed genuinely unused imports from all major subsystems
- agent/, hermes_cli/, tools/, gateway/, plugins/, cron/
- Includes imports in try/except blocks that were truly unused
(vs availability checks which were left alone)
Unused variables removed (~25):
- Removed dead variables: connected, inner, channels, last_exc,
source, new_server_names, verify, pconfig, default_terminal,
result, pending_handled, temperature, loop
- Dropped unused argparse subparser assignments in hermes_cli/main.py
(12 instances of add_parser() where result was never used)
Dead code removed:
- run_agent.py: Removed dead ternary (None if False else None) and
surrounding unreachable branch in identity fallback
- run_agent.py: Removed write-only attribute _last_reported_tool
- hermes_cli/providers.py: Removed dead @property decorator on
module-level function (decorator has no effect outside a class)
- gateway/run.py: Removed unused MCP config load before reconnect
- gateway/platforms/slack.py: Removed dead SessionSource construction
Undefined name bugs fixed (would cause NameError at runtime):
- batch_runner.py: Added missing logger = logging.getLogger(__name__)
- tools/environments/daytona.py: Added missing Dict and Path imports
Unnecessary global statements removed (14):
- tools/terminal_tool.py: 5 functions declared global for dicts
they only mutated via .pop()/[key]=value (no rebinding)
- tools/browser_tool.py: cleanup thread loop only reads flag
- tools/rl_training_tool.py: 4 functions only do dict mutations
- tools/mcp_oauth.py: only reads the global
- hermes_time.py: only reads cached values
Inefficient patterns fixed:
- startswith/endswith tuple form: 15 instances of
x.startswith('a') or x.startswith('b') consolidated to
x.startswith(('a', 'b'))
- len(x)==0 / len(x)>0: 13 instances replaced with pythonic
truthiness checks (not x / bool(x))
- in dict.keys(): 5 instances simplified to in dict
- Redefined unused name: removed duplicate _strip_mdv2 import in
send_message_tool.py
Other fixes:
- hermes_cli/doctor.py: Replaced undefined logger.debug() with pass
- hermes_cli/config.py: Consolidated chained .endswith() calls
Test results: 3934 passed, 17 failed (all pre-existing on main),
19 skipped. Zero regressions.
119 lines
3.6 KiB
Python
119 lines
3.6 KiB
Python
"""
|
|
Timezone-aware clock for Hermes.
|
|
|
|
Provides a single ``now()`` helper that returns a timezone-aware datetime
|
|
based on the user's configured IANA timezone (e.g. ``Asia/Kolkata``).
|
|
|
|
Resolution order:
|
|
1. ``HERMES_TIMEZONE`` environment variable
|
|
2. ``timezone`` key in ``~/.hermes/config.yaml``
|
|
3. Falls back to the server's local time (``datetime.now().astimezone()``)
|
|
|
|
Invalid timezone values log a warning and fall back safely — Hermes never
|
|
crashes due to a bad timezone string.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
from datetime import datetime
|
|
from hermes_constants import get_hermes_home
|
|
from typing import Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
from zoneinfo import ZoneInfo
|
|
except ImportError:
|
|
# Python 3.8 fallback (shouldn't be needed — Hermes requires 3.9+)
|
|
from backports.zoneinfo import ZoneInfo # type: ignore[no-redef]
|
|
|
|
# Cached state — resolved once, reused on every call.
|
|
# Call reset_cache() to force re-resolution (e.g. after config changes).
|
|
_cached_tz: Optional[ZoneInfo] = None
|
|
_cached_tz_name: Optional[str] = None
|
|
_cache_resolved: bool = False
|
|
|
|
|
|
def _resolve_timezone_name() -> str:
|
|
"""Read the configured IANA timezone string (or empty string).
|
|
|
|
This does file I/O when falling through to config.yaml, so callers
|
|
should cache the result rather than calling on every ``now()``.
|
|
"""
|
|
# 1. Environment variable (highest priority — set by Supervisor, etc.)
|
|
tz_env = os.getenv("HERMES_TIMEZONE", "").strip()
|
|
if tz_env:
|
|
return tz_env
|
|
|
|
# 2. config.yaml ``timezone`` key
|
|
try:
|
|
import yaml
|
|
hermes_home = get_hermes_home()
|
|
config_path = hermes_home / "config.yaml"
|
|
if config_path.exists():
|
|
with open(config_path) as f:
|
|
cfg = yaml.safe_load(f) or {}
|
|
tz_cfg = cfg.get("timezone", "")
|
|
if isinstance(tz_cfg, str) and tz_cfg.strip():
|
|
return tz_cfg.strip()
|
|
except Exception:
|
|
pass
|
|
|
|
return ""
|
|
|
|
|
|
def _get_zoneinfo(name: str) -> Optional[ZoneInfo]:
|
|
"""Validate and return a ZoneInfo, or None if invalid."""
|
|
if not name:
|
|
return None
|
|
try:
|
|
return ZoneInfo(name)
|
|
except (KeyError, Exception) as exc:
|
|
logger.warning(
|
|
"Invalid timezone '%s': %s. Falling back to server local time.",
|
|
name, exc,
|
|
)
|
|
return None
|
|
|
|
|
|
def get_timezone() -> Optional[ZoneInfo]:
|
|
"""Return the user's configured ZoneInfo, or None (meaning server-local).
|
|
|
|
Resolved once and cached. Call ``reset_cache()`` after config changes.
|
|
"""
|
|
global _cached_tz, _cached_tz_name, _cache_resolved
|
|
if not _cache_resolved:
|
|
_cached_tz_name = _resolve_timezone_name()
|
|
_cached_tz = _get_zoneinfo(_cached_tz_name)
|
|
_cache_resolved = True
|
|
return _cached_tz
|
|
|
|
|
|
def get_timezone_name() -> str:
|
|
"""Return the IANA name of the configured timezone, or empty string."""
|
|
if not _cache_resolved:
|
|
get_timezone() # populates cache
|
|
return _cached_tz_name or ""
|
|
|
|
|
|
def now() -> datetime:
|
|
"""
|
|
Return the current time as a timezone-aware datetime.
|
|
|
|
If a valid timezone is configured, returns wall-clock time in that zone.
|
|
Otherwise returns the server's local time (via ``astimezone()``).
|
|
"""
|
|
tz = get_timezone()
|
|
if tz is not None:
|
|
return datetime.now(tz)
|
|
# No timezone configured — use server-local (still tz-aware)
|
|
return datetime.now().astimezone()
|
|
|
|
|
|
def reset_cache() -> None:
|
|
"""Clear the cached timezone. Used by tests and after config changes."""
|
|
global _cached_tz, _cached_tz_name, _cache_resolved
|
|
_cached_tz = None
|
|
_cached_tz_name = None
|
|
_cache_resolved = False
|