refactor(arch): eliminate all cross-module model imports in service layer
Some checks failed
CI / ruff (push) Successful in 9s
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / pytest (push) Has been cancelled

Enforce MOD-025/MOD-026 rules: zero top-level cross-module model imports
remain in any service file. All 66 files migrated using deferred import
patterns (method-body, _get_model() helpers, instance-cached self._Model)
and new cross-module service methods in tenancy. Documentation updated
with Pattern 6 (deferred imports), migration plan marked complete, and
violations status reflects 84→0 service-layer violations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 06:13:15 +01:00
parent e3a52f6536
commit 86e85a98b8
66 changed files with 2242 additions and 1295 deletions

View File

@@ -8,6 +8,8 @@ This module provides functions for:
- Generating audit reports
"""
from __future__ import annotations
import logging
from typing import Any
@@ -16,7 +18,6 @@ from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from app.modules.tenancy.exceptions import AdminOperationException
from app.modules.tenancy.models import AdminAuditLog, User
from app.modules.tenancy.schemas.admin import (
AdminAuditLogFilters,
AdminAuditLogResponse,
@@ -25,6 +26,13 @@ from app.modules.tenancy.schemas.admin import (
logger = logging.getLogger(__name__)
def _get_audit_log_model():
"""Deferred import for AdminAuditLog model (lives in tenancy, consumed by monitoring)."""
from app.modules.tenancy.models import AdminAuditLog
return AdminAuditLog
class AdminAuditService:
"""Service for admin audit logging."""
@@ -57,6 +65,7 @@ class AdminAuditService:
Returns:
Created AdminAuditLog instance
"""
AdminAuditLog = _get_audit_log_model()
try:
audit_log = AdminAuditLog(
admin_user_id=admin_user_id,
@@ -98,9 +107,12 @@ class AdminAuditService:
Returns:
List of audit log responses
"""
AdminAuditLog = _get_audit_log_model()
try:
query = db.query(AdminAuditLog).join(
User, AdminAuditLog.admin_user_id == User.id
from sqlalchemy.orm import joinedload
query = db.query(AdminAuditLog).options(
joinedload(AdminAuditLog.admin_user)
)
# Apply filters
@@ -158,6 +170,7 @@ class AdminAuditService:
def get_audit_logs_count(self, db: Session, filters: AdminAuditLogFilters) -> int:
"""Get total count of audit logs matching filters."""
AdminAuditLog = _get_audit_log_model()
try:
query = db.query(AdminAuditLog)
@@ -199,6 +212,7 @@ class AdminAuditService:
self, db: Session, target_type: str, target_id: str, limit: int = 50
) -> list[AdminAuditLogResponse]:
"""Get all actions performed on a specific target."""
AdminAuditLog = _get_audit_log_model()
try:
logs = (
db.query(AdminAuditLog)

View File

@@ -8,16 +8,11 @@ AuditProviderProtocol interface.
"""
import logging
from typing import TYPE_CHECKING
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from app.modules.contracts.audit import AuditEvent
from app.modules.tenancy.models import AdminAuditLog
if TYPE_CHECKING:
pass
logger = logging.getLogger(__name__)
@@ -46,6 +41,8 @@ class DatabaseAuditProvider:
True if logged successfully, False otherwise
"""
try:
from app.modules.tenancy.models import AdminAuditLog
audit_log = AdminAuditLog(
admin_user_id=event.admin_user_id,
action=event.action,

View File

@@ -4,13 +4,16 @@ Background Tasks Service
Service for monitoring background tasks across the system
"""
from __future__ import annotations
from datetime import UTC, datetime
from typing import TYPE_CHECKING
from sqlalchemy import case, desc, func
from sqlalchemy.orm import Session
from app.modules.dev_tools.models import ArchitectureScan, TestRun
from app.modules.marketplace.models import MarketplaceImportJob
if TYPE_CHECKING:
from app.modules.dev_tools.models import ArchitectureScan, TestRun
class BackgroundTasksService:
@@ -18,100 +21,86 @@ class BackgroundTasksService:
def get_import_jobs(
self, db: Session, status: str | None = None, limit: int = 50
) -> list[MarketplaceImportJob]:
) -> list:
"""Get import jobs with optional status filter"""
query = db.query(MarketplaceImportJob)
if status:
query = query.filter(MarketplaceImportJob.status == status)
return query.order_by(desc(MarketplaceImportJob.created_at)).limit(limit).all()
from app.modules.marketplace.services.marketplace_import_job_service import (
marketplace_import_job_service,
)
jobs, _ = marketplace_import_job_service.get_all_import_jobs_paginated(
db, status=status, limit=limit,
)
return jobs
def get_test_runs(
self, db: Session, status: str | None = None, limit: int = 50
) -> list[TestRun]:
"""Get test runs with optional status filter"""
query = db.query(TestRun)
if status:
query = query.filter(TestRun.status == status)
return query.order_by(desc(TestRun.timestamp)).limit(limit).all()
from app.modules.dev_tools.models import TestRun as TestRunModel
def get_running_imports(self, db: Session) -> list[MarketplaceImportJob]:
query = db.query(TestRunModel)
if status:
query = query.filter(TestRunModel.status == status)
return query.order_by(desc(TestRunModel.timestamp)).limit(limit).all()
def get_running_imports(self, db: Session) -> list:
"""Get currently running import jobs"""
return (
db.query(MarketplaceImportJob)
.filter(MarketplaceImportJob.status == "processing")
.all()
from app.modules.marketplace.services.marketplace_import_job_service import (
marketplace_import_job_service,
)
jobs, _ = marketplace_import_job_service.get_all_import_jobs_paginated(
db, status="processing", limit=100,
)
return jobs
def get_running_test_runs(self, db: Session) -> list[TestRun]:
"""Get currently running test runs"""
from app.modules.dev_tools.models import TestRun as TestRunModel
# SVC-005 - Platform-level, TestRuns not store-scoped
return db.query(TestRun).filter(TestRun.status == "running").all() # SVC-005
return db.query(TestRunModel).filter(TestRunModel.status == "running").all() # SVC-005
def get_import_stats(self, db: Session) -> dict:
"""Get import job statistics"""
today_start = datetime.now(UTC).replace(
hour=0, minute=0, second=0, microsecond=0
)
stats = db.query(
func.count(MarketplaceImportJob.id).label("total"),
func.sum(
case((MarketplaceImportJob.status == "processing", 1), else_=0)
).label("running"),
func.sum(
case(
(
MarketplaceImportJob.status.in_(
["completed", "completed_with_errors"]
),
1,
),
else_=0,
)
).label("completed"),
func.sum(
case((MarketplaceImportJob.status == "failed", 1), else_=0)
).label("failed"),
).first()
today_count = (
db.query(func.count(MarketplaceImportJob.id))
.filter(MarketplaceImportJob.created_at >= today_start)
.scalar()
or 0
from app.modules.marketplace.services.marketplace_import_job_service import (
marketplace_import_job_service,
)
stats = marketplace_import_job_service.get_import_job_stats(db)
return {
"total": stats.total or 0,
"running": stats.running or 0,
"completed": stats.completed or 0,
"failed": stats.failed or 0,
"today": today_count,
"total": stats.get("total", 0),
"running": stats.get("processing", 0),
"completed": stats.get("completed", 0),
"failed": stats.get("failed", 0),
"today": stats.get("today", 0),
}
def get_test_run_stats(self, db: Session) -> dict:
"""Get test run statistics"""
from app.modules.dev_tools.models import TestRun as TestRunModel
today_start = datetime.now(UTC).replace(
hour=0, minute=0, second=0, microsecond=0
)
stats = db.query(
func.count(TestRun.id).label("total"),
func.sum(case((TestRun.status == "running", 1), else_=0)).label(
func.count(TestRunModel.id).label("total"),
func.sum(case((TestRunModel.status == "running", 1), else_=0)).label(
"running"
),
func.sum(case((TestRun.status == "passed", 1), else_=0)).label(
func.sum(case((TestRunModel.status == "passed", 1), else_=0)).label(
"completed"
),
func.sum(
case((TestRun.status.in_(["failed", "error"]), 1), else_=0)
case((TestRunModel.status.in_(["failed", "error"]), 1), else_=0)
).label("failed"),
func.avg(TestRun.duration_seconds).label("avg_duration"),
func.avg(TestRunModel.duration_seconds).label("avg_duration"),
).first()
today_count = (
db.query(func.count(TestRun.id))
.filter(TestRun.timestamp >= today_start)
db.query(func.count(TestRunModel.id))
.filter(TestRunModel.timestamp >= today_start)
.scalar()
or 0
)
@@ -129,36 +118,42 @@ class BackgroundTasksService:
self, db: Session, status: str | None = None, limit: int = 50
) -> list[ArchitectureScan]:
"""Get code quality scans with optional status filter"""
query = db.query(ArchitectureScan)
from app.modules.dev_tools.models import ArchitectureScan as ScanModel
query = db.query(ScanModel)
if status:
query = query.filter(ArchitectureScan.status == status)
return query.order_by(desc(ArchitectureScan.timestamp)).limit(limit).all()
query = query.filter(ScanModel.status == status)
return query.order_by(desc(ScanModel.timestamp)).limit(limit).all()
def get_running_scans(self, db: Session) -> list[ArchitectureScan]:
"""Get currently running code quality scans"""
from app.modules.dev_tools.models import ArchitectureScan as ScanModel
return (
db.query(ArchitectureScan)
.filter(ArchitectureScan.status.in_(["pending", "running"]))
db.query(ScanModel)
.filter(ScanModel.status.in_(["pending", "running"]))
.all()
)
def get_scan_stats(self, db: Session) -> dict:
"""Get code quality scan statistics"""
from app.modules.dev_tools.models import ArchitectureScan as ScanModel
today_start = datetime.now(UTC).replace(
hour=0, minute=0, second=0, microsecond=0
)
stats = db.query(
func.count(ArchitectureScan.id).label("total"),
func.count(ScanModel.id).label("total"),
func.sum(
case(
(ArchitectureScan.status.in_(["pending", "running"]), 1), else_=0
(ScanModel.status.in_(["pending", "running"]), 1), else_=0
)
).label("running"),
func.sum(
case(
(
ArchitectureScan.status.in_(
ScanModel.status.in_(
["completed", "completed_with_warnings"]
),
1,
@@ -167,14 +162,14 @@ class BackgroundTasksService:
)
).label("completed"),
func.sum(
case((ArchitectureScan.status == "failed", 1), else_=0)
case((ScanModel.status == "failed", 1), else_=0)
).label("failed"),
func.avg(ArchitectureScan.duration_seconds).label("avg_duration"),
func.avg(ScanModel.duration_seconds).label("avg_duration"),
).first()
today_count = (
db.query(func.count(ArchitectureScan.id))
.filter(ArchitectureScan.timestamp >= today_start)
db.query(func.count(ScanModel.id))
.filter(ScanModel.timestamp >= today_start)
.scalar()
or 0
)

View File

@@ -13,13 +13,14 @@ import logging
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.modules.contracts.metrics import MetricsContext
from app.modules.core.services.stats_aggregator import stats_aggregator
from app.modules.monitoring.models.capacity_snapshot import CapacitySnapshot
from app.modules.tenancy.models import Platform, Store, StoreUser
from app.modules.tenancy.services.platform_service import platform_service
from app.modules.tenancy.services.store_service import store_service
from app.modules.tenancy.services.team_service import team_service
logger = logging.getLogger(__name__)
@@ -63,17 +64,12 @@ class CapacityForecastService:
return existing
# Gather metrics
total_stores = db.query(func.count(Store.id)).scalar() or 0
active_stores = (
db.query(func.count(Store.id))
.filter(Store.is_active == True) # noqa: E712
.scalar()
or 0
)
total_stores = store_service.get_total_store_count(db)
active_stores = store_service.get_total_store_count(db, active_only=True)
# Resource metrics via provider pattern (avoids cross-module imports)
start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
platform = db.query(Platform).first()
platform = platform_service.get_default_platform(db)
if not platform:
raise ValueError("No platform found in database")
platform_id = platform.id
@@ -89,12 +85,7 @@ class CapacityForecastService:
trial_stores = stats.get("billing.trial_subscriptions", 0)
total_products = stats.get("catalog.total_products", 0)
total_team = (
db.query(func.count(StoreUser.id))
.filter(StoreUser.is_active == True) # noqa: E712
.scalar()
or 0
)
total_team = team_service.get_total_active_team_member_count(db)
# Orders this month (from stats aggregator)
total_orders = stats.get("orders.in_period", 0)

View File

@@ -21,7 +21,6 @@ from sqlalchemy.orm import Session
from app.core.config import settings
from app.exceptions import ResourceNotFoundException
from app.modules.tenancy.exceptions import AdminOperationException
from app.modules.tenancy.models import ApplicationLog
from app.modules.tenancy.schemas.admin import (
ApplicationLogFilters,
ApplicationLogListResponse,
@@ -33,6 +32,13 @@ from app.modules.tenancy.schemas.admin import (
logger = logging.getLogger(__name__)
def _get_application_log_model():
"""Deferred import for ApplicationLog model (lives in tenancy, consumed by monitoring)."""
from app.modules.tenancy.models import ApplicationLog
return ApplicationLog
class LogService:
"""Service for managing application logs."""
@@ -49,6 +55,7 @@ class LogService:
Returns:
Paginated list of logs
"""
ApplicationLog = _get_application_log_model()
try:
query = db.query(ApplicationLog)
@@ -125,6 +132,7 @@ class LogService:
Returns:
Log statistics
"""
ApplicationLog = _get_application_log_model()
try:
cutoff_date = datetime.now(UTC) - timedelta(days=days)
@@ -329,6 +337,7 @@ class LogService:
Returns:
Number of logs deleted
"""
ApplicationLog = _get_application_log_model()
try:
cutoff_date = datetime.now(UTC) - timedelta(days=retention_days)
@@ -356,6 +365,7 @@ class LogService:
def delete_log(self, db: Session, log_id: int) -> str:
"""Delete a specific log entry."""
ApplicationLog = _get_application_log_model()
try:
log_entry = (
db.query(ApplicationLog).filter(ApplicationLog.id == log_id).first()

View File

@@ -13,15 +13,11 @@ import logging
from datetime import datetime
import psutil
from sqlalchemy import func, text
from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from app.modules.catalog.models import Product
from app.modules.cms.services.media_service import media_service
from app.modules.inventory.models import Inventory
from app.modules.orders.models import Order
from app.modules.tenancy.models import Store
logger = logging.getLogger(__name__)
@@ -94,10 +90,15 @@ class PlatformHealthService:
def get_database_metrics(self, db: Session) -> dict:
"""Get database statistics."""
products_count = db.query(func.count(Product.id)).scalar() or 0
orders_count = db.query(func.count(Order.id)).scalar() or 0
stores_count = db.query(func.count(Store.id)).scalar() or 0
inventory_count = db.query(func.count(Inventory.id)).scalar() or 0
from app.modules.catalog.services.product_service import product_service
from app.modules.inventory.services.inventory_service import inventory_service
from app.modules.orders.services.order_service import order_service
from app.modules.tenancy.services.store_service import store_service
products_count = product_service.get_total_product_count(db)
orders_count = order_service.get_total_order_count(db)
stores_count = store_service.get_total_store_count(db)
inventory_count = inventory_service.get_total_inventory_count(db)
db_size = self._get_database_size(db)
@@ -122,17 +123,23 @@ class PlatformHealthService:
def get_capacity_metrics(self, db: Session) -> dict:
"""Get capacity-focused metrics for planning."""
from app.modules.catalog.services.product_service import product_service
from app.modules.orders.services.order_service import order_service
from app.modules.tenancy.services.store_service import store_service
# Products total
products_total = db.query(func.count(Product.id)).scalar() or 0
products_total = product_service.get_total_product_count(db)
# Products by store
store_counts = (
db.query(Store.name, func.count(Product.id))
.join(Product, Store.id == Product.store_id)
.group_by(Store.name)
.all()
products_by_store = {}
# Get stores that have products
from app.modules.catalog.services.store_product_service import (
store_product_service,
)
products_by_store = {name or "Unknown": count for name, count in store_counts}
catalog_stores = store_product_service.get_catalog_stores(db)
for s in catalog_stores:
count = product_service.get_store_product_count(db, s["id"])
products_by_store[s["name"] or "Unknown"] = count
# Image storage
image_stats = media_service.get_storage_stats(db)
@@ -142,20 +149,10 @@ class PlatformHealthService:
# Orders this month
start_of_month = datetime.utcnow().replace(day=1, hour=0, minute=0, second=0)
orders_this_month = (
db.query(func.count(Order.id))
.filter(Order.created_at >= start_of_month)
.scalar()
or 0
)
orders_this_month = order_service.get_total_order_count(db, date_from=start_of_month)
# Active stores
active_stores = (
db.query(func.count(Store.id))
.filter(Store.is_active == True) # noqa: E712
.scalar()
or 0
)
active_stores = store_service.get_total_store_count(db, active_only=True)
return {
"products_total": products_total,
@@ -173,15 +170,12 @@ class PlatformHealthService:
Returns aggregated limits and current usage for capacity planning.
"""
from app.modules.billing.models import MerchantSubscription
from app.modules.tenancy.models import StoreUser
from app.modules.billing.services.subscription_service import (
subscription_service,
)
# Get all active subscriptions with tier + feature limits
subscriptions = (
db.query(MerchantSubscription)
.filter(MerchantSubscription.status.in_(["active", "trial"]))
.all()
)
subscriptions = subscription_service.get_all_active_subscriptions(db)
# Aggregate theoretical limits from TierFeatureLimit
total_products_limit = 0
@@ -222,22 +216,16 @@ class PlatformHealthService:
total_team_limit += team_limit
# Get actual usage
actual_products = db.query(func.count(Product.id)).scalar() or 0
actual_team = (
db.query(func.count(StoreUser.id))
.filter(StoreUser.is_active == True) # noqa: E712
.scalar()
or 0
)
from app.modules.catalog.services.product_service import product_service
from app.modules.orders.services.order_service import order_service
from app.modules.tenancy.services.team_service import team_service
actual_products = product_service.get_total_product_count(db)
actual_team = team_service.get_total_active_team_member_count(db)
# Orders this month
start_of_month = datetime.utcnow().replace(day=1, hour=0, minute=0, second=0)
total_orders_used = (
db.query(func.count(Order.id))
.filter(Order.created_at >= start_of_month)
.scalar()
or 0
)
total_orders_used = order_service.get_total_order_count(db, date_from=start_of_month)
def calc_utilization(actual: int, limit: int, unlimited: int) -> dict:
if unlimited > 0: