BE: chore datetime_utils

Signed-off-by: jokob-sk <jokob.sk@gmail.com>
This commit is contained in:
jokob-sk
2025-11-05 16:08:04 +11:00
parent 746f1a8922
commit c08eb1dbba
37 changed files with 234 additions and 207 deletions

View File

@@ -42,7 +42,7 @@ Backend loop phases (see `server/__main__.py` and `server/plugin.py`): `once`, `
## Conventions & helpers to reuse ## Conventions & helpers to reuse
- Settings: add/modify via `ccd()` in `server/initialise.py` or perplugin manifest. Never hardcode ports or secrets; use `get_setting_value()`. - Settings: add/modify via `ccd()` in `server/initialise.py` or perplugin manifest. Never hardcode ports or secrets; use `get_setting_value()`.
- Logging: use `logger.mylog(level, [message])`; levels: none/minimal/verbose/debug/trace. - Logging: use `logger.mylog(level, [message])`; levels: none/minimal/verbose/debug/trace.
- Time/MAC/strings: `helper.py` (`timeNowTZ`, `normalize_mac`, sanitizers). Validate MACs before DB writes. - Time/MAC/strings: `helper.py` (`timeNowDB`, `normalize_mac`, sanitizers). Validate MACs before DB writes.
- DB helpers: prefer `server/db/db_helper.py` functions (e.g., `get_table_json`, device condition helpers) over raw SQL in new paths. - DB helpers: prefer `server/db/db_helper.py` functions (e.g., `get_table_json`, device condition helpers) over raw SQL in new paths.
## Dev workflow (devcontainer) ## Dev workflow (devcontainer)

View File

@@ -58,7 +58,7 @@ You can completely ignore detected devices globally. This could be because your
## Ignoring notifications 🔕 ## Ignoring notifications 🔕
You can filter out unwanted notifications globally. This could be because of a misbehaving device (GoogleNest/GoogleHub (See also [ARPSAN docs and the `--exclude-broadcast` flag](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/arp_scan#ip-flipping-on-google-nest-devices))) which flips between IP addresses, or because you want to ignore new device notifications of a certian pattern. You can filter out unwanted notifications globally. This could be because of a misbehaving device (GoogleNest/GoogleHub (See also [ARPSAN docs and the `--exclude-broadcast` flag](https://github.com/jokob-sk/NetAlertX/tree/main/front/plugins/arp_scan#ip-flipping-on-google-nest-devices))) which flips between IP addresses, or because you want to ignore new device notifications of a certain pattern.
1. Events Filter (`NTFPRCS_event_condition`) - filter out Events from notifications. 1. Events Filter (`NTFPRCS_event_condition`) - filter out Events from notifications.
2. New Devices Filter (`NTFPRCS_new_dev_condition`) - filter out New Devices from notifications, but log and keep a new device in the system. 2. New Devices Filter (`NTFPRCS_new_dev_condition`) - filter out New Devices from notifications, but log and keep a new device in the system.

View File

@@ -16,7 +16,8 @@ import conf
from const import confFileName, logPath from const import confFileName, logPath
from plugin_helper import Plugin_Objects from plugin_helper import Plugin_Objects
from logger import mylog, Logger, append_line_to_file from logger import mylog, Logger, append_line_to_file
from helper import timeNowDB, get_setting_value from helper import get_setting_value
from utils.datetime_utils import timeNowDB
from models.notification_instance import NotificationInstance from models.notification_instance import NotificationInstance
from database import DB from database import DB
from pytz import timezone from pytz import timezone

View File

@@ -25,7 +25,8 @@ import conf
from const import confFileName, logPath from const import confFileName, logPath
from plugin_helper import Plugin_Objects from plugin_helper import Plugin_Objects
from logger import mylog, Logger, append_line_to_file from logger import mylog, Logger, append_line_to_file
from helper import timeNowDB, get_setting_value, hide_email from helper import get_setting_value, hide_email
from utils.datetime_utils import timeNowDB
from models.notification_instance import NotificationInstance from models.notification_instance import NotificationInstance
from database import DB from database import DB
from pytz import timezone from pytz import timezone

View File

@@ -23,8 +23,9 @@ from const import confFileName, logPath
from plugin_utils import getPluginObject from plugin_utils import getPluginObject
from plugin_helper import Plugin_Objects from plugin_helper import Plugin_Objects
from logger import mylog, Logger from logger import mylog, Logger
from helper import timeNowDB, get_setting_value, bytes_to_string, \ from helper import get_setting_value, bytes_to_string, \
sanitize_string, normalize_string sanitize_string, normalize_string
from utils.datetime_utils import timeNowDB
from database import DB, get_device_stats from database import DB, get_device_stats

View File

@@ -19,7 +19,8 @@ import conf
from const import confFileName, logPath from const import confFileName, logPath
from plugin_helper import Plugin_Objects, handleEmpty from plugin_helper import Plugin_Objects, handleEmpty
from logger import mylog, Logger, append_line_to_file from logger import mylog, Logger, append_line_to_file
from helper import timeNowDB, get_setting_value from helper import get_setting_value
from utils.datetime_utils import timeNowDB
from models.notification_instance import NotificationInstance from models.notification_instance import NotificationInstance
from database import DB from database import DB
from pytz import timezone from pytz import timezone

View File

@@ -11,7 +11,8 @@ sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from plugin_helper import Plugin_Objects, handleEmpty # noqa: E402 from plugin_helper import Plugin_Objects, handleEmpty # noqa: E402
from logger import mylog, Logger # noqa: E402 from logger import mylog, Logger # noqa: E402
from helper import timeNowDB, get_setting_value, hide_string # noqa: E402 from helper import get_setting_value, hide_string # noqa: E402
from utils.datetime_utils import timeNowDB
from models.notification_instance import NotificationInstance # noqa: E402 from models.notification_instance import NotificationInstance # noqa: E402
from database import DB # noqa: E402 from database import DB # noqa: E402
import conf import conf

View File

@@ -19,7 +19,8 @@ import conf
from const import confFileName, logPath from const import confFileName, logPath
from plugin_helper import Plugin_Objects, handleEmpty from plugin_helper import Plugin_Objects, handleEmpty
from logger import mylog, Logger, append_line_to_file from logger import mylog, Logger, append_line_to_file
from helper import timeNowDB, get_setting_value, hide_string from helper import get_setting_value, hide_string
from utils.datetime_utils import timeNowDB
from models.notification_instance import NotificationInstance from models.notification_instance import NotificationInstance
from database import DB from database import DB
from pytz import timezone from pytz import timezone

View File

@@ -16,7 +16,8 @@ import conf
from const import confFileName, logPath from const import confFileName, logPath
from plugin_helper import Plugin_Objects from plugin_helper import Plugin_Objects
from logger import mylog, Logger, append_line_to_file from logger import mylog, Logger, append_line_to_file
from helper import timeNowDB, get_setting_value from helper import get_setting_value
from utils.datetime_utils import timeNowDB
from models.notification_instance import NotificationInstance from models.notification_instance import NotificationInstance
from database import DB from database import DB
from pytz import timezone from pytz import timezone

View File

@@ -22,7 +22,8 @@ import conf
from const import logPath, confFileName from const import logPath, confFileName
from plugin_helper import Plugin_Objects, handleEmpty from plugin_helper import Plugin_Objects, handleEmpty
from logger import mylog, Logger, append_line_to_file from logger import mylog, Logger, append_line_to_file
from helper import timeNowDB, get_setting_value, hide_string, write_file from helper import get_setting_value, hide_string, write_file
from utils.datetime_utils import timeNowDB
from models.notification_instance import NotificationInstance from models.notification_instance import NotificationInstance
from database import DB from database import DB
from pytz import timezone from pytz import timezone

View File

@@ -20,8 +20,9 @@ sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64 from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64
from logger import mylog, Logger, append_line_to_file from logger import mylog, Logger, append_line_to_file
from helper import timeNowDB, check_IP_format, get_setting_value from helper import check_IP_format, get_setting_value
from const import logPath, applicationPath, fullDbPath from const import logPath, applicationPath, fullDbPath
from utils.datetime_utils import timeNowDB
import conf import conf
from pytz import timezone from pytz import timezone

View File

@@ -13,7 +13,8 @@ sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from plugin_helper import Plugin_Objects from plugin_helper import Plugin_Objects
from logger import mylog, Logger, append_line_to_file from logger import mylog, Logger, append_line_to_file
from helper import timeNowDB, get_setting_value from helper import get_setting_value
from utils.datetime_utils import timeNowDB
import conf import conf
from pytz import timezone from pytz import timezone
from const import logPath from const import logPath

View File

@@ -14,7 +14,8 @@ sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64 from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64
from logger import mylog, Logger, append_line_to_file from logger import mylog, Logger, append_line_to_file
from helper import timeNowDB, get_setting_value from helper import get_setting_value
from utils.datetime_utils import timeNowDB
from const import logPath, applicationPath from const import logPath, applicationPath
import conf import conf
from pytz import timezone from pytz import timezone

View File

@@ -11,7 +11,8 @@ INSTALL_PATH = "/app"
sys.path.append(f"{INSTALL_PATH}/front/plugins") sys.path.append(f"{INSTALL_PATH}/front/plugins")
sys.path.append(f'{INSTALL_PATH}/server') sys.path.append(f'{INSTALL_PATH}/server')
from logger import mylog, Logger, timeNowDB from logger import mylog, Logger
from utils.datetime_utils import timeNowDB
from const import confFileName, default_tz from const import confFileName, default_tz
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------

View File

@@ -18,7 +18,8 @@ from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64
from plugin_utils import get_plugins_configs, decode_and_rename_files from plugin_utils import get_plugins_configs, decode_and_rename_files
from logger import mylog, Logger from logger import mylog, Logger
from const import pluginsPath, fullDbPath, logPath from const import pluginsPath, fullDbPath, logPath
from helper import timeNowDB, get_setting_value from helper import get_setting_value
from utils.datetime_utils import timeNowDB
from crypto_utils import encrypt_data from crypto_utils import encrypt_data
from messaging.in_app import write_notification from messaging.in_app import write_notification
import conf import conf

View File

@@ -26,7 +26,8 @@ from pathlib import Path
import conf import conf
from const import * from const import *
from logger import mylog from logger import mylog
from helper import filePermissions, timeNowTZ, get_setting_value from helper import filePermissions, get_setting_value
from utils.datetime_utils import timeNowTZ
from app_state import updateState from app_state import updateState
from api import update_api from api import update_api
from scan.session_events import process_scan from scan.session_events import process_scan

View File

@@ -7,7 +7,8 @@ import datetime
import conf import conf
from const import (apiPath, sql_appevents, sql_devices_all, sql_events_pending_alert, sql_settings, sql_plugins_events, sql_plugins_history, sql_plugins_objects,sql_language_strings, sql_notifications_all, sql_online_history, sql_devices_tiles, sql_devices_filters) from const import (apiPath, sql_appevents, sql_devices_all, sql_events_pending_alert, sql_settings, sql_plugins_events, sql_plugins_history, sql_plugins_objects,sql_language_strings, sql_notifications_all, sql_online_history, sql_devices_tiles, sql_devices_filters)
from logger import mylog from logger import mylog
from helper import write_file, get_setting_value, timeNowTZ from helper import write_file, get_setting_value
from utils.datetime_utils import timeNowTZ
from app_state import updateState from app_state import updateState
from models.user_events_queue_instance import UserEventsQueueInstance from models.user_events_queue_instance import UserEventsQueueInstance
from messaging.in_app import write_notification from messaging.in_app import write_notification

View File

@@ -14,7 +14,8 @@ INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection from database import get_temp_db_connection
from helper import is_random_mac, format_date, get_setting_value, timeNowDB from helper import is_random_mac, get_setting_value
from utils.datetime_utils import timeNowDB, format_date
from db.db_helper import row_to_json, get_date_from_period from db.db_helper import row_to_json, get_date_from_period
# -------------------------- # --------------------------

View File

@@ -19,8 +19,9 @@ INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection from database import get_temp_db_connection
from helper import is_random_mac, format_date, get_setting_value from helper import is_random_mac, get_setting_value
from db.db_helper import get_table_json, get_device_condition_by_status from db.db_helper import get_table_json, get_device_condition_by_status
from utils.datetime_utils import format_date
# -------------------------- # --------------------------

View File

@@ -14,8 +14,9 @@ INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection from database import get_temp_db_connection
from helper import is_random_mac, format_date, get_setting_value, format_date_iso, format_event_date, mylog, ensure_datetime from helper import is_random_mac, get_setting_value, mylog
from db.db_helper import row_to_json, get_date_from_period from db.db_helper import row_to_json, get_date_from_period
from utils.datetime_utils import format_date, format_date_iso, format_event_date, ensure_datetime
# -------------------------- # --------------------------

View File

@@ -14,7 +14,8 @@ INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection from database import get_temp_db_connection
from helper import is_random_mac, format_date, get_setting_value from helper import is_random_mac, get_setting_value
from utils.datetime_utils import format_date
# -------------------------------------------------- # --------------------------------------------------

View File

@@ -16,8 +16,9 @@ INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection from database import get_temp_db_connection
from helper import is_random_mac, format_date, get_setting_value, format_date_iso, format_event_date, mylog, format_date_diff, format_ip_long, parse_datetime from helper import is_random_mac, get_setting_value, mylog, format_ip_long
from db.db_helper import row_to_json, get_date_from_period from db.db_helper import row_to_json, get_date_from_period
from utils.datetime_utils import format_date_iso, format_event_date, format_date_diff, parse_datetime, format_date
# -------------------------- # --------------------------

View File

@@ -2,7 +2,8 @@ import os
import base64 import base64
from flask import jsonify, request from flask import jsonify, request
from logger import mylog from logger import mylog
from helper import get_setting_value, timeNowDB from helper import get_setting_value
from utils.datetime_utils import timeNowDB
from messaging.in_app import write_notification from messaging.in_app import write_notification
INSTALL_PATH = "/app" INSTALL_PATH = "/app"

View File

@@ -4,7 +4,8 @@ import json
import conf import conf
from const import * from const import *
from logger import mylog, logResult from logger import mylog, logResult
from helper import timeNowDB, timeNow, checkNewVersion from helper import checkNewVersion
from utils.datetime_utils import timeNowDB, timeNow
# Register NetAlertX directories # Register NetAlertX directories
INSTALL_PATH="/app" INSTALL_PATH="/app"

View File

@@ -7,7 +7,6 @@ import os
import re import re
import unicodedata import unicodedata
import subprocess import subprocess
from typing import Union
import pytz import pytz
from pytz import timezone from pytz import timezone
import json import json
@@ -29,144 +28,6 @@ from logger import mylog, logResult
# Register NetAlertX directories # Register NetAlertX directories
INSTALL_PATH="/app" INSTALL_PATH="/app"
#-------------------------------------------------------------------------------
# DateTime
#-------------------------------------------------------------------------------
def timeNowTZ():
if conf.tz:
return datetime.datetime.now(conf.tz).replace(microsecond=0)
else:
return datetime.datetime.now().replace(microsecond=0)
def timeNow():
return datetime.datetime.now().replace(microsecond=0)
def get_timezone_offset():
now = datetime.datetime.now(conf.tz)
offset_hours = now.utcoffset().total_seconds() / 3600
offset_formatted = "{:+03d}:{:02d}".format(int(offset_hours), int((offset_hours % 1) * 60))
return offset_formatted
def timeNowDB(local=True):
"""
Return the current time (local or UTC) as ISO 8601 for DB storage.
Safe for SQLite, PostgreSQL, etc.
Example local: '2025-11-04 18:09:11'
Example UTC: '2025-11-04 07:09:11'
"""
if local:
try:
if isinstance(conf.tz, datetime.tzinfo):
tz = conf.tz
elif conf.tz:
tz = ZoneInfo(conf.tz)
else:
tz = None
except Exception:
tz = None
return datetime.datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S')
else:
return datetime.datetime.now(datetime.UTC).strftime('%Y-%m-%d %H:%M:%S')
#-------------------------------------------------------------------------------
# Date and time methods
#-------------------------------------------------------------------------------
# -------------------------------------------------------------------------------------------
def format_date_iso(date1: str) -> str:
"""Return ISO 8601 string for a date or None if empty"""
if date1 is None:
return None
dt = datetime.datetime.fromisoformat(date1) if isinstance(date1, str) else date1
return dt.isoformat()
# -------------------------------------------------------------------------------------------
def format_event_date(date_str: str, event_type: str) -> str:
"""Format event date with fallback rules."""
if date_str:
return format_date(date_str)
elif event_type == "<missing event>":
return "<missing event>"
else:
return "<still connected>"
# -------------------------------------------------------------------------------------------
def ensure_datetime(dt: Union[str, datetime.datetime, None]) -> datetime.datetime:
if dt is None:
return timeNowTZ()
if isinstance(dt, str):
return datetime.datetime.fromisoformat(dt)
return dt
def parse_datetime(dt_str):
if not dt_str:
return None
try:
# Try ISO8601 first
return datetime.datetime.fromisoformat(dt_str)
except ValueError:
# Try RFC1123 / HTTP format
try:
return datetime.datetime.strptime(dt_str, '%a, %d %b %Y %H:%M:%S GMT')
except ValueError:
return None
def format_date(date_str: str) -> str:
try:
dt = parse_datetime(date_str)
if dt.tzinfo is None:
# Set timezone if missing — change to timezone.utc if you prefer UTC
now = datetime.datetime.now(conf.tz)
dt = dt.replace(tzinfo=now.astimezone().tzinfo)
return dt.astimezone().isoformat()
except Exception:
return "invalid"
def format_date_diff(date1, date2):
"""
Return difference between two datetimes as 'Xd HH:MM'.
Uses app timezone if datetime is naive.
date2 can be None (uses now).
"""
# Get timezone from settings
tz_name = get_setting_value("TIMEZONE") or "UTC"
tz = pytz.timezone(tz_name)
def parse_dt(dt):
if dt is None:
return datetime.datetime.now(tz)
if isinstance(dt, str):
try:
dt_parsed = email.utils.parsedate_to_datetime(dt)
except Exception:
# fallback: parse ISO string
dt_parsed = datetime.datetime.fromisoformat(dt)
# convert naive GMT/UTC to app timezone
if dt_parsed.tzinfo is None:
dt_parsed = tz.localize(dt_parsed)
else:
dt_parsed = dt_parsed.astimezone(tz)
return dt_parsed
return dt if dt.tzinfo else tz.localize(dt)
dt1 = parse_dt(date1)
dt2 = parse_dt(date2)
delta = dt2 - dt1
total_minutes = int(delta.total_seconds() // 60)
days, rem_minutes = divmod(total_minutes, 1440) # 1440 mins in a day
hours, minutes = divmod(rem_minutes, 60)
return {
"text": f"{days}d {hours:02}:{minutes:02}",
"days": days,
"hours": hours,
"minutes": minutes,
"total_minutes": total_minutes
}
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
# File system permission handling # File system permission handling

View File

@@ -12,7 +12,8 @@ import re
# Register NetAlertX libraries # Register NetAlertX libraries
import conf import conf
from const import fullConfPath, applicationPath, fullConfFolder, default_tz from const import fullConfPath, applicationPath, fullConfFolder, default_tz
from helper import getBuildTimeStamp, fixPermissions, collect_lang_strings, updateSubnets, isJsonObject, setting_value_to_python_type, timeNowDB, get_setting_value, generate_random_string from helper import getBuildTimeStamp, fixPermissions, collect_lang_strings, updateSubnets, isJsonObject, setting_value_to_python_type, get_setting_value, generate_random_string
from utils.datetime_utils import timeNowDB
from app_state import updateState from app_state import updateState
from logger import mylog from logger import mylog
from api import update_api from api import update_api

View File

@@ -7,40 +7,15 @@ import time
import logging import logging
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
# NetAlertX imports # NetAlertX imports
import conf import conf
from const import * from const import *
from utils.datetime_utils import timeNowTZ
#-------------------------------------------------------------------------------
# duplication from helper to avoid circle
#-------------------------------------------------------------------------------
def timeNowTZ():
if conf.tz:
return datetime.datetime.now(conf.tz).replace(microsecond=0)
else:
return datetime.datetime.now().replace(microsecond=0)
def timeNowDB(local=True):
"""
Return the current time (local or UTC) as ISO 8601 for DB storage.
Safe for SQLite, PostgreSQL, etc.
Example local: '2025-11-04 18:09:11'
Example UTC: '2025-11-04 07:09:11'
"""
if local:
try:
if isinstance(conf.tz, datetime.tzinfo):
tz = conf.tz
elif conf.tz:
tz = ZoneInfo(conf.tz)
else:
tz = None
except Exception:
tz = None
return datetime.datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S')
else:
return datetime.datetime.now(datetime.UTC).strftime('%Y-%m-%d %H:%M:%S')
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
# Map custom debug levels to Python logging levels # Map custom debug levels to Python logging levels

View File

@@ -20,7 +20,8 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
import conf import conf
from const import applicationPath, logPath, apiPath, confFileName, reportTemplatesPath from const import applicationPath, logPath, apiPath, confFileName, reportTemplatesPath
from logger import logResult, mylog from logger import logResult, mylog
from helper import generate_mac_links, removeDuplicateNewLines, timeNowDB, get_file_content, write_file, get_setting_value, get_timezone_offset from helper import generate_mac_links, removeDuplicateNewLines, get_file_content, write_file, get_setting_value
from utils.datetime_utils import timeNowDB
NOTIFICATION_API_FILE = apiPath + 'user_notifications.json' NOTIFICATION_API_FILE = apiPath + 'user_notifications.json'

View File

@@ -20,9 +20,10 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
import conf import conf
from const import applicationPath, logPath, apiPath, confFileName from const import applicationPath, logPath, apiPath, confFileName
from helper import get_file_content, write_file, get_timezone_offset, get_setting_value from helper import get_file_content, write_file, get_setting_value
from logger import logResult, mylog from logger import logResult, mylog
from db.sql_safe_builder import create_safe_condition_builder from db.sql_safe_builder import create_safe_condition_builder
from utils.datetime_utils import get_timezone_offset
#=============================================================================== #===============================================================================
# REPORTING # REPORTING

View File

@@ -16,12 +16,10 @@ from const import applicationPath, logPath, apiPath, reportTemplatesPath
from logger import mylog, Logger from logger import mylog, Logger
from helper import generate_mac_links, \ from helper import generate_mac_links, \
removeDuplicateNewLines, \ removeDuplicateNewLines, \
timeNowDB, \
timeNowTZ, \
write_file, \ write_file, \
get_setting_value, \ get_setting_value
get_timezone_offset
from messaging.in_app import write_notification from messaging.in_app import write_notification
from utils.datetime_utils import timeNowDB, get_timezone_offset
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -12,7 +12,8 @@ from collections import namedtuple
import conf import conf
from const import pluginsPath, logPath, applicationPath, reportTemplatesPath from const import pluginsPath, logPath, applicationPath, reportTemplatesPath
from logger import mylog, Logger from logger import mylog, Logger
from helper import timeNowDB, timeNowTZ, get_file_content, write_file, get_setting, get_setting_value from helper import get_file_content, write_file, get_setting, get_setting_value
from utils.datetime_utils import timeNowTZ, timeNowDB
from app_state import updateState from app_state import updateState
from api import update_api from api import update_api
from plugin_utils import logEventStatusCounts, get_plugin_string, get_plugin_setting_obj, print_plugin_info, list_to_csv, combine_plugin_objects, resolve_wildcards_arr, handle_empty, custom_plugin_decoder, decode_and_rename_files from plugin_utils import logEventStatusCounts, get_plugin_string, get_plugin_setting_obj, print_plugin_info, list_to_csv, combine_plugin_objects, resolve_wildcards_arr, handle_empty, custom_plugin_decoder, decode_and_rename_files

View File

@@ -10,7 +10,8 @@ from dateutil import parser
INSTALL_PATH="/app" INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/server"]) sys.path.extend([f"{INSTALL_PATH}/server"])
from helper import timeNowDB, timeNowTZ, get_setting_value, check_IP_format from helper import get_setting_value, check_IP_format
from utils.datetime_utils import timeNowDB
from logger import mylog, Logger from logger import mylog, Logger
from const import vendorsPath, vendorsPathNewest, sql_generateGuid from const import vendorsPath, vendorsPathNewest, sql_generateGuid
from models.device_instance import DeviceInstance from models.device_instance import DeviceInstance

View File

@@ -6,7 +6,8 @@ sys.path.extend([f"{INSTALL_PATH}/server"])
import conf import conf
from scan.device_handling import create_new_devices, print_scan_stats, save_scanned_devices, exclude_ignored_devices, update_devices_data_from_scan from scan.device_handling import create_new_devices, print_scan_stats, save_scanned_devices, exclude_ignored_devices, update_devices_data_from_scan
from helper import timeNowDB, get_setting_value from helper import get_setting_value
from utils.datetime_utils import timeNowDB
from db.db_helper import print_table_schema from db.db_helper import print_table_schema
from logger import mylog, Logger from logger import mylog, Logger
from messaging.reporting import skip_repeated_notifications from messaging.reporting import skip_repeated_notifications

View File

@@ -0,0 +1,162 @@
#!/usr/bin/env python
import os
import pathlib
import sys
from datetime import datetime
import pytz
from pytz import timezone
import datetime
from typing import Union
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
# Register NetAlertX directories
INSTALL_PATH="/app"
import conf
from const import *
#-------------------------------------------------------------------------------
# DateTime
#-------------------------------------------------------------------------------
def timeNowTZ():
if conf.tz:
return datetime.datetime.now(conf.tz).replace(microsecond=0)
else:
return datetime.datetime.now().replace(microsecond=0)
def timeNow():
return datetime.datetime.now().replace(microsecond=0)
def get_timezone_offset():
now = datetime.datetime.now(conf.tz)
offset_hours = now.utcoffset().total_seconds() / 3600
offset_formatted = "{:+03d}:{:02d}".format(int(offset_hours), int((offset_hours % 1) * 60))
return offset_formatted
def timeNowDB(local=True):
"""
Return the current time (local or UTC) as ISO 8601 for DB storage.
Safe for SQLite, PostgreSQL, etc.
Example local: '2025-11-04 18:09:11'
Example UTC: '2025-11-04 07:09:11'
"""
if local:
try:
if isinstance(conf.tz, datetime.tzinfo):
tz = conf.tz
elif conf.tz:
tz = ZoneInfo(conf.tz)
else:
tz = None
except Exception:
tz = None
return datetime.datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S')
else:
return datetime.datetime.now(datetime.UTC).strftime('%Y-%m-%d %H:%M:%S')
#-------------------------------------------------------------------------------
# Date and time methods
#-------------------------------------------------------------------------------
# -------------------------------------------------------------------------------------------
def format_date_iso(date1: str) -> str:
"""Return ISO 8601 string for a date or None if empty"""
if date1 is None:
return None
dt = datetime.datetime.fromisoformat(date1) if isinstance(date1, str) else date1
return dt.isoformat()
# -------------------------------------------------------------------------------------------
def format_event_date(date_str: str, event_type: str) -> str:
"""Format event date with fallback rules."""
if date_str:
return format_date(date_str)
elif event_type == "<missing event>":
return "<missing event>"
else:
return "<still connected>"
# -------------------------------------------------------------------------------------------
def ensure_datetime(dt: Union[str, datetime.datetime, None]) -> datetime.datetime:
if dt is None:
return timeNowTZ()
if isinstance(dt, str):
return datetime.datetime.fromisoformat(dt)
return dt
def parse_datetime(dt_str):
if not dt_str:
return None
try:
# Try ISO8601 first
return datetime.datetime.fromisoformat(dt_str)
except ValueError:
# Try RFC1123 / HTTP format
try:
return datetime.datetime.strptime(dt_str, '%a, %d %b %Y %H:%M:%S GMT')
except ValueError:
return None
def format_date(date_str: str) -> str:
try:
dt = parse_datetime(date_str)
if dt.tzinfo is None:
# Set timezone if missing — change to timezone.utc if you prefer UTC
now = datetime.datetime.now(conf.tz)
dt = dt.replace(tzinfo=now.astimezone().tzinfo)
return dt.astimezone().isoformat()
except Exception:
return "invalid"
def format_date_diff(date1, date2):
"""
Return difference between two datetimes as 'Xd HH:MM'.
Uses app timezone if datetime is naive.
date2 can be None (uses now).
"""
# Get timezone from settings
tz_name = get_setting_value("TIMEZONE") or "UTC"
tz = pytz.timezone(tz_name)
def parse_dt(dt):
if dt is None:
return datetime.datetime.now(tz)
if isinstance(dt, str):
try:
dt_parsed = email.utils.parsedate_to_datetime(dt)
except Exception:
# fallback: parse ISO string
dt_parsed = datetime.datetime.fromisoformat(dt)
# convert naive GMT/UTC to app timezone
if dt_parsed.tzinfo is None:
dt_parsed = tz.localize(dt_parsed)
else:
dt_parsed = dt_parsed.astimezone(tz)
return dt_parsed
return dt if dt.tzinfo else tz.localize(dt)
dt1 = parse_dt(date1)
dt2 = parse_dt(date2)
delta = dt2 - dt1
total_minutes = int(delta.total_seconds() // 60)
days, rem_minutes = divmod(total_minutes, 1440) # 1440 mins in a day
hours, minutes = divmod(rem_minutes, 60)
return {
"text": f"{days}d {hours:02}:{minutes:02}",
"days": days,
"hours": hours,
"minutes": minutes,
"total_minutes": total_minutes
}

View File

@@ -6,7 +6,8 @@ import pytest
INSTALL_PATH = "/app" INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from helper import get_setting_value, timeNowDB from helper import get_setting_value
from utils.datetime_utils import timeNowDB
from api_server.api_server_start import app from api_server.api_server_start import app

View File

@@ -10,7 +10,8 @@ from datetime import datetime, timedelta
INSTALL_PATH = "/app" INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from helper import timeNowTZ, get_setting_value from helper import get_setting_value
from utils.datetime_utils import timeNowTZ
from api_server.api_server_start import app from api_server.api_server_start import app
@pytest.fixture(scope="session") @pytest.fixture(scope="session")

View File

@@ -10,7 +10,8 @@ from datetime import datetime, timedelta
INSTALL_PATH = "/app" INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"]) sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from helper import timeNowDB, timeNowTZ, get_setting_value from helper import get_setting_value
from utils.datetime_utils import timeNowTZ, timeNowDB
from api_server.api_server_start import app from api_server.api_server_start import app
@pytest.fixture(scope="session") @pytest.fixture(scope="session")