diff --git a/changedetectionio/conditions/default_plugin.py b/changedetectionio/conditions/default_plugin.py
index ffc1f7296..3dc456952 100644
--- a/changedetectionio/conditions/default_plugin.py
+++ b/changedetectionio/conditions/default_plugin.py
@@ -1,7 +1,6 @@
import re
import pluggy
-from price_parser import Price
from loguru import logger
from flask_babel import lazy_gettext as _l
@@ -70,6 +69,7 @@ def register_field_choices():
@hookimpl
def add_data(current_watch_uuid, application_datastruct, ephemeral_data):
+ from price_parser import Price
res = {}
if 'text' in ephemeral_data:
res['page_filtered_text'] = ephemeral_data['text']
diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py
index 109fe0bfe..facb8de4b 100644
--- a/changedetectionio/forms.py
+++ b/changedetectionio/forms.py
@@ -696,14 +696,14 @@ class ValidateCSSJSONXPATHInput(object):
raise ValidationError("XPath not permitted in this field!")
from lxml import etree, html
import elementpath
- from changedetectionio.html_tools import SafeXPath3Parser, lxml_guard, lxml_html_parser
+ from changedetectionio.html_tools import get_safe_xpath3_parser, lxml_guard, lxml_html_parser
line = line.replace('xpath:', '')
try:
# Runs on a Flask request thread - must share the worker's lxml lock.
with lxml_guard():
tree = html.fromstring("", parser=lxml_html_parser())
- elementpath.select(tree, line.strip(), parser=SafeXPath3Parser)
+ elementpath.select(tree, line.strip(), parser=get_safe_xpath3_parser())
except elementpath.ElementPathError as e:
message = field.gettext('\'%(expression)s\' is not a valid XPath expression. (%(error)s)')
raise ValidationError(message % {'expression': line, 'error': str(e)})
diff --git a/changedetectionio/html_tools.py b/changedetectionio/html_tools.py
index ae869c04d..8d4504e3d 100644
--- a/changedetectionio/html_tools.py
+++ b/changedetectionio/html_tools.py
@@ -164,7 +164,8 @@ _DEFAULT_UNSAFE_XPATH3_FUNCTIONS = [
]
-def _build_safe_xpath3_parser():
+@lru_cache(maxsize=1)
+def get_safe_xpath3_parser():
"""Return an XPath3Parser subclass with filesystem/environment access functions removed.
XPath 3.0 includes functions that can read arbitrary files or environment variables:
@@ -196,9 +197,6 @@ def _build_safe_xpath3_parser():
return SafeXPath3Parser
-# Module-level singleton — built once, reused everywhere.
-SafeXPath3Parser = _build_safe_xpath3_parser()
-
# Doesn't look like python supports forward slash auto enclosure in re.findall
# So convert it to inline flag "(?i)foobar" type configuration
@lru_cache(maxsize=100)
@@ -386,7 +384,7 @@ def xpath_filter(xpath_filter, html_content, append_pretty_line_formatting=False
# This allows //title to match elements in the default namespace
namespaces[''] = tree.nsmap[None]
- r = elementpath.select(tree, xpath_filter.strip(), namespaces=namespaces, parser=SafeXPath3Parser)
+ r = elementpath.select(tree, xpath_filter.strip(), namespaces=namespaces, parser=get_safe_xpath3_parser())
#@note: //title/text() now works with default namespaces (fixed by registering '' prefix)
#@note: //title/text() wont work where
CDATA.. (use cdata_in_document_to_text first)
diff --git a/changedetectionio/jinja2_custom/extensions/TimeExtension.py b/changedetectionio/jinja2_custom/extensions/TimeExtension.py
index 9becdda7b..15e8f5c7f 100644
--- a/changedetectionio/jinja2_custom/extensions/TimeExtension.py
+++ b/changedetectionio/jinja2_custom/extensions/TimeExtension.py
@@ -80,7 +80,6 @@ Note:
This extension uses the Arrow library for timezone-aware datetime handling.
All timezone names should be valid IANA timezone identifiers (e.g., 'America/New_York').
"""
-import arrow
from jinja2 import nodes
from jinja2.ext import Extension
@@ -125,6 +124,7 @@ class TimeExtension(Extension):
_datetime('UTC', '+', 'hours=2,minutes=30', '%Y-%m-%d %H:%M:%S')
# Returns current time + 2.5 hours
"""
+ import arrow
# Use default timezone if none specified
if not timezone or timezone == '':
timezone = self.environment.default_timezone
@@ -162,6 +162,7 @@ class TimeExtension(Extension):
_now('America/New_York', '%Y-%m-%d %H:%M:%S')
# Returns current time in New York timezone
"""
+ import arrow
# Use default timezone if none specified
if not timezone or timezone == '':
timezone = self.environment.default_timezone
diff --git a/changedetectionio/notification/handler.py b/changedetectionio/notification/handler.py
index 6329bdc58..cca332873 100644
--- a/changedetectionio/notification/handler.py
+++ b/changedetectionio/notification/handler.py
@@ -1,11 +1,8 @@
import time
import re
-import apprise
-from apprise import NotifyFormat
from loguru import logger
from urllib.parse import urlparse
-from .apprise_plugin.assets import apprise_asset, APPRISE_AVATAR_URL
from .email_helpers import as_monospaced_html_email
from ..diff import HTML_REMOVED_STYLE, REMOVED_PLACEMARKER_OPEN, REMOVED_PLACEMARKER_CLOSED, ADDED_PLACEMARKER_OPEN, HTML_ADDED_STYLE, \
ADDED_PLACEMARKER_CLOSED, CHANGED_INTO_PLACEMARKER_OPEN, CHANGED_INTO_PLACEMARKER_CLOSED, CHANGED_PLACEMARKER_OPEN, \
@@ -64,6 +61,7 @@ def notification_format_align_with_apprise(n_format : str):
:param n_format:
:return:
"""
+ from apprise import NotifyFormat
if not n_format:
return NotifyFormat.TEXT.value
@@ -208,6 +206,7 @@ def replace_placemarkers_in_text(text, url, requested_output_format):
def apply_service_tweaks(url, n_body, n_title, requested_output_format):
+ from .apprise_plugin.assets import APPRISE_AVATAR_URL
logger.debug(f"Applying markup in '{requested_output_format}' mode")
# Re 323 - Limit discord length to their 2000 char limit total or it wont send.
@@ -305,6 +304,9 @@ def apply_service_tweaks(url, n_body, n_title, requested_output_format):
def process_notification(n_object: NotificationContextData, datastore):
+ import apprise
+ from apprise import NotifyFormat
+ from .apprise_plugin.assets import apprise_asset
from changedetectionio.jinja2_custom import render as jinja_render
from . import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH, default_notification_format, valid_notification_formats
# be sure its registered
diff --git a/changedetectionio/time_handler.py b/changedetectionio/time_handler.py
index 9def380a2..0081c48f0 100644
--- a/changedetectionio/time_handler.py
+++ b/changedetectionio/time_handler.py
@@ -1,6 +1,5 @@
from functools import lru_cache
-import arrow
from enum import IntEnum
@@ -32,6 +31,7 @@ def am_i_inside_time(
Returns:
bool: True if the current time is within the time range, False otherwise.
"""
+ import arrow
# Parse the target day of the week
try:
target_weekday = Weekday[day_of_week.capitalize()]
@@ -91,6 +91,7 @@ def is_within_schedule(time_schedule_limit, default_tz="UTC"):
Returns:
bool: True if current time is within the schedule, False otherwise.
"""
+ import arrow
if time_schedule_limit and time_schedule_limit.get('enabled'):
# Get the timezone the time schedule is in, so we know what day it is there
tz_name = time_schedule_limit.get('timezone')