From 79e07006092278e651b2c7d22d4cf12e042bff34 Mon Sep 17 00:00:00 2001 From: Roland Date: Tue, 2 Dec 2025 16:07:32 -0700 Subject: [PATCH] Fixing local imports. --- main.py | 1 + modules/archive/api.py | 2 +- modules/archive/lib.py | 20 +++++++------------- modules/archive/models.py | 6 ++++++ wrolpi/collections/lib.py | 16 ++++++++++++---- wrolpi/collections/models.py | 11 ++++++----- 6 files changed, 33 insertions(+), 23 deletions(-) diff --git a/main.py b/main.py index 9109619a..1a152d43 100755 --- a/main.py +++ b/main.py @@ -372,6 +372,7 @@ async def perpetual_check_for_updates(): Updates shared_ctx with update info for the /api/status endpoint. Only runs on native installs (not Docker). """ + # Local import to avoid loading upgrade module at startup from wrolpi.upgrade import check_for_update # Don't check for updates in Docker environments diff --git a/modules/archive/api.py b/modules/archive/api.py index d80b6179..7077bf88 100644 --- a/modules/archive/api.py +++ b/modules/archive/api.py @@ -81,6 +81,7 @@ async def get_upload_singlefile(request: Request): @register_switch_handler('singlefile_upload_switch_handler') async def singlefile_upload_switch_handler(url=None): """Used by `post_upload_singlefile` to upload a single file""" + # Local imports to avoid circular import: downloader -> archive -> api -> downloader from wrolpi.downloader import Download from . import ArchiveDownloader @@ -153,7 +154,6 @@ async def generate_screenshot_switch_handler(archive_id=None): try: await lib.generate_archive_screenshot(archive_id) # Always send success event since exceptions are raised on failure - from modules.archive import Archive archive = lib.get_archive(archive_id=archive_id) location = archive.location name = archive.file_group.title or archive.file_group.url diff --git a/modules/archive/lib.py b/modules/archive/lib.py index 9ea95393..b37f91f1 100644 --- a/modules/archive/lib.py +++ b/modules/archive/lib.py @@ -1,5 +1,4 @@ import base64 -import base64 import gzip import json import os @@ -14,19 +13,21 @@ from typing import Optional, Tuple, List, Union from urllib.parse import urlparse import pytz -from sqlalchemy import asc +from sqlalchemy import asc, func, BigInteger from sqlalchemy.orm import Session from modules.archive.models import Archive from wrolpi import dates from wrolpi.cmd import READABILITY_BIN, run_command from wrolpi.collections import Collection -from wrolpi.common import get_media_directory, logger, extract_domain, escape_file_name, aiohttp_post, \ - format_html_string, split_lines_by_length, get_html_soup, get_title_from_html, get_wrolpi_config, html_screenshot, \ - ConfigFile +from wrolpi.common import get_media_directory, get_relative_to_media_directory, logger, extract_domain, \ + escape_file_name, aiohttp_post, format_html_string, split_lines_by_length, get_html_soup, get_title_from_html, \ + get_wrolpi_config, html_screenshot, ConfigFile from wrolpi.dates import now, Seconds from wrolpi.db import get_db_session, get_db_curs, optional_session from wrolpi.errors import UnknownArchive, InvalidOrderBy, InvalidDatetime +from wrolpi.files.lib import handle_file_group_search_results +from wrolpi.files.models import FileGroup from wrolpi.events import Events from wrolpi.switches import register_switch_handler, ActivateSwitchMethod from wrolpi.tags import tag_append_sub_select_where @@ -607,8 +608,8 @@ def link_domain_and_downloads(session: Session): - Recurring downloads whose destination is within the domain collection's directory (including subdirectories) - RSS downloads with sub_downloader='archive' (matched by URL domain) """ + # Local import to avoid circular import: downloader -> archive -> downloader from wrolpi.downloader import Download - from wrolpi.collections.models import Collection # Only Downloads with a frequency can be a Collection Download. downloads = list(session.query(Download).filter(Download.frequency.isnot(None)).all()) @@ -855,9 +856,6 @@ def get_domains(): This is a thin wrapper around Collection queries that adds archive-specific statistics. Returns a list of dicts with collection id, domain name, url_count, and total size. """ - from sqlalchemy import func, BigInteger - from wrolpi.files.models import FileGroup - with get_db_session() as session: # Query all domain collections with archive statistics in a single query # This uses ORM for better maintainability while keeping performance @@ -1032,7 +1030,6 @@ def search_archives(search_str: str, domain: str, limit: int, offset: int, order '''.strip() logger.debug(stmt, params) - from wrolpi.files.lib import handle_file_group_search_results results, total = handle_file_group_search_results(stmt, params) return results, total @@ -1058,7 +1055,6 @@ async def search_domains_by_name(name: str, limit: int = 5, session: Session = N .all() # Convert to old Domain format for backward compatibility - from wrolpi.common import get_relative_to_media_directory archive_dir = get_archive_directory() return [ { @@ -1155,8 +1151,6 @@ async def generate_archive_screenshot(archive_id: int) -> pathlib.Path: ValueError: If Archive has no singlefile RuntimeError: If screenshot generation fails """ - from wrolpi.db import get_db_session - with get_db_session() as session: archive = Archive.find_by_id(archive_id, session=session) diff --git a/modules/archive/models.py b/modules/archive/models.py index 13228908..1fe720f0 100644 --- a/modules/archive/models.py +++ b/modules/archive/models.py @@ -91,6 +91,7 @@ class Archive(Base, ModelHelper): @staticmethod def can_model(file_group: FileGroup) -> bool: + # Local import to avoid circular import within archive module from modules.archive.lib import is_singlefile_file if file_group.mimetype.startswith('text') and is_singlefile_file(file_group.primary_path): return True @@ -98,6 +99,7 @@ class Archive(Base, ModelHelper): @staticmethod def do_model(file_group: FileGroup, session: Session) -> 'Archive': + # Local import to avoid circular import within archive module from modules.archive import model_archive archive = model_archive(file_group, session) archive.validate() @@ -155,6 +157,7 @@ class Archive(Base, ModelHelper): @property def singlefile_file(self) -> Optional[dict]: + # Local import to avoid circular import within archive module from modules.archive.lib import is_singlefile_file for file in self.file_group.my_html_files(): if is_singlefile_file(file['path']): @@ -315,6 +318,7 @@ class Archive(Base, ModelHelper): def apply_domain(self): """Get the domain collection from the URL.""" + # Local import to avoid circular import within archive module from modules.archive.lib import get_or_create_domain_collection collection = None if self.file_group.url: @@ -332,6 +336,7 @@ class Archive(Base, ModelHelper): def apply_metadata(self): """Read and apply (and more) data from the Singlefile HTML.""" + # Local import to avoid circular import within archive module from modules.archive import lib contents = self.singlefile_path.read_bytes() @@ -365,6 +370,7 @@ class Archive(Base, ModelHelper): """Create a new Archive and FileGroup from the provided paths. The files will be read and Archive data extracted.""" + # Local import to avoid circular import within archive module from modules.archive import model_archive file_group = FileGroup.from_paths(session, *paths) archive = model_archive(file_group, session) diff --git a/wrolpi/collections/lib.py b/wrolpi/collections/lib.py index bd8d4dac..165b71cb 100644 --- a/wrolpi/collections/lib.py +++ b/wrolpi/collections/lib.py @@ -16,7 +16,7 @@ from wrolpi.errors import ValidationError from wrolpi.events import Events from wrolpi.tags import Tag from .errors import UnknownCollection -from .models import Collection +from .models import Collection, validate_collection_directory logger = logger.getChild(__name__) @@ -68,6 +68,7 @@ def get_collections(kind: Optional[str] = None, session: Session = None) -> List # Add type-specific statistics if collection.kind == 'domain': # Add archive statistics for domain collections + # Local imports to avoid circular import: collections -> archive -> collections from modules.archive import Archive from wrolpi.files.models import FileGroup @@ -86,6 +87,7 @@ def get_collections(kind: Optional[str] = None, session: Session = None) -> List elif collection.kind == 'channel': # Add video statistics for channel collections + # Local imports to avoid circular import: collections -> videos -> collections from modules.videos.models import Video, Channel from wrolpi.files.models import FileGroup @@ -144,6 +146,7 @@ def get_collection_with_stats(collection_id: int, session: Session = None) -> di # This can be extended for different collection types if collection.kind == 'domain': # Add archive statistics for domain collections + # Local imports to avoid circular import: collections -> archive -> collections from modules.archive import Archive from wrolpi.files.models import FileGroup @@ -162,6 +165,7 @@ def get_collection_with_stats(collection_id: int, session: Session = None) -> di elif collection.kind == 'channel': # Add video statistics for channel collections + # Local imports to avoid circular import: collections -> videos -> collections from modules.videos.models import Video, Channel from wrolpi.files.models import FileGroup @@ -329,7 +333,6 @@ def update_collection( if directory is not None: if directory: # Convert relative path to absolute with validation - from .models import validate_collection_directory directory_path = validate_collection_directory(directory) collection.directory = directory_path else: @@ -362,6 +365,7 @@ def update_collection( # Trigger domain config save if this is a domain collection if collection.kind == 'domain': + # Local import to avoid circular import: collections -> archive -> collections from modules.archive.lib import save_domains_config save_domains_config.activate_switch() @@ -380,6 +384,7 @@ def refresh_collection(collection_id: int, send_events: bool = True) -> None: UnknownCollection: If collection not found ValidationError: If collection has no directory """ + # Local import to avoid circular import: collections -> files -> collections from wrolpi.files.lib import refresh_files with get_db_session() as session: @@ -439,6 +444,7 @@ def tag_collection( # Trigger domain config save if this is a domain collection if collection.kind == 'domain': + # Local import to avoid circular import: collections -> archive -> collections from modules.archive.lib import save_domains_config save_domains_config.activate_switch() @@ -462,7 +468,6 @@ def tag_collection( # Determine target directory if directory: # User specified a directory - validate it - from .models import validate_collection_directory target_directory = validate_collection_directory(directory) elif collection.directory: # Use existing directory @@ -470,6 +475,7 @@ def tag_collection( else: # Suggest a directory based on collection type and name if collection.kind == 'domain': + # Local import to avoid circular import: collections -> archive -> collections from modules.archive.lib import get_archive_directory base_dir = get_archive_directory() else: @@ -485,6 +491,7 @@ def tag_collection( # Trigger domain config save if this is a domain collection if collection.kind == 'domain': + # Local import to avoid circular import: collections -> archive -> collections from modules.archive.lib import save_domains_config save_domains_config.activate_switch() @@ -595,14 +602,15 @@ def delete_collection( # Orphan child Archives if this is a domain collection if collection.kind == 'domain': + # Local imports to avoid circular import: collections -> archive -> collections from modules.archive.models import Archive + from modules.archive.lib import save_domains_config archives = session.query(Archive).filter_by(collection_id=collection_id).all() for archive in archives: archive.collection_id = None session.flush() # Trigger domain config save - from modules.archive.lib import save_domains_config save_domains_config.activate_switch() # Delete the collection diff --git a/wrolpi/collections/models.py b/wrolpi/collections/models.py index 20c6bdab..8d50a88b 100644 --- a/wrolpi/collections/models.py +++ b/wrolpi/collections/models.py @@ -5,8 +5,9 @@ from sqlalchemy import Column, Integer, String, ForeignKey, Text, DateTime, Inde from sqlalchemy.orm import relationship, Session from sqlalchemy.orm.collections import InstrumentedList -from wrolpi.common import Base, ModelHelper, logger, get_media_directory +from wrolpi.common import Base, ModelHelper, logger, get_media_directory, get_relative_to_media_directory from wrolpi.db import optional_session +from wrolpi.errors import ValidationError from wrolpi.files.models import FileGroup from wrolpi.media_path import MediaPathType from wrolpi.tags import Tag @@ -30,8 +31,6 @@ def validate_collection_directory(directory: pathlib.Path) -> pathlib.Path: Raises: ValidationError: If absolute path is outside media directory """ - from wrolpi.errors import ValidationError - media_directory = get_media_directory() directory = pathlib.Path(directory) @@ -197,6 +196,7 @@ class Collection(ModelHelper, Base): Raises: InvalidDownload: If url or frequency is missing """ + # Local imports to avoid circular import: collections -> downloader -> collections from wrolpi.downloader import Download, download_manager from wrolpi.errors import InvalidDownload @@ -683,8 +683,6 @@ class Collection(ModelHelper, Base): Returns directory as a string relative to media directory if applicable. """ - from wrolpi.common import get_relative_to_media_directory - # Get directory as string, relative to media directory directory_str = None if self.directory: @@ -717,10 +715,12 @@ class Collection(ModelHelper, Base): archive// """ if self.kind == 'channel': + # Local import to avoid circular import: collections -> videos -> collections from modules.videos.lib import format_videos_destination return format_videos_destination(self.name, tag_name, None) elif self.kind == 'domain': # Use configured archive directory for domain collections + # Local import to avoid circular import: collections -> archive -> collections from modules.archive.lib import get_archive_directory base = get_archive_directory() if tag_name: @@ -737,6 +737,7 @@ class Collection(ModelHelper, Base): Similar to Channel.move_channel but without download management. """ + # Local imports to avoid circular import: collections -> files/events/flags -> collections from wrolpi.files.lib import move as move_files, refresh_files from wrolpi.events import Events from wrolpi import flags