Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
dgtlmoon committed Dec 3, 2024
1 parent 47aebd6 commit 326e36a
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 55 deletions.
58 changes: 42 additions & 16 deletions changedetectionio/flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from changedetectionio import html_tools, __version__
from changedetectionio import queuedWatchMetaData
from changedetectionio.api import api_v1
from .time_handler import is_within_schedule

datastore = None

Expand Down Expand Up @@ -807,16 +808,31 @@ def edit_page(uuid):
datastore.needs_write_urgent = True

# Do not queue on edit if its not within the time range
# @todo connect with watch.get('time_between_check_use_default')

# @todo maybe it should never queue anyway on edit...
is_in_schedule = True
time_schedule_limit = datastore.data['watching'][uuid].get('time_schedule_limit')
if time_schedule_limit and time_schedule_limit.get('enabled'):
is_in_schedule = datastore.data['watching'][uuid].watch_recheck_is_within_schedule(
default_tz=datastore.data['settings']['application'].get('timezone', 'UTC')
)
watch = datastore.data['watching'].get(uuid)

if watch.get('time_between_check_use_default'):
time_schedule_limit = datastore.data['settings']['requests'].get('time_schedule_limit', {})
else:
time_schedule_limit = watch.get('time_schedule_limit')

tz_name = time_schedule_limit.get('timezone')
if not tz_name:
tz_name = datastore.data['settings']['application'].get('timezone', 'UTC')

if time_schedule_limit and time_schedule_limit.get('enabled'):
try:
is_in_schedule = is_within_schedule(time_schedule_limit=time_schedule_limit,
default_tz=tz_name
)
except Exception as e:
logger.error(
f"{uuid} - Recheck scheduler, error handling timezone, check skipped - TZ name '{tz_name}' - {str(e)}")
return False

#############################
if not datastore.data['watching'][uuid].get('paused') and is_in_schedule:
# Queue the watch for immediate recheck, with a higher priority
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid}))
Expand Down Expand Up @@ -1801,18 +1817,28 @@ def ticker_thread_check_time_launch_checks():
if watch['paused']:
continue

# @todo - Maybe make this a hook?
# Time schedule limit - Decide between watch or global settings
if watch.get('time_between_check_use_default'):
time_schedule_limit = datastore.data['settings']['requests'].get('time_schedule_limit', {})
logger.trace(f"{uuid} Time scheduler - Using system/global settings")
else:
time_schedule_limit = watch.get('time_schedule_limit')
logger.trace(f"{uuid} Time scheduler - Using watch settings (not global settings)")
tz_name = datastore.data['settings']['application'].get('timezone', 'UTC')

# Maybe make this a hook?

# Check if we are inside the time range
# @todo connect with watch.get('time_between_check_use_default')
time_schedule_limit = watch.get('time_schedule_limit')
if time_schedule_limit and time_schedule_limit.get('enabled'):
result = watch.watch_recheck_is_within_schedule(default_tz=datastore.data['settings']['application'].get('timezone', 'UTC'))
if not result:
logger.trace(f"{uuid} Time scheduler - not within schedule skipping.")
continue

try:
result = is_within_schedule(time_schedule_limit=time_schedule_limit,
default_tz=tz_name
)
if not result:
logger.trace(f"{uuid} Time scheduler - not within schedule skipping.")
continue
except Exception as e:
logger.error(
f"{uuid} - Recheck scheduler, error handling timezone, check skipped - TZ name '{tz_name}' - {str(e)}")
return False
# If they supplied an individual entry minutes to threshold.
threshold = recheck_time_system_seconds if watch.get('time_between_check_use_default') else watch.threshold_seconds()

Expand Down
34 changes: 2 additions & 32 deletions changedetectionio/model/Watch.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from more_itertools.more import time_limited

from changedetectionio.strtobool import strtobool
from changedetectionio.safe_jinja import render as jinja_render
from changedetectionio.time_handler import am_i_inside_time
Expand Down Expand Up @@ -631,38 +633,6 @@ def _prune_last_fetched_html_snapshots(self):
if index > 1 and os.path.isfile(filepath):
os.remove(filepath)

def watch_recheck_is_within_schedule(self, default_tz="UTC"):
from datetime import datetime

# Check if we are inside the time range
time_schedule_limit = self.get('time_schedule_limit')
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')
if not tz_name:
tz_name = default_tz

try:
now_day_name_in_tz = datetime.now(ZoneInfo(tz_name.strip())).strftime('%A')
except Exception as e:
logger.error(
f"{self.get('uuid')} - Recheck scheduler, error handling timezone, check skipped - TZ name '{tz_name}' - {str(e)}")
return False

selected_day_schedule = time_schedule_limit.get(now_day_name_in_tz.lower())
if not selected_day_schedule.get('enabled'):
logger.trace(f"{self.get('uuid')} - Skipped check for {now_day_name_in_tz} in {tz_name}, not enabled.")
return False

duration = selected_day_schedule.get('duration')
selected_day_run_duration_m = int(duration.get('hours')) * 60 + int(duration.get('minutes'))

is_valid = am_i_inside_time(day_of_week=now_day_name_in_tz,
time_str=selected_day_schedule['start_time'],
timezone_str=tz_name,
duration=selected_day_run_duration_m)

return is_valid

@property
def get_browsersteps_available_screenshots(self):
Expand Down
91 changes: 88 additions & 3 deletions changedetectionio/tests/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def test_check_basic_scheduler_functionality(client, live_server, measure_memory
url_for("settings_page"),
data={"application-empty_pages_are_a_change": "",
"requests-time_between_check-seconds": 1,
"application-timezone": "Pacific/Kiritimati", # Most Forward Time Zone (UTC+14:00)
"application-timezone": "Pacific/Kiritimati", # Most Forward Time Zone (UTC+14:00)
'application-fetch_backend': "html_requests"},
follow_redirects=True
)
Expand Down Expand Up @@ -71,22 +71,107 @@ def test_check_basic_scheduler_functionality(client, live_server, measure_memory
)
assert b"Updated watch." in res.data


