feat: enforce community source cooldown
CI / backend-and-migrations (push) Canceled after 0s
CI / astro-build (push) Canceled after 0s
CI / compose-e2e (push) Canceled after 0s

This commit is contained in:
ik
2026-09-05 07:52:37 +07:00
parent 1a09bd59f3
commit 3f468799bd
10 changed files with 114 additions and 8 deletions
+4
View File
@@ -15,11 +15,15 @@ SOURCE_DEFAULTS = {
"rf4db": ("RF4DB", "https://rf4db.com", 70),
"rf4stat-fishing": ("RF4-STAT fishing", "https://rf4-stat.ru/fishing/", 65),
"rf4stat-post": ("RF4-STAT posts", "https://rf4-stat.ru/posts/", 60),
"rf4map": ("RF4MAP", "https://rf4map.ru", 55),
"rf4posts-spot": ("RF4 Posts spots", "https://rf4-posts.com", 50),
}
SOURCE_HOSTS = {
"rf4db": {"rf4db.com", "download.rf4db.com"},
"rf4stat-fishing": {"rf4-stat.ru"},
"rf4stat-post": {"rf4-stat.ru"},
"rf4map": {"rf4map.ru"},
"rf4posts-spot": {"rf4-posts.com"},
}
+37 -2
View File
@@ -10,17 +10,24 @@ from sqlalchemy.orm import Session
from app.community_importer import CommunityImportError, stage_observations
from app.database import Base
from app.models import DataSource, ExternalObservation
from rf4_research.community_sources import parse_rf4db_catches
from rf4_research.community_sources import parse_rf4db_catches, parse_rf4map_point, parse_rf4posts_spot
FIXTURE = Path(__file__).parents[3] / "tests" / "fixtures" / "rf4db_catches_sample.html"
FIXTURES = FIXTURE.parent
def record(source: str = "rf4db", external_id: str = "catch-1") -> dict[str, object]:
urls = {
"rf4db": f"https://rf4db.com/ru/catches/{external_id}",
"rf4stat-fishing": f"https://rf4-stat.ru/fishing/{external_id}",
"rf4map": f"https://rf4map.ru/points/{external_id}",
"rf4posts-spot": f"https://rf4-posts.com/ru/spots/{external_id}",
}
return {
"source_system": source,
"source_external_id": external_id,
"source_url": f"https://rf4db.com/ru/catches/{external_id}" if source == "rf4db" else f"https://rf4-stat.ru/fishing/{external_id}",
"source_url": urls.get(source, f"https://rf4-stat.ru/fishing/{external_id}"),
"fish": "Щука",
"fish_external_id": "pike",
"waterbody": "Тестовое озеро",
@@ -66,6 +73,14 @@ def test_external_ids_are_isolated_by_source(db: Session) -> None:
assert (created, updated) == (2, 0)
def test_research_sources_can_enter_disabled_staging(db: Session) -> None:
created, updated = stage_observations(db, [record("rf4map"), record("rf4posts-spot")])
assert (created, updated) == (2, 0)
assert db.get(DataSource, "rf4map").enabled is False
assert db.get(DataSource, "rf4posts-spot").enabled is False
def test_parser_json_can_be_staged_without_losing_provenance(db: Session) -> None:
parsed = parse_rf4db_catches(FIXTURE.read_text(encoding="utf-8"))
payload = json.loads(json.dumps([asdict(item) for item in parsed], default=str))
@@ -76,6 +91,26 @@ def test_parser_json_can_be_staged_without_losing_provenance(db: Session) -> Non
assert (item.source_system, item.fish_external_id, item.x, item.y) == ("rf4db", "pike", 71, 92)
@pytest.mark.parametrize(("fixture_name", "source_url", "parser"), [
("rf4map_point_sample.html", "https://rf4map.ru/points/275", parse_rf4map_point),
(
"rf4posts_spot_sample.html",
"https://rf4-posts.com/ru/spots/d0c6d9c6-4ebf-49a7-98a8-9a562553a8ee",
parse_rf4posts_spot,
),
])
def test_new_parser_json_can_be_staged(
db: Session, fixture_name: str, source_url: str, parser,
) -> None:
parsed = parser((FIXTURES / fixture_name).read_text(encoding="utf-8"), source_url=source_url)
payload = json.loads(json.dumps([asdict(item) for item in parsed], default=str))
created, updated = stage_observations(db, payload)
assert (created, updated) == (len(parsed), 0)
assert db.scalar(select(func.count()).select_from(ExternalObservation)) == len(parsed)
def test_invalid_source_rolls_back_caller_transaction(db: Session) -> None:
with pytest.raises(CommunityImportError, match="unsupported source_system"):
stage_observations(db, [record("unknown")])