diff --git a/modules/extensions.py b/modules/extensions.py index 1063e5eb7..134176e3d 100644 --- a/modules/extensions.py +++ b/modules/extensions.py @@ -1,6 +1,6 @@ from __future__ import annotations import os -from datetime import datetime +from datetime import datetime, timezone import git from modules import shared, errors from modules.paths import extensions_dir, extensions_builtin_dir @@ -11,6 +11,30 @@ if not os.path.exists(extensions_dir): os.makedirs(extensions_dir) +def parse_isotime(time_string: str) -> datetime: + # If Python minimum version is 3.11+, this function can be replaced with datetime.fromisoformat() + trimmed = time_string.rstrip("Z") + if "." in trimmed: + trimmed = trimmed.split(".")[0] + match len(trimmed): + case 16: + return datetime.strptime(trimmed, "%Y-%m-%dT%H:%M").replace(tzinfo=timezone.utc) + case 19: + return datetime.strptime(trimmed, "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc) + case _: + raise ValueError(f"Unexpected time string format: '{time_string}'") + + +def format_dt(d: datetime) -> str: + if d.tzinfo is None: + return d.strftime('%Y-%m-%d %H:%M') + return d.astimezone(timezone.utc).strftime('%Y-%m-%d %H:%M:%S') # Ensure UTC time is shown (just in case) + + +def ts2utc(timestamp: int) -> datetime: + return datetime.fromtimestamp(timestamp, timezone.utc) + + def active(): if shared.opts.disable_all_extensions == "all": return [] @@ -106,8 +130,8 @@ class Extension: self.branch = None self.remote = None self.have_info_from_repo = False - self.mtime = 0 - self.ctime = 0 + self.mtime = "2000-01-01T00:00Z" + self.ctime = "2000-01-01T00:00Z" def read_info(self, force=False): if self.have_info_from_repo and not force: @@ -142,7 +166,7 @@ class Extension: except Exception: self.branch = 'unknown' self.commit_hash = head.hexsha - self.version = f"

{self.commit_hash[:8]}

{datetime.fromtimestamp(self.commit_date).strftime('%a %b%d %Y %H:%M')}

" + self.version = f"

{self.commit_hash[:8]}

{format_dt(ts2utc(self.commit_date))}