res = client.get(url_for("edit_page", uuid="first"))
assert b"Pacific/Kiritimati" in res.data, "Should be Pacific/Kiritimati in placeholder data"

# "Edit" should not trigger a check because it's not enabled in the schedule.
time.sleep(2)
assert live_server.app.config['DATASTORE'].data['watching'][uuid]['last_checked'] == last_check


# Enabling today in Kiritimati should work flawless
kiritimati_time = datetime.now(timezone.utc).astimezone(ZoneInfo("Pacific/Kiritimati"))
kiritimati_time_day_of_week = kiritimati_time.strftime("%A").lower()
live_server.app.config['DATASTORE'].data['watching'][uuid]["time_schedule_limit"][kiritimati_time_day_of_week]["enabled"] = True
time.sleep(3)
assert live_server.app.config['DATASTORE'].data['watching'][uuid]['last_checked'] != last_check

# Cleanup everything
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data


def test_check_basic_global_scheduler_functionality(client, live_server, measure_memory_usage):
live_server_setup(live_server)
days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
test_url = url_for('test_random_content_endpoint', _external=True)

res = client.post(
url_for("import_page"),
data={"urls": test_url},
follow_redirects=True
)

assert b"1 Imported" in res.data
wait_for_all_checks(client)
uuid = extract_UUID_from_client(client)

# Setup all the days of the weeks using XXX as the placeholder for monday/tuesday/etc

tpl = {
"requests-time_schedule_limit-XXX-start_time": "00:00",
"requests-time_schedule_limit-XXX-duration-hours": 24,
"requests-time_schedule_limit-XXX-duration-minutes": 0,
"requests-time_schedule_limit-XXX-enabled": '', # All days are turned off
"requests-time_schedule_limit-enabled": 'y', # Scheduler is enabled, all days however are off.
}

scheduler_data = {}
for day in days:
for key, value in tpl.items():
# Replace "XXX" with the current day in the key
new_key = key.replace("XXX", day)
scheduler_data[new_key] = value

data = {
"application-empty_pages_are_a_change": "",
"application-timezone": "Pacific/Kiritimati", # Most Forward Time Zone (UTC+14:00)
'application-fetch_backend': "html_requests",
"requests-time_between_check-hours": 0,
"requests-time_between_check-minutes": 0,
"requests-time_between_check-seconds": 1,
}
data.update(scheduler_data)

#####################
res = client.post(
url_for("settings_page"),
data=data,
follow_redirects=True
)

assert b"Settings updated." in res.data

res = client.get(url_for("settings_page"))
assert b'Pacific/Kiritimati' in res.data

wait_for_all_checks(client)

# UI Sanity check

res = client.get(url_for("edit_page", uuid="first"))
assert b"Pacific/Kiritimati" in res.data, "Should be Pacific/Kiritimati in placeholder data"

#### HITTING SAVE SHOULD NOT TRIGGER A CHECK
last_check = live_server.app.config['DATASTORE'].data['watching'][uuid]['last_checked']
res = client.post(
url_for("edit_page", uuid="first"),
data={
"url": test_url,
"fetch_backend": "html_requests",
"time_between_check_use_default": "y"},
follow_redirects=True
)
assert b"Updated watch." in res.data
time.sleep(2)
assert live_server.app.config['DATASTORE'].data['watching'][uuid]['last_checked'] == last_check

# Enabling "today" in Kiritimati time should make the system check that watch
kiritimati_time = datetime.now(timezone.utc).astimezone(ZoneInfo("Pacific/Kiritimati"))
kiritimati_time_day_of_week = kiritimati_time.strftime("%A").lower()
live_server.app.config['DATASTORE'].data['settings']['requests']['time_schedule_limit'][kiritimati_time_day_of_week]["enabled"] = True

time.sleep(3)
assert live_server.app.config['DATASTORE'].data['watching'][uuid]['last_checked'] != last_check

# Cleanup everything
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
Expand Down
33 changes: 29 additions & 4 deletions changedetectionio/time_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from enum import IntEnum
from zoneinfo import ZoneInfo


class Weekday(IntEnum):
"""Enumeration for days of the week."""
Monday = 0
Expand All @@ -14,10 +15,10 @@ class Weekday(IntEnum):


def am_i_inside_time(
day_of_week: str,
time_str: str,
timezone_str: str,
duration: int = 15,
day_of_week: str,
time_str: str,
timezone_str: str,
duration: int = 15,
) -> bool:
"""
Determines if the current time falls within a specified time range.
Expand Down Expand Up @@ -78,3 +79,27 @@ def am_i_inside_time(

return False


def is_within_schedule(time_schedule_limit, default_tz="UTC"):
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')
if not tz_name:
tz_name = default_tz

now_day_name_in_tz = datetime.now(ZoneInfo(tz_name.strip())).strftime('%A')
selected_day_schedule = time_schedule_limit.get(now_day_name_in_tz.lower())
if not selected_day_schedule.get('enabled'):
return False

duration = selected_day_schedule.get('duration')
selected_day_run_duration_m = int(duration.get('hours')) * 60 + int(duration.get('minutes'))

is_valid = am_i_inside_time(day_of_week=now_day_name_in_tz,
time_str=selected_day_schedule['start_time'],
timezone_str=tz_name,
duration=selected_day_run_duration_m)

return is_valid

return False

0 comments on commit 326e36a

Please sign in to comment.