mirror of https://github.com/caronc/apprise
initial commit - work in progress - global timezone setting
parent
52801b6369
commit
886fd253ee
|
@ -25,6 +25,7 @@
|
|||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from datetime import datetime, tzinfo
|
||||
from os.path import abspath, dirname, isfile, join
|
||||
import re
|
||||
from typing import Any, Optional, Union
|
||||
|
@ -37,6 +38,7 @@ from .common import (
|
|||
PersistentStoreMode,
|
||||
)
|
||||
from .manager_plugins import NotificationManager
|
||||
from .utils.time import zoneinfo
|
||||
|
||||
# Grant access to our Notification Manager Singleton
|
||||
N_MGR = NotificationManager()
|
||||
|
@ -205,6 +207,14 @@ class AppriseAsset:
|
|||
# A unique identifer we can use to associate our calling source
|
||||
_uid = str(uuid4())
|
||||
|
||||
# Default timezone to use (pass in timezone value)
|
||||
# A list of timezones can be found here:
|
||||
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
# You can specify things such as 'America/Montreal'
|
||||
# If no timezone is specified, then the one detected on the system
|
||||
# is uzed
|
||||
_tzinfo = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
plugin_paths: Optional[list[str]] = None,
|
||||
|
@ -212,6 +222,7 @@ class AppriseAsset:
|
|||
storage_mode: Optional[Union[str, PersistentStoreMode]] = None,
|
||||
storage_salt: Optional[Union[str, bytes]] = None,
|
||||
storage_idlen: Optional[int] = None,
|
||||
timezone: Optional[Union[str, tzinfo]] = None,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
"""Asset Initialization."""
|
||||
|
@ -259,6 +270,18 @@ class AppriseAsset:
|
|||
# Store value
|
||||
self.__storage_idlen = storage_idlen
|
||||
|
||||
if isinstance(timezone, tzinfo):
|
||||
self._tzinfo = timezone
|
||||
|
||||
elif timezone is not None:
|
||||
self._tzinfo = zoneinfo(timezone)
|
||||
if not self._tzinfo:
|
||||
raise AttributeError(
|
||||
"AppriseAsset timezone provided is invalid") from None
|
||||
else:
|
||||
# Default our timezone to what is detected on the system
|
||||
self._tzinfo = datetime.now().astimezone().tzinfo
|
||||
|
||||
if storage_salt is not None:
|
||||
# Define the number of characters utilized from our namespace lengh
|
||||
|
||||
|
@ -484,3 +507,8 @@ class AppriseAsset:
|
|||
"""Return the persistent storage id length."""
|
||||
|
||||
return self.__storage_idlen
|
||||
|
||||
@property
|
||||
def tzinfo(self) -> tzinfo:
|
||||
"""Return the timezone object"""
|
||||
return self._tzinfo
|
||||
|
|
|
@ -37,6 +37,7 @@ from ..asset import AppriseAsset
|
|||
from ..manager_config import ConfigurationManager
|
||||
from ..manager_plugins import NotificationManager
|
||||
from ..url import URLBase
|
||||
from ..utils.time import zoneinfo
|
||||
from ..utils.cwe312 import cwe312_url
|
||||
from ..utils.parse import GET_SCHEMA_RE, parse_bool, parse_list, parse_urls
|
||||
|
||||
|
@ -890,10 +891,30 @@ class ConfigBase(URLBase):
|
|||
# global asset object
|
||||
#
|
||||
asset = asset if isinstance(asset, AppriseAsset) else AppriseAsset()
|
||||
|
||||
# Prepare our default timezone
|
||||
default_timezone = asset.tzinfo
|
||||
|
||||
# Acquire our asset tokens
|
||||
tokens = result.get("asset", None)
|
||||
if tokens and isinstance(tokens, dict):
|
||||
for k, v in tokens.items():
|
||||
|
||||
# Prepare our default timezone (if specified)
|
||||
timezone = str(tokens.get('timezone', tokens.get('tz', '')))
|
||||
if timezone:
|
||||
default_timezone = zoneinfo(re.sub(r'[^\w/-]+', '', timezone))
|
||||
if not default_timezone:
|
||||
ConfigBase.logger.warning(
|
||||
'Ignored invalid timezone "%s"', timezone)
|
||||
# Restore our timezone back to what was found in the
|
||||
# asset object
|
||||
default_timezone = asset.tzinfo
|
||||
else:
|
||||
# Set our newly specified timezone
|
||||
setattr(asset, '_tzinfo', default_timezone)
|
||||
|
||||
# Iterate over remaining tokens
|
||||
for k, v in tokens.items():
|
||||
if k.startswith("_") or k.endswith("_"):
|
||||
# Entries are considered reserved if they start or end
|
||||
# with an underscore
|
||||
|
|
|
@ -30,6 +30,7 @@ from collections.abc import Generator
|
|||
from functools import partial
|
||||
import re
|
||||
from typing import Any, ClassVar, Optional, TypedDict, Union
|
||||
from zoneinfo import available_timezones
|
||||
|
||||
from ..apprise_attachment import AppriseAttachment
|
||||
from ..common import (
|
||||
|
@ -141,6 +142,9 @@ class NotifyBase(URLBase):
|
|||
# Persistent storage default settings
|
||||
persistent_storage = True
|
||||
|
||||
# Timezone Default
|
||||
timezone = 'UTC'
|
||||
|
||||
# Default Notify Format
|
||||
notify_format = NotifyFormat.TEXT
|
||||
|
||||
|
@ -225,6 +229,17 @@ class NotifyBase(URLBase):
|
|||
# runtime.
|
||||
"_lookup_default": "persistent_storage",
|
||||
},
|
||||
"tz": {
|
||||
"name": _("Timezone"),
|
||||
"type": "choice:string",
|
||||
# Supported timezones
|
||||
"values": available_timezones(),
|
||||
# Provide a default
|
||||
"default": timezone,
|
||||
# look up default using the following parent class value at
|
||||
# runtime.
|
||||
"_lookup_default": "timezone",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2025, Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
import contextlib
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
from ..logger import logger
|
||||
|
||||
|
||||
def zoneinfo(name: str) -> ZoneInfo:
|
||||
"""
|
||||
More forgiving ZoneInfo instantiation
|
||||
- Accepts lower/upper case
|
||||
- Normalises common UTC variants
|
||||
"""
|
||||
if not isinstance(name, str):
|
||||
return None
|
||||
|
||||
# Normalise common UTC spellings
|
||||
if name.lower() in {"utc", "z", "gmt"}:
|
||||
name = "UTC"
|
||||
|
||||
# Try exact match first
|
||||
try:
|
||||
return ZoneInfo(name)
|
||||
|
||||
except ValueError:
|
||||
# Just all around bad data; no need to proceed
|
||||
return None
|
||||
|
||||
except ZoneInfoNotFoundError:
|
||||
pass
|
||||
|
||||
# Try case-insensitive match across available keys
|
||||
from zoneinfo import available_timezones
|
||||
lowered = name.lower().strip()
|
||||
for zone in available_timezones():
|
||||
full_zone = zone.lower()
|
||||
if full_zone == lowered:
|
||||
return ZoneInfo(zone)
|
||||
|
||||
with contextlib.suppress(IndexError):
|
||||
location = full_zone.split("/")[1:][0]
|
||||
if location and location == lowered:
|
||||
return ZoneInfo(zone)
|
||||
|
||||
logger.warning("Unknown timezone specified: %s", name)
|
||||
return None
|
|
@ -0,0 +1,56 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2025, Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# Disable logging for a cleaner testing output
|
||||
from datetime import tzinfo
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from apprise.asset import AppriseAsset
|
||||
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
# Ensure we don't create .pyc files for these tests
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
|
||||
def test_timezone():
|
||||
"asset: timezone() testing"
|
||||
asset = AppriseAsset(timezone="utc")
|
||||
assert isinstance(asset.tzinfo, tzinfo)
|
||||
|
||||
# Default (uses system value)
|
||||
asset = AppriseAsset(timezone=None)
|
||||
assert isinstance(asset.tzinfo, tzinfo)
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
AppriseAsset(timezone=object)
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
AppriseAsset(timezone="invalid")
|
|
@ -25,6 +25,7 @@
|
|||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from datetime import tzinfo
|
||||
from inspect import cleandoc
|
||||
|
||||
# Disable logging for a cleaner testing output
|
||||
|
@ -3233,3 +3234,23 @@ def test_bytes_to_str():
|
|||
# Support strings too
|
||||
assert utils.disk.bytes_to_str("0") == "0.00B"
|
||||
assert utils.disk.bytes_to_str("1024") == "1.00KB"
|
||||
|
||||
|
||||
def test_zoneinfo():
|
||||
"""utils: zoneinfo() testing"""
|
||||
|
||||
# Some valid strings
|
||||
assert isinstance(utils.time.zoneinfo("UTC"), tzinfo)
|
||||
assert isinstance(utils.time.zoneinfo("z"), tzinfo)
|
||||
assert isinstance(utils.time.zoneinfo("gmt"), tzinfo)
|
||||
assert isinstance(utils.time.zoneinfo("utc"), tzinfo)
|
||||
assert isinstance(utils.time.zoneinfo("Toronto"), tzinfo)
|
||||
assert isinstance(utils.time.zoneinfo("America/Toronto"), tzinfo)
|
||||
assert isinstance(utils.time.zoneinfo("america/toronto"), tzinfo)
|
||||
|
||||
# bad data
|
||||
assert utils.time.zoneinfo(object) is None
|
||||
assert utils.time.zoneinfo(None) is None
|
||||
assert utils.time.zoneinfo(1) is None
|
||||
assert utils.time.zoneinfo("") is None
|
||||
assert utils.time.zoneinfo("invalid") is None
|
||||
|
|
|
@ -33,6 +33,8 @@ import logging
|
|||
import pytest
|
||||
import yaml
|
||||
|
||||
from datetime import tzinfo
|
||||
from apprise.utils.time import zoneinfo
|
||||
from apprise import Apprise, AppriseAsset, ConfigFormat
|
||||
from apprise.config import ConfigBase
|
||||
from apprise.plugins.email import NotifyEmail
|
||||
|
@ -1005,6 +1007,9 @@ asset:
|
|||
|
||||
image_path_mask: tmp/path
|
||||
|
||||
# Timezone (supports tz keyword too)
|
||||
tz: America/Montreal
|
||||
|
||||
# invalid entry
|
||||
theme:
|
||||
-
|
||||
|
@ -1042,6 +1047,10 @@ urls:
|
|||
# Boolean types stay boolean
|
||||
assert asset.async_mode is False
|
||||
|
||||
# Our TimeZone
|
||||
assert isinstance(asset.tzinfo, tzinfo)
|
||||
assert asset.tzinfo.key == zoneinfo('America/Montreal').key
|
||||
|
||||
# the theme was not updated and remains the same as it was
|
||||
assert asset.theme == AppriseAsset().theme
|
||||
|
||||
|
@ -1067,6 +1076,9 @@ asset:
|
|||
app_desc: Apprise Test Notifications
|
||||
app_url: http://nuxref.com
|
||||
|
||||
# An invalid timezone
|
||||
timezone: invalid
|
||||
|
||||
# Optionally define some global tags to associate with ALL of your
|
||||
# urls below.
|
||||
tag: admin, devops
|
||||
|
|
Loading…
Reference in New Issue