refactor: centralize bootstrap dependency assembly
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
"""Application packages introduced by architecture governance."""
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from .container import ApplicationContainer, build_application_container
|
||||||
|
from .settings import RuntimeSettings, load_runtime_settings
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ApplicationContainer",
|
||||||
|
"RuntimeSettings",
|
||||||
|
"build_application_container",
|
||||||
|
"load_runtime_settings",
|
||||||
|
]
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from alert_service import AlertService
|
||||||
|
from chart_data_provider import EastmoneyChartClient, MarketChartClient
|
||||||
|
from database import ReviewDatabase
|
||||||
|
from ifind_client import IfindHttpClient
|
||||||
|
from mentor_agent import MentorSkillRegistry
|
||||||
|
from realtime_aggregator import WebRealtimeAggregator
|
||||||
|
from screener import ScreenerEngine
|
||||||
|
from strategy_tracking import StrategyTrackingService
|
||||||
|
from trade_journal import TradeJournalService
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ApplicationContainer:
|
||||||
|
database: ReviewDatabase
|
||||||
|
ifind: IfindHttpClient
|
||||||
|
screener: ScreenerEngine
|
||||||
|
strategy_tracking: StrategyTrackingService
|
||||||
|
alert_service: AlertService
|
||||||
|
trade_journal: TradeJournalService
|
||||||
|
mentor_skills: MentorSkillRegistry
|
||||||
|
realtime_aggregator: WebRealtimeAggregator
|
||||||
|
chart_data: MarketChartClient
|
||||||
|
|
||||||
|
|
||||||
|
def build_application_container(
|
||||||
|
database: ReviewDatabase,
|
||||||
|
credentials: dict[str, object],
|
||||||
|
mentor_skills_dir: Path,
|
||||||
|
private_mentor_skills_dir: Path,
|
||||||
|
) -> ApplicationContainer:
|
||||||
|
ifind = IfindHttpClient(
|
||||||
|
str(credentials.get("ifind_refresh_token") or ""),
|
||||||
|
str(credentials.get("ifind_access_token") or ""),
|
||||||
|
)
|
||||||
|
return ApplicationContainer(
|
||||||
|
database=database,
|
||||||
|
ifind=ifind,
|
||||||
|
screener=ScreenerEngine(database),
|
||||||
|
strategy_tracking=StrategyTrackingService(database),
|
||||||
|
alert_service=AlertService(database),
|
||||||
|
trade_journal=TradeJournalService(database),
|
||||||
|
mentor_skills=MentorSkillRegistry(mentor_skills_dir, private_mentor_skills_dir),
|
||||||
|
realtime_aggregator=WebRealtimeAggregator(),
|
||||||
|
chart_data=MarketChartClient(ifind, EastmoneyChartClient()),
|
||||||
|
)
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Mapping
|
||||||
|
|
||||||
|
from app_config import load_local_env, save_local_env
|
||||||
|
from security import SecretVault
|
||||||
|
|
||||||
|
|
||||||
|
def environment_credentials(environment: Mapping[str, str]) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"tushare_token": str(environment.get("TUSHARE_TOKEN") or "").strip(),
|
||||||
|
"ifind_refresh_token": str(environment.get("IFIND_REFRESH_TOKEN") or "").strip(),
|
||||||
|
"ifind_access_token": str(environment.get("IFIND_ACCESS_TOKEN") or "").strip(),
|
||||||
|
"platform_llm_primary_api_key": str(
|
||||||
|
environment.get("LLM_PRIMARY_API_KEY") or environment.get("LLM_API_KEY") or ""
|
||||||
|
).strip(),
|
||||||
|
"platform_llm_primary_base_url": str(
|
||||||
|
environment.get("LLM_PRIMARY_BASE_URL")
|
||||||
|
or environment.get("LLM_BASE_URL")
|
||||||
|
or "https://api.openai.com/v1"
|
||||||
|
).strip(),
|
||||||
|
"platform_llm_primary_model": str(
|
||||||
|
environment.get("LLM_PRIMARY_MODEL") or environment.get("LLM_MODEL") or ""
|
||||||
|
).strip(),
|
||||||
|
"platform_llm_fallback_api_key": str(
|
||||||
|
environment.get("LLM_FALLBACK_API_KEY") or ""
|
||||||
|
).strip(),
|
||||||
|
"platform_llm_fallback_base_url": str(
|
||||||
|
environment.get("LLM_FALLBACK_BASE_URL") or ""
|
||||||
|
).strip(),
|
||||||
|
"platform_llm_fallback_model": str(
|
||||||
|
environment.get("LLM_FALLBACK_MODEL") or ""
|
||||||
|
).strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RuntimeSettings:
|
||||||
|
encryption_key: str
|
||||||
|
initial_credentials: dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
def load_runtime_settings() -> RuntimeSettings:
|
||||||
|
load_local_env()
|
||||||
|
encryption_key = os.environ.get("APP_ENCRYPTION_KEY", "").strip()
|
||||||
|
if not encryption_key:
|
||||||
|
encryption_key = SecretVault.generate_key()
|
||||||
|
save_local_env({"APP_ENCRYPTION_KEY": encryption_key})
|
||||||
|
os.environ["APP_ENCRYPTION_KEY"] = encryption_key
|
||||||
|
return RuntimeSettings(
|
||||||
|
encryption_key=encryption_key,
|
||||||
|
initial_credentials=environment_credentials(os.environ),
|
||||||
|
)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"captured_from": "323c734",
|
"captured_from": "governed source tree",
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"http_server": "http.server.ThreadingHTTPServer",
|
"http_server": "http.server.ThreadingHTTPServer",
|
||||||
"application_processes": 1,
|
"application_processes": 1,
|
||||||
@@ -265,8 +265,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "server.py",
|
"path": "server.py",
|
||||||
"bytes": 269167,
|
"bytes": 267525,
|
||||||
"lines": 5965
|
"lines": 5938
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "static/redesign-v2.css",
|
"path": "static/redesign-v2.css",
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Stage 05: Bootstrap and Dependency Assembly
|
||||||
|
|
||||||
|
Date: 2026-07-29
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
- Environment and legacy LLM credential resolution moved to `backend/bootstrap/settings.py`.
|
||||||
|
- Encryption-key initialization remains behavior-compatible and server-side.
|
||||||
|
- Stable service construction moved to `backend/bootstrap/container.py`.
|
||||||
|
- `DashboardService` keeps its compatibility attributes but receives them from one application
|
||||||
|
container.
|
||||||
|
- The iFinD client is instantiated once and shared by chart services.
|
||||||
|
- HTTP routes, API payloads, background thread timing, database paths, and frontend assets are
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
## Transitional Boundary
|
||||||
|
|
||||||
|
Some methods still construct Tushare clients directly. Stage 06 introduces `DataGateway` and
|
||||||
|
migrates those provider creation paths without combining that work with bootstrap changes.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Reverting this stage restores inline service construction. No database or configuration
|
||||||
|
migration is required.
|
||||||
@@ -4,7 +4,6 @@ import argparse
|
|||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
import threading
|
import threading
|
||||||
@@ -16,10 +15,10 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import parse_qs, unquote, urlparse
|
from urllib.parse import parse_qs, unquote, urlparse
|
||||||
|
|
||||||
from alert_service import AlertService
|
|
||||||
from assistant_agent import ReviewAssistantError, stream_review_assistant
|
from assistant_agent import ReviewAssistantError, stream_review_assistant
|
||||||
from api_access import required_role
|
from api_access import required_role
|
||||||
from chart_data_provider import ChartDataError, EastmoneyChartClient, MarketChartClient
|
from backend.bootstrap import build_application_container, load_runtime_settings
|
||||||
|
from chart_data_provider import ChartDataError
|
||||||
from app_config import (
|
from app_config import (
|
||||||
DATA_DIR,
|
DATA_DIR,
|
||||||
MENTOR_SKILLS_DIR,
|
MENTOR_SKILLS_DIR,
|
||||||
@@ -30,12 +29,9 @@ from app_config import (
|
|||||||
TOKEN_PATTERN,
|
TOKEN_PATTERN,
|
||||||
USERNAME_PATTERN,
|
USERNAME_PATTERN,
|
||||||
add_months as _add_months,
|
add_months as _add_months,
|
||||||
load_local_env,
|
|
||||||
membership_boundary as _membership_boundary,
|
membership_boundary as _membership_boundary,
|
||||||
normalize_date,
|
normalize_date,
|
||||||
parse_iso_datetime as _parse_iso_datetime,
|
parse_iso_datetime as _parse_iso_datetime,
|
||||||
remove_local_env,
|
|
||||||
save_local_env,
|
|
||||||
tushare_code,
|
tushare_code,
|
||||||
validate_stock_code,
|
validate_stock_code,
|
||||||
validate_text,
|
validate_text,
|
||||||
@@ -50,17 +46,15 @@ from heaven_engine import (
|
|||||||
build_personal_field,
|
build_personal_field,
|
||||||
hexagram_from_lines,
|
hexagram_from_lines,
|
||||||
)
|
)
|
||||||
from ifind_client import IfindError, IfindHttpClient
|
from ifind_client import IfindError
|
||||||
from llm_strategy import LLMCompilerError, compile_strategy_with_llm, test_llm_connection
|
from llm_strategy import LLMCompilerError, compile_strategy_with_llm, test_llm_connection
|
||||||
from mentor_agent import MentorAgentError, MentorSkillRegistry, stream_with_mentor
|
from mentor_agent import MentorAgentError, stream_with_mentor
|
||||||
from market_insights import MarketInsightsService
|
from market_insights import MarketInsightsService
|
||||||
from realtime_aggregator import WebRealtimeAggregator
|
|
||||||
from screener import (
|
from screener import (
|
||||||
FACTOR_FIELDS,
|
FACTOR_FIELDS,
|
||||||
FACTOR_GROUPS,
|
FACTOR_GROUPS,
|
||||||
REGIMES,
|
REGIMES,
|
||||||
FactorDataService,
|
FactorDataService,
|
||||||
ScreenerEngine,
|
|
||||||
compile_local_strategy,
|
compile_local_strategy,
|
||||||
)
|
)
|
||||||
from security import SecretVault, hash_password, token_hash, verify_password
|
from security import SecretVault, hash_password, token_hash, verify_password
|
||||||
@@ -71,8 +65,6 @@ from sentiment_engine import (
|
|||||||
build_sentiment_history,
|
build_sentiment_history,
|
||||||
latest_contiguous_history,
|
latest_contiguous_history,
|
||||||
)
|
)
|
||||||
from strategy_tracking import StrategyTrackingService
|
|
||||||
from trade_journal import TradeJournalService
|
|
||||||
from tushare_client import TushareClient, TushareError, _sector_coverage_issue
|
from tushare_client import TushareClient, TushareError, _sector_coverage_issue
|
||||||
|
|
||||||
|
|
||||||
@@ -172,30 +164,8 @@ MENTOR_ETF_UNIVERSE = (
|
|||||||
|
|
||||||
class DashboardService:
|
class DashboardService:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
load_local_env()
|
runtime = load_runtime_settings()
|
||||||
environment_credentials = {
|
self.vault = SecretVault(runtime.encryption_key)
|
||||||
"tushare_token": os.environ.get("TUSHARE_TOKEN", "").strip(),
|
|
||||||
"ifind_refresh_token": os.environ.get("IFIND_REFRESH_TOKEN", "").strip(),
|
|
||||||
"ifind_access_token": os.environ.get("IFIND_ACCESS_TOKEN", "").strip(),
|
|
||||||
"platform_llm_primary_api_key": os.environ.get(
|
|
||||||
"LLM_PRIMARY_API_KEY", os.environ.get("LLM_API_KEY", "")
|
|
||||||
).strip(),
|
|
||||||
"platform_llm_primary_base_url": os.environ.get(
|
|
||||||
"LLM_PRIMARY_BASE_URL", os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1")
|
|
||||||
).strip(),
|
|
||||||
"platform_llm_primary_model": os.environ.get(
|
|
||||||
"LLM_PRIMARY_MODEL", os.environ.get("LLM_MODEL", "")
|
|
||||||
).strip(),
|
|
||||||
"platform_llm_fallback_api_key": os.environ.get("LLM_FALLBACK_API_KEY", "").strip(),
|
|
||||||
"platform_llm_fallback_base_url": os.environ.get("LLM_FALLBACK_BASE_URL", "").strip(),
|
|
||||||
"platform_llm_fallback_model": os.environ.get("LLM_FALLBACK_MODEL", "").strip(),
|
|
||||||
}
|
|
||||||
encryption_key = os.environ.get("APP_ENCRYPTION_KEY", "").strip()
|
|
||||||
if not encryption_key:
|
|
||||||
encryption_key = SecretVault.generate_key()
|
|
||||||
save_local_env({"APP_ENCRYPTION_KEY": encryption_key})
|
|
||||||
os.environ["APP_ENCRYPTION_KEY"] = encryption_key
|
|
||||||
self.vault = SecretVault(encryption_key)
|
|
||||||
self.database = ReviewDatabase(DATA_DIR / "review.db")
|
self.database = ReviewDatabase(DATA_DIR / "review.db")
|
||||||
self.sync_lock = threading.Lock()
|
self.sync_lock = threading.Lock()
|
||||||
self.auth_lock = threading.Lock()
|
self.auth_lock = threading.Lock()
|
||||||
@@ -204,18 +174,21 @@ class DashboardService:
|
|||||||
self._auto_screener_last_attempt: dict[str, datetime] = {}
|
self._auto_screener_last_attempt: dict[str, datetime] = {}
|
||||||
self._ifind_event_lock = threading.Lock()
|
self._ifind_event_lock = threading.Lock()
|
||||||
self._request_context = threading.local()
|
self._request_context = threading.local()
|
||||||
self._system_credentials = self._load_system_credentials(environment_credentials)
|
self._system_credentials = self._load_system_credentials(runtime.initial_credentials)
|
||||||
self.ifind = IfindHttpClient(
|
self.container = build_application_container(
|
||||||
str(self._system_credentials.get("ifind_refresh_token") or ""),
|
self.database,
|
||||||
str(self._system_credentials.get("ifind_access_token") or ""),
|
self._system_credentials,
|
||||||
|
MENTOR_SKILLS_DIR,
|
||||||
|
PRIVATE_MENTOR_SKILLS_DIR,
|
||||||
)
|
)
|
||||||
self.screener = ScreenerEngine(self.database)
|
self.ifind = self.container.ifind
|
||||||
self.strategy_tracking = StrategyTrackingService(self.database)
|
self.screener = self.container.screener
|
||||||
self.alert_service = AlertService(self.database)
|
self.strategy_tracking = self.container.strategy_tracking
|
||||||
self.trade_journal = TradeJournalService(self.database)
|
self.alert_service = self.container.alert_service
|
||||||
self.mentor_skills = MentorSkillRegistry(MENTOR_SKILLS_DIR, PRIVATE_MENTOR_SKILLS_DIR)
|
self.trade_journal = self.container.trade_journal
|
||||||
self.realtime_aggregator = WebRealtimeAggregator()
|
self.mentor_skills = self.container.mentor_skills
|
||||||
self.chart_data = MarketChartClient(self.ifind, EastmoneyChartClient())
|
self.realtime_aggregator = self.container.realtime_aggregator
|
||||||
|
self.chart_data = self.container.chart_data
|
||||||
self.screener.ensure_builtin_strategies()
|
self.screener.ensure_builtin_strategies()
|
||||||
self._background_stop = threading.Event()
|
self._background_stop = threading.Event()
|
||||||
self._background_thread = threading.Thread(
|
self._background_thread = threading.Thread(
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from backend.bootstrap import build_application_container
|
||||||
|
from backend.bootstrap.settings import environment_credentials
|
||||||
|
from database import ReviewDatabase
|
||||||
|
|
||||||
|
|
||||||
|
class BootstrapContainerTests(unittest.TestCase):
|
||||||
|
def test_environment_credentials_preserve_legacy_model_fallbacks(self) -> None:
|
||||||
|
result = environment_credentials(
|
||||||
|
{
|
||||||
|
"TUSHARE_TOKEN": " tushare ",
|
||||||
|
"IFIND_REFRESH_TOKEN": " refresh ",
|
||||||
|
"LLM_API_KEY": "legacy-key",
|
||||||
|
"LLM_BASE_URL": "https://legacy.example/v1",
|
||||||
|
"LLM_MODEL": "legacy-model",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(result["tushare_token"], "tushare")
|
||||||
|
self.assertEqual(result["ifind_refresh_token"], "refresh")
|
||||||
|
self.assertEqual(result["platform_llm_primary_api_key"], "legacy-key")
|
||||||
|
self.assertEqual(result["platform_llm_primary_base_url"], "https://legacy.example/v1")
|
||||||
|
self.assertEqual(result["platform_llm_primary_model"], "legacy-model")
|
||||||
|
|
||||||
|
def test_container_shares_one_database_and_one_ifind_client(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
public_skills = root / "public"
|
||||||
|
private_skills = root / "private"
|
||||||
|
public_skills.mkdir()
|
||||||
|
private_skills.mkdir()
|
||||||
|
database = ReviewDatabase(root / "review.db")
|
||||||
|
container = build_application_container(
|
||||||
|
database,
|
||||||
|
{"ifind_refresh_token": "refresh-token", "ifind_access_token": "access-token"},
|
||||||
|
public_skills,
|
||||||
|
private_skills,
|
||||||
|
)
|
||||||
|
self.assertIs(container.database, database)
|
||||||
|
self.assertIs(container.screener.database, database)
|
||||||
|
self.assertIs(container.strategy_tracking.database, database)
|
||||||
|
self.assertIs(container.alert_service.database, database)
|
||||||
|
self.assertIs(container.trade_journal.database, database)
|
||||||
|
self.assertIs(container.chart_data.ifind, container.ifind)
|
||||||
|
self.assertTrue(container.ifind.configured)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -105,7 +105,7 @@ def build() -> dict[str, Any]:
|
|||||||
tables = database_inventory(database)
|
tables = database_inventory(database)
|
||||||
return {
|
return {
|
||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"captured_from": "323c734",
|
"captured_from": "governed source tree",
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"http_server": "http.server.ThreadingHTTPServer",
|
"http_server": "http.server.ThreadingHTTPServer",
|
||||||
"application_processes": 1,
|
"application_processes": 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user