" except Exception as ex: shared.log.error(f"Extension: failed reading data from git repo={self.name}: {ex}") self.remote = None diff --git a/modules/ui_extensions.py b/modules/ui_extensions.py index c9271780e..a6c5ca3ec 100644 --- a/modules/ui_extensions.py +++ b/modules/ui_extensions.py @@ -3,7 +3,7 @@ import json import shutil import errno import html -from datetime import datetime, timedelta +from datetime import datetime, timezone, timedelta import gradio as gr from modules import extensions, shared, paths, errors, ui_symbols, call_queue @@ -37,12 +37,9 @@ def get_installed(ext): def list_extensions(): global extensions_list # pylint: disable=global-statement fn = os.path.join(paths.script_path, "html", "extensions.json") - extensions_list = shared.readfile(fn, silent=True) or [] - if type(extensions_list) != list: - shared.log.warning(f'Invalid extensions list: file="{fn}"') - extensions_list = [] + extensions_list = shared.readfile(fn, silent=True, as_type="list") if len(extensions_list) == 0: - shared.log.info('Extension list is empty: refresh required') + shared.log.info("Extension List: No information found. Refresh required.") found = [] for ext in extensions.extensions: ext.read_info() @@ -111,10 +108,10 @@ def check_updates(_id_task, disable_list, search_text, sort_column): ext.git_fetch() ext.read_info() commit_date = ext.commit_date or 1577836800 - shared.log.info(f'Extensions updated: {ext.name} {ext.commit_hash[:8]} {datetime.utcfromtimestamp(commit_date)}') + shared.log.info(f'Extensions updated: {ext.name} {ext.commit_hash[:8]} {extensions.format_dt(extensions.ts2utc(commit_date))}') else: commit_date = ext.commit_date or 1577836800 - shared.log.debug(f'Extensions no update available: {ext.name} {ext.commit_hash[:8]} {datetime.utcfromtimestamp(commit_date)}') + shared.log.debug(f'Extensions no update available: {ext.name} {ext.commit_hash[:8]} {extensions.format_dt(extensions.ts2utc(commit_date))}') except FileNotFoundError as e: if 'FETCH_HEAD' not in str(e): raise @@ -232,10 +229,10 @@ def update_extension(extension_path, search_text, sort_column): ext.git_fetch() ext.read_info() commit_date = ext.commit_date or 1577836800 - shared.log.info(f'Extensions updated: {ext.name} {ext.commit_hash[:8]} {datetime.utcfromtimestamp(commit_date)}') + shared.log.info(f'Extensions updated: {ext.name} {ext.commit_hash[:8]} {extensions.format_dt(extensions.ts2utc(commit_date))}') else: commit_date = ext.commit_date or 1577836800 - shared.log.info(f'Extensions no update available: {ext.name} {ext.commit_hash[:8]} {datetime.utcfromtimestamp(commit_date)}') + shared.log.info(f'Extensions no update available: {ext.name} {ext.commit_hash[:8]} {extensions.format_dt(extensions.ts2utc(commit_date))}') except FileNotFoundError as e: if 'FETCH_HEAD' not in str(e): raise @@ -308,13 +305,13 @@ def create_html(search_text, sort_column): ext['enabled'] = installed.enabled if installed is not None else '' ext['remote'] = installed.remote if installed is not None else None ext['path'] = installed.path if installed is not None else '' - ext['sort_default'] = f"{'1' if ext['is_builtin'] else '0'}{'1' if ext['installed'] else '0'}{ext.get('updated', '2000-01-01T00:00')}" + ext['sort_default'] = f"{'1' if ext['is_builtin'] else '0'}{'1' if ext['installed'] else '0'}{ext.get('updated', '2000-01-01T00:00Z')}" sort_reverse, sort_function = sort_ordering[sort_column] def dt(x: str): val = ext.get(x, None) try: - return datetime.fromisoformat(val[:-1]).strftime('%a %b%d %Y %H:%M') if val is not None else "N/A" + return extensions.format_dt(extensions.parse_isotime(val)) if val is not None else "N/A" except Exception: return 'N/A' @@ -322,22 +319,23 @@ def create_html(search_text, sort_column): for ext in sorted(extensions_list, key=sort_function, reverse=sort_reverse): installed = get_installed(ext) author = '' - updated = datetime.timestamp(datetime.now()) + updated = datetime.now(timezone.utc) # TZ-aware try: if 'github' in ext['url']: author = 'Author: ' + ext['url'].split('/')[-2].split(':')[-1] if '/' in ext['url'] else ext['url'].split(':')[1].split('/')[0] - updated = datetime.timestamp(datetime.fromisoformat(ext.get('updated', '2000-01-01T00:00:00.000Z').rstrip('Z'))) + updated = extensions.parse_isotime(ext.get('updated', '2000-01-01T00:00:00Z')) # TZ-aware else: debug(f'Extension not from github: name={ext["name"]} url={ext["url"]}') except Exception as e: debug(f'Extension get updated error: name={ext["name"]} url={ext["url"]} {e}') - update_available = (installed is not None) and (ext['remote'] is not None) and (updated > ext['commit_date']) + local_ver_date = extensions.ts2utc(ext['commit_date']) # TZ-aware + update_available = (installed is not None) and (not ext['is_builtin']) and (ext['remote'] is not None) and (updated > local_ver_date) # TZ-aware if update_available: - debug(f'Extension update available: name={ext["name"]} updated={updated}/{datetime.utcfromtimestamp(updated)} commit={ext["commit_date"]}/{datetime.utcfromtimestamp(ext["commit_date"])}') + debug(f'Extension update available: name={ext["name"]} updated={extensions.format_dt(updated)} commit={extensions.format_dt(local_ver_date)}') # TZ-aware ext['sort_user'] = f"{'0' if ext['is_builtin'] else '1'}{'1' if ext['installed'] else '0'}{ext.get('name', '')}" - ext['sort_enabled'] = f"{'0' if ext['enabled'] else '1'}{'1' if ext['is_builtin'] else '0'}{'1' if ext['installed'] else '0'}{ext.get('updated', '2000-01-01T00:00')}" - ext['sort_update'] = f"{'1' if update_available else '0'}{'1' if ext['installed'] else '0'}{ext.get('updated', '2000-01-01T00:00')}" - delta = datetime.now() - datetime.fromisoformat(ext.get('created', '2000-01-01T00:00Z')[:-1]) + ext['sort_enabled'] = f"{'0' if ext['enabled'] else '1'}{'1' if ext['is_builtin'] else '0'}{'1' if ext['installed'] else '0'}{ext.get('updated', '2000-01-01T00:00Z')}" + ext['sort_update'] = f"{'1' if update_available else '0'}{'1' if ext['installed'] else '0'}{ext.get('updated', '2000-01-01T00:00Z')}" + delta = datetime.now(timezone.utc) - extensions.parse_isotime(ext.get('created', '2000-01-01T00:00Z')) # TZ-aware to prep for 3.11+ datetime.fromisoformat() behavior ext['sort_trending'] = round(ext.get('stars', 0) / max(delta.days, 5), 1) tags = ext.get("tags", []) if not isinstance(tags, list): @@ -399,7 +397,7 @@ def create_html(search_text, sort_column): else: status = f"
{ui_symbols.svg_bullet.color('#008EBC')}
" else: - if updated < datetime.timestamp(datetime.now() - timedelta(6*30)): + if updated < datetime.now(timezone.utc) - timedelta(6*30): # TZ-aware status = f"
{ui_symbols.svg_bullet.color('#C000CF')}
" else: status = f"
{ui_symbols.svg_bullet.color('#7C7C7C')}
"