Files
jackwener e496d8f870 refactor: unify exception handling, add ISO 8601 time, dedup commands, expand tests
- Replace _error_code_for_message() string matching with error_code attribute on exception classes
- Add error_code to all TwitterError subclasses (AuthenticationError, RateLimitError, etc.)
- Add InvalidInputError exception class
- TwitterAPIError derives error_code from HTTP status code automatically
- auth.py: use AuthenticationError instead of RuntimeError
- cli.py: catch (TwitterError, RuntimeError) for backward compat
- Extract _fetch_and_display_users() to deduplicate followers/following commands
- Add format_iso8601() to timeutil.py
- Add createdAtISO field to tweet and user profile serialization
- New test files: test_output.py, test_cache.py, test_timeutil.py
- Expand test_filter.py (topN, score mode, custom weights, empty input)
- Tests: 152 → 194 unit tests, all passing
2026-03-16 18:24:35 +08:00

83 lines
2.3 KiB
Python

"""Time formatting utilities for twitter-cli.
Converts Twitter API timestamps (e.g. "Sat Mar 08 12:00:00 +0000 2026")
into human-friendly local time and relative time strings.
"""
from __future__ import annotations
import logging
from datetime import datetime, timezone
from typing import Optional
logger = logging.getLogger(__name__)
# Twitter API timestamp format: "Sat Mar 08 12:00:00 +0000 2026"
_TWITTER_TIME_FORMAT = "%a %b %d %H:%M:%S %z %Y"
def _parse_twitter_time(created_at: str) -> Optional[datetime]:
"""Parse a Twitter API timestamp into a timezone-aware datetime."""
if not created_at:
return None
try:
return datetime.strptime(created_at, _TWITTER_TIME_FORMAT)
except (ValueError, TypeError):
logger.debug("Failed to parse Twitter timestamp: %s", created_at)
return None
def format_local_time(created_at: str) -> str:
"""Convert Twitter timestamp to local time string.
Returns "2026-03-14 21:08" or the original string on parse failure.
"""
dt = _parse_twitter_time(created_at)
if dt is None:
return created_at
local_dt = dt.astimezone()
return local_dt.strftime("%Y-%m-%d %H:%M")
def format_relative_time(created_at: str) -> str:
"""Convert Twitter timestamp to a relative time string.
Returns e.g. "2m ago", "3h ago", "5d ago", or the original string on failure.
"""
dt = _parse_twitter_time(created_at)
if dt is None:
return created_at
now = datetime.now(timezone.utc)
delta = now - dt
seconds = int(delta.total_seconds())
if seconds < 0:
return "just now"
if seconds < 60:
return "%ds ago" % seconds
minutes = seconds // 60
if minutes < 60:
return "%dm ago" % minutes
hours = minutes // 60
if hours < 24:
return "%dh ago" % hours
days = hours // 24
if days < 30:
return "%dd ago" % days
months = days // 30
if months < 12:
return "%dmo ago" % months
years = days // 365
return "%dy ago" % years
def format_iso8601(created_at: str) -> str:
"""Convert Twitter timestamp to ISO 8601 format.
Returns e.g. "2026-03-08T12:00:00+00:00" or the original string on failure.
"""
dt = _parse_twitter_time(created_at)
if dt is None:
return created_at
return dt.isoformat()