mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-10-11 14:31:25 +00:00
Build and push containers / metadata (push) Canceled after 0s
Build and push containers / build-push-containers (push) Canceled after 0s
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Canceled after 0s
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built package works basically. (push) Canceled after 0s
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Canceled after 0s
ChangeDetection.io App Test / lint-code (push) Canceled after 0s
ChangeDetection.io App Test / lint-translations (push) Canceled after 0s
ChangeDetection.io App Test / lint-template-i18n (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-11 (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-12 (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-13 (push) Canceled after 0s
ChangeDetection.io App Test / test-application-3-14 (push) Canceled after 0s
118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
"""
|
|
Unix-timestamp conversion filter plugin for Jinja2 templates.
|
|
|
|
Provides the ``unixtime`` filter, which converts a Unix timestamp (seconds,
|
|
milliseconds or microseconds) into a human-readable, strftime-formatted string.
|
|
|
|
Requested in #3641: API/JSON-style monitored pages often expose values such as
|
|
``last_battle_at: 1759188682`` and notifications printed the raw integer.
|
|
"""
|
|
import datetime
|
|
import os
|
|
import re
|
|
import typing as t
|
|
|
|
from loguru import logger
|
|
|
|
# Bound the size of input we will parse, mirroring the defensive limits used by
|
|
# the other Jinja plugins. A notification token should never be multi-MB.
|
|
MAX_INPUT_SIZE = 1024 * 1024 # 1 MB
|
|
|
|
# Default output format (overridable per call) and default timezone.
|
|
DEFAULT_FORMAT = "%Y-%m-%d %H:%M:%S %Z"
|
|
|
|
# A bare 10+ digit run is enough to locate a timestamp embedded in prose such
|
|
# as "last_battle_at: 1759188682" without accepting unrelated small numbers.
|
|
_TIMESTAMP_RE = re.compile(r"-?\d{9,}")
|
|
|
|
|
|
def _default_tzname() -> str:
|
|
return os.getenv("TZ", "UTC").strip() or "UTC"
|
|
|
|
|
|
def _resolve_timezone(tz_name: t.Optional[str]):
|
|
"""Return a tzinfo for tz_name, defaulting to UTC without third-party deps."""
|
|
name = (tz_name or _default_tzname()).strip()
|
|
if name.upper() in ("", "UTC"):
|
|
return datetime.timezone.utc
|
|
# datetime.timezone only supports fixed offsets; resolve common named zones
|
|
# via the UTC-offset environment when available. Avoid importing zoneinfo on
|
|
# runtimes without the tz database by falling back to UTC.
|
|
try:
|
|
from zoneinfo import ZoneInfo # stdlib on Python 3.9+
|
|
|
|
return ZoneInfo(name)
|
|
except Exception:
|
|
return datetime.timezone.utc
|
|
|
|
|
|
def _coerce_timestamp(raw: t.Any) -> t.Optional[int]:
|
|
"""
|
|
Extract a Unix timestamp in seconds from ints, floats, numeric strings or
|
|
prose containing one. Millisecond (13-digit) and microsecond (16-digit)
|
|
values are normalised to seconds. Returns None when no value is usable.
|
|
"""
|
|
if raw is None:
|
|
return None
|
|
|
|
if isinstance(raw, bool): # bool is a subclass of int — never treat as ts
|
|
return None
|
|
|
|
if isinstance(raw, (int, float)):
|
|
value = int(raw)
|
|
else:
|
|
text = str(raw)
|
|
if len(text) > MAX_INPUT_SIZE:
|
|
logger.warning("unixtime: input too large (%d bytes), truncating", len(text))
|
|
text = text[:MAX_INPUT_SIZE]
|
|
match = _TIMESTAMP_RE.search(text)
|
|
if not match:
|
|
return None
|
|
value = int(match.group(0))
|
|
|
|
if abs(value) >= 1_000_000_000_000_000: # microseconds
|
|
value //= 1_000_000
|
|
elif abs(value) >= 1_000_000_000_000: # milliseconds
|
|
value //= 1_000
|
|
# Anything >= ~1e9 is plausible as seconds. Smaller 9-digit values
|
|
# (e.g. 999999999 ≈ 2001) are still accepted by the regex.
|
|
return value
|
|
|
|
|
|
def unixtime(
|
|
value: t.Any,
|
|
fmt: str = DEFAULT_FORMAT,
|
|
tz_name: t.Optional[str] = None,
|
|
) -> t.Any:
|
|
"""
|
|
Format a Unix timestamp as a human-readable datetime.
|
|
|
|
Args:
|
|
value: timestamp as int/float/string, or text containing one. Seconds,
|
|
milliseconds and microseconds are all accepted.
|
|
fmt: ``strftime`` format string (default ``%Y-%m-%d %H:%M:%S %Z``).
|
|
tz_name: timezone name (default: ``$TZ`` or UTC).
|
|
|
|
Returns:
|
|
The formatted datetime string. On any failure (missing/invalid value or
|
|
format) the original value is returned unchanged, so a notification
|
|
never crashes and simply falls back to the raw token.
|
|
|
|
Examples:
|
|
{{ triggered_text | unixtime }}
|
|
{{ triggered_text | unixtime("%d.%m.%Y %H:%M") }}
|
|
{{ triggered_text | unixtime("%d.%m.%Y %H:%M", "Europe/Berlin") }}
|
|
"""
|
|
seconds = _coerce_timestamp(value)
|
|
if seconds is None:
|
|
return value
|
|
|
|
try:
|
|
tzinfo = _resolve_timezone(tz_name)
|
|
dt = datetime.datetime.fromtimestamp(seconds, tz=tzinfo)
|
|
return dt.strftime(fmt)
|
|
except (ValueError, OSError, OverflowError) as e:
|
|
# Bad format string or out-of-range timestamp — keep the raw token.
|
|
logger.warning("unixtime: could not format value %r (%s)", value, e)
|
|
return value
|