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
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
from datetime import datetime, tzinfo
|
||||||
from os.path import abspath, dirname, isfile, join
|
from os.path import abspath, dirname, isfile, join
|
||||||
import re
|
import re
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
@ -37,6 +38,7 @@ from .common import (
|
||||||
PersistentStoreMode,
|
PersistentStoreMode,
|
||||||
)
|
)
|
||||||
from .manager_plugins import NotificationManager
|
from .manager_plugins import NotificationManager
|
||||||
|
from .utils.time import zoneinfo
|
||||||
|
|
||||||
# Grant access to our Notification Manager Singleton
|
# Grant access to our Notification Manager Singleton
|
||||||
N_MGR = NotificationManager()
|
N_MGR = NotificationManager()
|
||||||
|
@ -205,6 +207,14 @@ class AppriseAsset:
|
||||||
# A unique identifer we can use to associate our calling source
|
# A unique identifer we can use to associate our calling source
|
||||||
_uid = str(uuid4())
|
_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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
plugin_paths: Optional[list[str]] = None,
|
plugin_paths: Optional[list[str]] = None,
|
||||||
|
@ -212,6 +222,7 @@ class AppriseAsset:
|
||||||
storage_mode: Optional[Union[str, PersistentStoreMode]] = None,
|
storage_mode: Optional[Union[str, PersistentStoreMode]] = None,
|
||||||
storage_salt: Optional[Union[str, bytes]] = None,
|
storage_salt: Optional[Union[str, bytes]] = None,
|
||||||
storage_idlen: Optional[int] = None,
|
storage_idlen: Optional[int] = None,
|
||||||
|
timezone: Optional[Union[str, tzinfo]] = None,
|
||||||
**kwargs: Any
|
**kwargs: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Asset Initialization."""
|
"""Asset Initialization."""
|
||||||
|
@ -259,6 +270,18 @@ class AppriseAsset:
|
||||||
# Store value
|
# Store value
|
||||||
self.__storage_idlen = storage_idlen
|
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:
|
if storage_salt is not None:
|
||||||
# Define the number of characters utilized from our namespace lengh
|
# Define the number of characters utilized from our namespace lengh
|
||||||
|
|
||||||
|
@ -484,3 +507,8 @@ class AppriseAsset:
|
||||||
"""Return the persistent storage id length."""
|
"""Return the persistent storage id length."""
|
||||||
|
|
||||||
return self.__storage_idlen
|
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_config import ConfigurationManager
|
||||||
from ..manager_plugins import NotificationManager
|
from ..manager_plugins import NotificationManager
|
||||||
from ..url import URLBase
|
from ..url import URLBase
|
||||||
|
from ..utils.time import zoneinfo
|
||||||
from ..utils.cwe312 import cwe312_url
|
from ..utils.cwe312 import cwe312_url
|
||||||
from ..utils.parse import GET_SCHEMA_RE, parse_bool, parse_list, parse_urls
|
from ..utils.parse import GET_SCHEMA_RE, parse_bool, parse_list, parse_urls
|
||||||
|
|
||||||
|
@ -890,10 +891,30 @@ class ConfigBase(URLBase):
|
||||||
# global asset object
|
# global asset object
|
||||||
#
|
#
|
||||||
asset = asset if isinstance(asset, AppriseAsset) else AppriseAsset()
|
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)
|
tokens = result.get("asset", None)
|
||||||
if tokens and isinstance(tokens, dict):
|
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("_"):
|
if k.startswith("_") or k.endswith("_"):
|
||||||
# Entries are considered reserved if they start or end
|
# Entries are considered reserved if they start or end
|
||||||
# with an underscore
|
# with an underscore
|
||||||
|
|
|
@ -30,6 +30,7 @@ from collections.abc import Generator
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import re
|
import re
|
||||||
from typing import Any, ClassVar, Optional, TypedDict, Union
|
from typing import Any, ClassVar, Optional, TypedDict, Union
|
||||||
|
from zoneinfo import available_timezones
|
||||||
|
|
||||||
from ..apprise_attachment import AppriseAttachment
|
from ..apprise_attachment import AppriseAttachment
|
||||||
from ..common import (
|
from ..common import (
|
||||||
|
@ -141,6 +142,9 @@ class NotifyBase(URLBase):
|
||||||
# Persistent storage default settings
|
# Persistent storage default settings
|
||||||
persistent_storage = True
|
persistent_storage = True
|
||||||
|
|
||||||
|
# Timezone Default
|
||||||
|
timezone = 'UTC'
|
||||||
|
|
||||||
# Default Notify Format
|
# Default Notify Format
|
||||||
notify_format = NotifyFormat.TEXT
|
notify_format = NotifyFormat.TEXT
|
||||||
|
|
||||||
|
@ -225,6 +229,17 @@ class NotifyBase(URLBase):
|
||||||
# runtime.
|
# runtime.
|
||||||
"_lookup_default": "persistent_storage",
|
"_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
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
# POSSIBILITY OF SUCH DAMAGE.
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
from datetime import tzinfo
|
||||||
from inspect import cleandoc
|
from inspect import cleandoc
|
||||||
|
|
||||||
# Disable logging for a cleaner testing output
|
# Disable logging for a cleaner testing output
|
||||||
|
@ -3233,3 +3234,23 @@ def test_bytes_to_str():
|
||||||
# Support strings too
|
# Support strings too
|
||||||
assert utils.disk.bytes_to_str("0") == "0.00B"
|
assert utils.disk.bytes_to_str("0") == "0.00B"
|
||||||
assert utils.disk.bytes_to_str("1024") == "1.00KB"
|
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 pytest
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from datetime import tzinfo
|
||||||
|
from apprise.utils.time import zoneinfo
|
||||||
from apprise import Apprise, AppriseAsset, ConfigFormat
|
from apprise import Apprise, AppriseAsset, ConfigFormat
|
||||||
from apprise.config import ConfigBase
|
from apprise.config import ConfigBase
|
||||||
from apprise.plugins.email import NotifyEmail
|
from apprise.plugins.email import NotifyEmail
|
||||||
|
@ -1005,6 +1007,9 @@ asset:
|
||||||
|
|
||||||
image_path_mask: tmp/path
|
image_path_mask: tmp/path
|
||||||
|
|
||||||
|
# Timezone (supports tz keyword too)
|
||||||
|
tz: America/Montreal
|
||||||
|
|
||||||
# invalid entry
|
# invalid entry
|
||||||
theme:
|
theme:
|
||||||
-
|
-
|
||||||
|
@ -1042,6 +1047,10 @@ urls:
|
||||||
# Boolean types stay boolean
|
# Boolean types stay boolean
|
||||||
assert asset.async_mode is False
|
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
|
# the theme was not updated and remains the same as it was
|
||||||
assert asset.theme == AppriseAsset().theme
|
assert asset.theme == AppriseAsset().theme
|
||||||
|
|
||||||
|
@ -1067,6 +1076,9 @@ asset:
|
||||||
app_desc: Apprise Test Notifications
|
app_desc: Apprise Test Notifications
|
||||||
app_url: http://nuxref.com
|
app_url: http://nuxref.com
|
||||||
|
|
||||||
|
# An invalid timezone
|
||||||
|
timezone: invalid
|
||||||
|
|
||||||
# Optionally define some global tags to associate with ALL of your
|
# Optionally define some global tags to associate with ALL of your
|
||||||
# urls below.
|
# urls below.
|
||||||
tag: admin, devops
|
tag: admin, devops
|
||||||
|
|
Loading…
Reference in New Issue