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