122 lines
4.5 KiB
Python
122 lines
4.5 KiB
Python
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))
|