feat(sector-radar): 接入Tushare事实与版本化存储
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
from dataclasses import replace
|
||||
from datetime import UTC, date, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from zhixing_server.modules.sector_radar.domain.models import (
|
||||
PublicationStatus,
|
||||
RadarPublication,
|
||||
SectorType,
|
||||
)
|
||||
from zhixing_server.modules.sector_radar.domain.persistence import MembershipRecord
|
||||
from zhixing_server.modules.sector_radar.domain.source import build_source_snapshot
|
||||
from zhixing_server.modules.sector_radar.infrastructure.memory import (
|
||||
InMemorySectorRadarRepository,
|
||||
)
|
||||
|
||||
TARGET_DATE = date(2026, 8, 28)
|
||||
STARTED_AT = datetime(2026, 8, 28, 17, 30, tzinfo=UTC)
|
||||
|
||||
|
||||
def make_running(publication_id: str, target_trade_date: date = TARGET_DATE) -> RadarPublication:
|
||||
return RadarPublication(
|
||||
publication_id=publication_id,
|
||||
target_trade_date=target_trade_date,
|
||||
status=PublicationStatus.RUNNING,
|
||||
source_version="tushare-pro-v1",
|
||||
universe_version="eastmoney-dc-v1",
|
||||
metric_versions=("zhixing_amount_net_bn_v1",),
|
||||
input_hash=None,
|
||||
coverage=Decimal(0),
|
||||
started_at=STARTED_AT,
|
||||
)
|
||||
|
||||
|
||||
def finish(
|
||||
publication: RadarPublication,
|
||||
status: PublicationStatus,
|
||||
*,
|
||||
offset_minutes: int = 5,
|
||||
) -> RadarPublication:
|
||||
return replace(
|
||||
publication,
|
||||
status=status,
|
||||
input_hash="a" * 64 if status is PublicationStatus.SUCCESS else None,
|
||||
coverage=Decimal("1") if status is PublicationStatus.SUCCESS else Decimal("0.8"),
|
||||
finished_at=publication.started_at + timedelta(minutes=offset_minutes),
|
||||
error_summary=None if status is PublicationStatus.SUCCESS else "safe_error",
|
||||
)
|
||||
|
||||
|
||||
def test_source_and_membership_revisions_are_idempotent_but_not_overwritable() -> None:
|
||||
repository = InMemorySectorRadarRepository()
|
||||
snapshot = build_source_snapshot(
|
||||
api_name="dc_member",
|
||||
params={"trade_date": "20260828"},
|
||||
rows=(
|
||||
{
|
||||
"trade_date": "20260828",
|
||||
"ts_code": "BK0001.DC",
|
||||
"con_code": "000001.SZ",
|
||||
"name": "平安银行",
|
||||
},
|
||||
),
|
||||
target_trade_date=TARGET_DATE,
|
||||
observed_at=STARTED_AT,
|
||||
)
|
||||
member = MembershipRecord(
|
||||
source_snapshot_id=snapshot.snapshot_id,
|
||||
trade_date=TARGET_DATE,
|
||||
sector_type=SectorType.CONCEPT,
|
||||
sector_code="BK0001.DC",
|
||||
sector_name="示例概念",
|
||||
stock_code="000001.SZ",
|
||||
stock_name="平安银行",
|
||||
)
|
||||
|
||||
assert repository.save_source_snapshots((snapshot,)).inserted == 1
|
||||
assert repository.save_source_snapshots((snapshot,)).unchanged == 1
|
||||
assert repository.save_memberships((member,)).inserted == 1
|
||||
assert repository.save_memberships((member,)).unchanged == 1
|
||||
|
||||
with pytest.raises(ValueError, match="cannot change content"):
|
||||
repository.save_memberships((replace(member, stock_name="已改变"),))
|
||||
|
||||
|
||||
def test_partial_and_failed_revisions_never_replace_last_good() -> None:
|
||||
repository = InMemorySectorRadarRepository()
|
||||
successful = make_running("success-a")
|
||||
partial = make_running("partial-b")
|
||||
failed = make_running("failed-c", TARGET_DATE + timedelta(days=1))
|
||||
repository.create_publication(successful)
|
||||
repository.finish_publication(finish(successful, PublicationStatus.SUCCESS))
|
||||
repository.create_publication(partial)
|
||||
repository.finish_publication(finish(partial, PublicationStatus.PARTIAL, offset_minutes=6))
|
||||
repository.create_publication(failed)
|
||||
repository.finish_publication(finish(failed, PublicationStatus.FAILED, offset_minutes=7))
|
||||
|
||||
last_good = repository.get_last_good_publication()
|
||||
|
||||
assert last_good is not None
|
||||
assert last_good.publication_id == "success-a"
|
||||
assert repository.list_successful_dates() == (TARGET_DATE,)
|
||||
|
||||
|
||||
def test_publication_identity_allows_sequential_same_date_revisions() -> None:
|
||||
repository = InMemorySectorRadarRepository()
|
||||
first = make_running("revision-a")
|
||||
second = make_running("revision-b")
|
||||
|
||||
assert repository.create_publication(first).inserted == 1
|
||||
with pytest.raises(ValueError, match="already has a running"):
|
||||
repository.create_publication(second)
|
||||
|
||||
with pytest.raises(ValueError, match="terminal"):
|
||||
repository.finish_publication(first)
|
||||
|
||||
repository.finish_publication(finish(first, PublicationStatus.FAILED))
|
||||
assert repository.create_publication(second).inserted == 1
|
||||
with pytest.raises(ValueError, match="running status"):
|
||||
repository.finish_publication(finish(first, PublicationStatus.SUCCESS))
|
||||
Reference in New Issue
Block a user