mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-19 15:42:13 +02:00
339 lines
11 KiB
Python
339 lines
11 KiB
Python
# streaming_providers/base/utils/timestamp_converter.py
|
|
"""
|
|
Timestamp conversion utilities for streaming providers.
|
|
|
|
Provides methods to convert between Unix epoch seconds and ISO 8601 formats,
|
|
with support for timezone handling.
|
|
"""
|
|
|
|
import datetime
|
|
from typing import Optional, Union
|
|
|
|
|
|
class TimestampConverter:
|
|
"""
|
|
Utility class for converting between epoch seconds and ISO 8601 timestamps.
|
|
|
|
Supports both extended (human-readable) and basic (compact) ISO 8601 formats.
|
|
"""
|
|
|
|
# ISO 8601 format constants
|
|
ISO_EXTENDED = "%Y-%m-%dT%H:%M:%S"
|
|
ISO_BASIC = "%Y%m%dT%H%M%S"
|
|
ISO_WITH_MICROSECONDS = "%Y-%m-%dT%H:%M:%S.%f"
|
|
ISO_WITH_TIMEZONE = "%Y-%m-%dT%H:%M:%S%z"
|
|
|
|
# UTC timezone constant (Python 3.11+ compatibility)
|
|
if hasattr(datetime, "UTC"):
|
|
UTC = datetime.UTC
|
|
else:
|
|
UTC = datetime.timezone.utc
|
|
|
|
@staticmethod
|
|
def epoch_to_iso(
|
|
epoch_seconds: Union[int, float],
|
|
format_type: str = "extended",
|
|
timezone: Optional[datetime.tzinfo] = None,
|
|
as_utc: bool = True,
|
|
) -> str:
|
|
"""
|
|
Convert Unix epoch seconds to ISO 8601 timestamp.
|
|
|
|
Args:
|
|
epoch_seconds: Unix timestamp in seconds
|
|
format_type: "extended" (2025-12-08T07:59:30), "basic" (20251208T075930),
|
|
"microseconds" (2025-12-08T07:59:30.000000), or
|
|
"with_timezone" (2025-12-08T07:59:30+00:00)
|
|
timezone: Optional timezone object (if None and as_utc=True, uses UTC)
|
|
as_utc: If True and no timezone provided, treat epoch as UTC
|
|
|
|
Returns:
|
|
ISO 8601 formatted timestamp string
|
|
|
|
Raises:
|
|
ValueError: If format_type is invalid
|
|
"""
|
|
# Create timezone-aware datetime from epoch
|
|
if as_utc and timezone is None:
|
|
# Use UTC timezone
|
|
dt = datetime.datetime.fromtimestamp(
|
|
epoch_seconds, tz=TimestampConverter.UTC
|
|
)
|
|
elif timezone is not None:
|
|
# Use specified timezone
|
|
dt = datetime.datetime.fromtimestamp(epoch_seconds, tz=timezone)
|
|
else:
|
|
# No timezone specified and not forcing UTC - create naive datetime
|
|
dt = datetime.datetime.fromtimestamp(epoch_seconds)
|
|
|
|
# Choose format
|
|
if format_type == "extended":
|
|
format_str = TimestampConverter.ISO_EXTENDED
|
|
elif format_type == "basic":
|
|
format_str = TimestampConverter.ISO_BASIC
|
|
elif format_type == "microseconds":
|
|
format_str = TimestampConverter.ISO_WITH_MICROSECONDS
|
|
elif format_type == "with_timezone":
|
|
format_str = TimestampConverter.ISO_WITH_TIMEZONE
|
|
else:
|
|
raise ValueError(
|
|
f"Invalid format_type: {format_type}. "
|
|
f"Must be 'extended', 'basic', 'microseconds', or 'with_timezone'"
|
|
)
|
|
|
|
return dt.strftime(format_str)
|
|
|
|
@staticmethod
|
|
def iso_to_epoch(
|
|
iso_string: str,
|
|
format_type: Optional[str] = None,
|
|
timezone: Optional[datetime.tzinfo] = None,
|
|
) -> float:
|
|
"""
|
|
Convert ISO 8601 timestamp to Unix epoch seconds.
|
|
|
|
Args:
|
|
iso_string: ISO 8601 timestamp string
|
|
format_type: Optional format type. If None, auto-detects based on string format.
|
|
timezone: Optional timezone object. If provided and the input string doesn't
|
|
have timezone info, interpret it in this timezone.
|
|
|
|
Returns:
|
|
Unix epoch seconds (float)
|
|
|
|
Raises:
|
|
ValueError: If string cannot be parsed
|
|
"""
|
|
# First try datetime.fromisoformat() which handles many ISO formats
|
|
try:
|
|
# Clean up the string for fromisoformat
|
|
cleaned = iso_string.replace("Z", "+00:00")
|
|
dt = datetime.datetime.fromisoformat(cleaned)
|
|
# If we got here, fromisoformat worked
|
|
except ValueError:
|
|
# Try our custom parsers
|
|
dt = TimestampConverter._parse_custom_iso(iso_string, format_type)
|
|
|
|
# If datetime is naive and timezone is provided, localize it
|
|
if dt.tzinfo is None and timezone is not None:
|
|
dt = dt.replace(tzinfo=timezone)
|
|
|
|
# Convert to epoch seconds
|
|
if dt.tzinfo is not None:
|
|
# For timezone-aware datetimes, convert to UTC first
|
|
dt_utc = dt.astimezone(TimestampConverter.UTC)
|
|
return dt_utc.timestamp()
|
|
else:
|
|
# For naive datetimes, assume UTC
|
|
return dt.replace(tzinfo=TimestampConverter.UTC).timestamp()
|
|
|
|
@staticmethod
|
|
def _parse_custom_iso(
|
|
iso_string: str, format_type: Optional[str] = None
|
|
) -> datetime.datetime:
|
|
"""
|
|
Parse custom ISO formats not handled by fromisoformat.
|
|
|
|
Args:
|
|
iso_string: ISO string to parse
|
|
format_type: Optional format type hint
|
|
|
|
Returns:
|
|
datetime object
|
|
"""
|
|
# Auto-detect format if not specified
|
|
if format_type is None:
|
|
if "T" in iso_string:
|
|
if "-" in iso_string and ":" in iso_string:
|
|
if "." in iso_string:
|
|
format_type = "microseconds"
|
|
else:
|
|
format_type = "extended"
|
|
else:
|
|
format_type = "basic"
|
|
else:
|
|
raise ValueError("String does not appear to be ISO 8601 format")
|
|
|
|
# Map format type to format string
|
|
format_map = {
|
|
"extended": TimestampConverter.ISO_EXTENDED,
|
|
"basic": TimestampConverter.ISO_BASIC,
|
|
"microseconds": TimestampConverter.ISO_WITH_MICROSECONDS,
|
|
"with_timezone": TimestampConverter.ISO_WITH_TIMEZONE,
|
|
}
|
|
|
|
if format_type not in format_map:
|
|
raise ValueError(f"Invalid format_type: {format_type}")
|
|
|
|
format_str = format_map[format_type]
|
|
|
|
try:
|
|
dt = datetime.datetime.strptime(iso_string, format_str)
|
|
except ValueError:
|
|
# Try without microseconds if microseconds format fails
|
|
if format_type == "microseconds":
|
|
dt = datetime.datetime.strptime(
|
|
iso_string, TimestampConverter.ISO_EXTENDED
|
|
)
|
|
else:
|
|
raise
|
|
|
|
return dt
|
|
|
|
@staticmethod
|
|
def now_iso(
|
|
format_type: str = "extended", timezone: Optional[datetime.tzinfo] = None
|
|
) -> str:
|
|
"""
|
|
Get current time as ISO 8601 string.
|
|
|
|
Args:
|
|
format_type: "extended", "basic", "microseconds", or "with_timezone"
|
|
timezone: Optional timezone for the output (defaults to UTC)
|
|
|
|
Returns:
|
|
Current time as ISO 8601 string
|
|
"""
|
|
if timezone:
|
|
now = datetime.datetime.now(timezone)
|
|
else:
|
|
now = datetime.datetime.now(TimestampConverter.UTC)
|
|
|
|
if format_type == "extended":
|
|
format_str = TimestampConverter.ISO_EXTENDED
|
|
elif format_type == "basic":
|
|
format_str = TimestampConverter.ISO_BASIC
|
|
elif format_type == "microseconds":
|
|
format_str = TimestampConverter.ISO_WITH_MICROSECONDS
|
|
elif format_type == "with_timezone":
|
|
format_str = TimestampConverter.ISO_WITH_TIMEZONE
|
|
else:
|
|
raise ValueError(f"Invalid format_type: {format_type}")
|
|
|
|
return now.strftime(format_str)
|
|
|
|
@staticmethod
|
|
def parse_flexible(timestamp: Union[int, float, str]) -> datetime.datetime:
|
|
"""
|
|
Parse a timestamp that could be epoch seconds or ISO string.
|
|
|
|
Args:
|
|
timestamp: Could be epoch (int/float) or ISO string
|
|
|
|
Returns:
|
|
timezone-aware datetime object (in UTC if from epoch)
|
|
"""
|
|
if isinstance(timestamp, (int, float)):
|
|
# Create UTC-aware datetime from epoch
|
|
return datetime.datetime.fromtimestamp(timestamp, tz=TimestampConverter.UTC)
|
|
elif isinstance(timestamp, str):
|
|
# Clean and parse ISO string
|
|
cleaned = timestamp.replace("Z", "+00:00")
|
|
try:
|
|
dt = datetime.datetime.fromisoformat(cleaned)
|
|
# If naive, assume UTC
|
|
if dt.tzinfo is None:
|
|
dt = dt.replace(tzinfo=TimestampConverter.UTC)
|
|
return dt
|
|
except ValueError:
|
|
# Try our custom parser
|
|
return TimestampConverter._parse_custom_iso(cleaned)
|
|
else:
|
|
raise TypeError(f"Unsupported timestamp type: {type(timestamp)}")
|
|
|
|
@staticmethod
|
|
def duration_between(
|
|
start: Union[int, float, str],
|
|
end: Union[int, float, str],
|
|
unit: str = "seconds",
|
|
) -> float:
|
|
"""
|
|
Calculate duration between two timestamps.
|
|
|
|
Args:
|
|
start: Start timestamp (epoch or ISO string)
|
|
end: End timestamp (epoch or ISO string)
|
|
unit: Output unit: "seconds", "minutes", "hours", or "days"
|
|
|
|
Returns:
|
|
Duration in specified unit
|
|
"""
|
|
# Parse timestamps to timezone-aware datetime objects
|
|
dt_start = TimestampConverter.parse_flexible(start)
|
|
dt_end = TimestampConverter.parse_flexible(end)
|
|
|
|
# Ensure both are in UTC for comparison
|
|
dt_start_utc = dt_start.astimezone(TimestampConverter.UTC)
|
|
dt_end_utc = dt_end.astimezone(TimestampConverter.UTC)
|
|
|
|
# Calculate duration
|
|
duration = dt_end_utc - dt_start_utc
|
|
seconds = duration.total_seconds()
|
|
|
|
# Convert to requested unit
|
|
unit_conversion = {
|
|
"seconds": 1,
|
|
"minutes": 1 / 60,
|
|
"hours": 1 / 3600,
|
|
"days": 1 / 86400,
|
|
}
|
|
|
|
if unit not in unit_conversion:
|
|
raise ValueError(f"Invalid unit: {unit}")
|
|
|
|
return seconds * unit_conversion[unit]
|
|
|
|
@staticmethod
|
|
def to_utc(dt: datetime.datetime) -> datetime.datetime:
|
|
"""
|
|
Convert any datetime to UTC timezone-aware datetime.
|
|
|
|
Args:
|
|
dt: datetime object (naive or timezone-aware)
|
|
|
|
Returns:
|
|
UTC timezone-aware datetime
|
|
"""
|
|
if dt.tzinfo is None:
|
|
# Naive datetime - assume UTC
|
|
return dt.replace(tzinfo=TimestampConverter.UTC)
|
|
else:
|
|
# Convert to UTC
|
|
return dt.astimezone(TimestampConverter.UTC)
|
|
|
|
@staticmethod
|
|
def get_timezone(name: str = "UTC") -> datetime.tzinfo:
|
|
"""
|
|
Get timezone object by name.
|
|
|
|
Args:
|
|
name: Timezone name (e.g., "UTC", "Europe/Berlin")
|
|
|
|
Returns:
|
|
timezone object
|
|
|
|
Note:
|
|
For named timezones beyond UTC, install pytz or use zoneinfo (Python 3.9+)
|
|
"""
|
|
if name.upper() == "UTC":
|
|
return TimestampConverter.UTC
|
|
|
|
# Try zoneinfo (Python 3.9+)
|
|
try:
|
|
from zoneinfo import ZoneInfo
|
|
|
|
return ZoneInfo(name)
|
|
except ImportError:
|
|
pass
|
|
|
|
# Try pytz
|
|
try:
|
|
import pytz
|
|
|
|
return pytz.timezone(name)
|
|
except ImportError:
|
|
raise ImportError(
|
|
f"Need pytz or zoneinfo to use timezone '{name}'. "
|
|
f"Install with: pip install pytz"
|
|
)
|