refactor: enforce account scoped repository boundaries
This commit is contained in:
@@ -6,6 +6,7 @@ from collections.abc import Callable
|
||||
|
||||
from alert_service import AlertService
|
||||
from backend.data import DataGateway, build_data_gateway
|
||||
from backend.database.repositories import RepositoryBundle, build_repository_bundle
|
||||
from chart_data_provider import MarketChartClient
|
||||
from database import ReviewDatabase
|
||||
from ifind_client import IfindHttpClient
|
||||
@@ -19,6 +20,7 @@ from trade_journal import TradeJournalService
|
||||
@dataclass(frozen=True)
|
||||
class ApplicationContainer:
|
||||
database: ReviewDatabase
|
||||
repositories: RepositoryBundle
|
||||
data_gateway: DataGateway
|
||||
ifind: IfindHttpClient
|
||||
screener: ScreenerEngine
|
||||
@@ -38,14 +40,16 @@ def build_application_container(
|
||||
tushare_token_supplier: Callable[[], str] | None = None,
|
||||
) -> ApplicationContainer:
|
||||
data_gateway = build_data_gateway(credentials, tushare_token_supplier)
|
||||
repositories = build_repository_bundle(database)
|
||||
return ApplicationContainer(
|
||||
database=database,
|
||||
repositories=repositories,
|
||||
data_gateway=data_gateway,
|
||||
ifind=data_gateway.ifind,
|
||||
screener=ScreenerEngine(database),
|
||||
strategy_tracking=StrategyTrackingService(database),
|
||||
alert_service=AlertService(database),
|
||||
trade_journal=TradeJournalService(database),
|
||||
strategy_tracking=StrategyTrackingService(repositories.strategy_tracking),
|
||||
alert_service=AlertService(repositories.alerts),
|
||||
trade_journal=TradeJournalService(repositories.trades),
|
||||
mentor_skills=MentorSkillRegistry(mentor_skills_dir, private_mentor_skills_dir),
|
||||
realtime_aggregator=data_gateway.realtime_observer,
|
||||
chart_data=data_gateway.chart_data,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from .ports import AlertRepository, StrategyTrackingRepository, TradeJournalRepository
|
||||
from .sqlite import (
|
||||
RepositoryBundle,
|
||||
SQLiteAlertRepository,
|
||||
SQLiteStrategyTrackingRepository,
|
||||
SQLiteTradeJournalRepository,
|
||||
build_repository_bundle,
|
||||
require_user_id,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AlertRepository",
|
||||
"RepositoryBundle",
|
||||
"SQLiteAlertRepository",
|
||||
"SQLiteStrategyTrackingRepository",
|
||||
"SQLiteTradeJournalRepository",
|
||||
"StrategyTrackingRepository",
|
||||
"TradeJournalRepository",
|
||||
"build_repository_bundle",
|
||||
"require_user_id",
|
||||
]
|
||||
@@ -0,0 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Protocol
|
||||
|
||||
|
||||
class AlertRepository(Protocol):
|
||||
def save_alert(
|
||||
self, user_id: int, kind: str, title: str, content: str,
|
||||
available_date: str, code: str, dedupe_key: str,
|
||||
) -> int: ...
|
||||
|
||||
def list_alerts(
|
||||
self, user_id: int, as_of: str, unread_only: bool = False, limit: int = 100,
|
||||
) -> list[dict[str, Any]]: ...
|
||||
|
||||
def count_unread_alerts(self, user_id: int, as_of: str) -> int: ...
|
||||
|
||||
def mark_alert_read(self, user_id: int, alert_id: int) -> bool: ...
|
||||
|
||||
def mark_all_alerts_read(self, user_id: int, as_of: str) -> int: ...
|
||||
|
||||
def delete_alert(self, user_id: int, alert_id: int) -> bool: ...
|
||||
|
||||
|
||||
class TradeJournalRepository(Protocol):
|
||||
def save_trade_entry(self, *args: Any, **kwargs: Any) -> int: ...
|
||||
|
||||
def list_trade_entries(
|
||||
self, user_id: int, start_date: str = "", end_date: str = "",
|
||||
code: str = "", limit: int = 300,
|
||||
) -> list[dict[str, Any]]: ...
|
||||
|
||||
def delete_trade_entry(self, user_id: int, trade_id: int) -> bool: ...
|
||||
|
||||
|
||||
class StrategyTrackingRepository(Protocol):
|
||||
def save_strategy_tracks(
|
||||
self, user_id: int, run_id: int, selection_date: str,
|
||||
strategy_name: str, candidates: list[dict[str, Any]],
|
||||
) -> int: ...
|
||||
|
||||
def get_screener_run(self, user_id: int, run_id: int) -> dict[str, Any] | None: ...
|
||||
|
||||
def delete_strategy_track(self, user_id: int, track_id: int) -> bool: ...
|
||||
|
||||
def list_strategy_tracks(
|
||||
self, user_id: int, limit_batches: int = 12,
|
||||
) -> list[dict[str, Any]]: ...
|
||||
|
||||
def load_tracking_bars(
|
||||
self, targets: list[tuple[str, str]], limit: int = 5,
|
||||
) -> dict[tuple[str, str], list[dict[str, Any]]]: ...
|
||||
@@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from database import ReviewDatabase
|
||||
|
||||
|
||||
def require_user_id(value: int) -> int:
|
||||
user_id = int(value)
|
||||
if user_id <= 0:
|
||||
raise ValueError("A positive account owner is required")
|
||||
return user_id
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SQLiteAlertRepository:
|
||||
database: ReviewDatabase
|
||||
|
||||
def save_alert(self, user_id: int, *args: Any, **kwargs: Any) -> int:
|
||||
return self.database.save_alert(require_user_id(user_id), *args, **kwargs)
|
||||
|
||||
def list_alerts(
|
||||
self, user_id: int, as_of: str, unread_only: bool = False, limit: int = 100,
|
||||
) -> list[dict[str, Any]]:
|
||||
return self.database.list_alerts(
|
||||
require_user_id(user_id), as_of, unread_only, limit
|
||||
)
|
||||
|
||||
def count_unread_alerts(self, user_id: int, as_of: str) -> int:
|
||||
return self.database.count_unread_alerts(require_user_id(user_id), as_of)
|
||||
|
||||
def mark_alert_read(self, user_id: int, alert_id: int) -> bool:
|
||||
return self.database.mark_alert_read(require_user_id(user_id), alert_id)
|
||||
|
||||
def mark_all_alerts_read(self, user_id: int, as_of: str) -> int:
|
||||
return self.database.mark_all_alerts_read(require_user_id(user_id), as_of)
|
||||
|
||||
def delete_alert(self, user_id: int, alert_id: int) -> bool:
|
||||
return self.database.delete_alert(require_user_id(user_id), alert_id)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SQLiteTradeJournalRepository:
|
||||
database: ReviewDatabase
|
||||
|
||||
def save_trade_entry(self, user_id: int, *args: Any, **kwargs: Any) -> int:
|
||||
return self.database.save_trade_entry(require_user_id(user_id), *args, **kwargs)
|
||||
|
||||
def list_trade_entries(
|
||||
self, user_id: int, start_date: str = "", end_date: str = "",
|
||||
code: str = "", limit: int = 300,
|
||||
) -> list[dict[str, Any]]:
|
||||
return self.database.list_trade_entries(
|
||||
require_user_id(user_id), start_date, end_date, code, limit
|
||||
)
|
||||
|
||||
def delete_trade_entry(self, user_id: int, trade_id: int) -> bool:
|
||||
return self.database.delete_trade_entry(require_user_id(user_id), trade_id)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SQLiteStrategyTrackingRepository:
|
||||
database: ReviewDatabase
|
||||
|
||||
def save_strategy_tracks(
|
||||
self, user_id: int, run_id: int, selection_date: str,
|
||||
strategy_name: str, candidates: list[dict[str, Any]],
|
||||
) -> int:
|
||||
return self.database.save_strategy_tracks(
|
||||
require_user_id(user_id), run_id, selection_date, strategy_name, candidates
|
||||
)
|
||||
|
||||
def get_screener_run(self, user_id: int, run_id: int) -> dict[str, Any] | None:
|
||||
owner_id = int(user_id)
|
||||
if owner_id < 0:
|
||||
raise ValueError("Account owner cannot be negative")
|
||||
return self.database.get_screener_run(owner_id, run_id)
|
||||
|
||||
def delete_strategy_track(self, user_id: int, track_id: int) -> bool:
|
||||
return self.database.delete_strategy_track(require_user_id(user_id), track_id)
|
||||
|
||||
def list_strategy_tracks(
|
||||
self, user_id: int, limit_batches: int = 12,
|
||||
) -> list[dict[str, Any]]:
|
||||
return self.database.list_strategy_tracks(
|
||||
require_user_id(user_id), limit_batches
|
||||
)
|
||||
|
||||
def load_tracking_bars(
|
||||
self, targets: list[tuple[str, str]], limit: int = 5,
|
||||
) -> dict[tuple[str, str], list[dict[str, Any]]]:
|
||||
return self.database.load_tracking_bars(targets, limit)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RepositoryBundle:
|
||||
alerts: SQLiteAlertRepository
|
||||
trades: SQLiteTradeJournalRepository
|
||||
strategy_tracking: SQLiteStrategyTrackingRepository
|
||||
|
||||
|
||||
def build_repository_bundle(database: ReviewDatabase) -> RepositoryBundle:
|
||||
return RepositoryBundle(
|
||||
alerts=SQLiteAlertRepository(database),
|
||||
trades=SQLiteTradeJournalRepository(database),
|
||||
strategy_tracking=SQLiteStrategyTrackingRepository(database),
|
||||
)
|
||||
Reference in New Issue
Block a user