Files
rf4-spotter/tests/test_community_sources.py
T
ik 4c75db1f74
CI / backend-and-migrations (push) Canceled after 0s
CI / astro-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s
CI / compose-e2e (push) Canceled after 0s
feat: parse RF4DB waterbody catalog
2026-09-15 17:19:15 +07:00

147 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from datetime import datetime, time, timezone
from pathlib import Path
import pytest
from rf4_research.community_sources import (
CommunityParseError,
parse_rf4db_catches,
parse_rf4db_detail,
parse_rf4db_waterbodies,
parse_rf4map_point,
parse_rf4posts_spot,
parse_rf4stat_fishing,
parse_rf4stat_posts,
)
FIXTURES = Path(__file__).parent / "fixtures"
def fixture(name: str) -> str:
return (FIXTURES / name).read_text(encoding="utf-8")
def test_parses_rf4db_public_catch_cards() -> None:
row = parse_rf4db_catches(fixture("rf4db_catches_sample.html"))[0]
assert (row.source_system, row.source_external_id) == ("rf4db", "11111111-1111-4111-8111-111111111111")
assert (row.fish, row.fish_external_id) == ("Щука обыкновенная", "pike")
assert (row.waterbody, row.waterbody_external_id) == ("Тестовое озеро", "test-lake")
assert (row.x, row.y, row.weight_g) == (71, 92, None)
assert row.game_time == time(6, 14)
assert row.water_temperature_c == 12.5
def test_parses_rf4db_detail_equipment_and_cast_facts() -> None:
detail = parse_rf4db_detail(
fixture("rf4db_detail_sample.html"),
source_url="https://rf4db.com/ru/catches/11111111-1111-4111-8111-111111111111",
)
assert detail.source_external_id == "11111111-1111-4111-8111-111111111111"
assert (detail.wind, detail.line_release_m, detail.clip_m, detail.cast_direction_deg) == ("SE 4 м/с", 5.1, 5.0, 181.1)
assert [(item.kind, item.name, item.external_id) for item in detail.equipment] == [
("Приманка поклёвки", "Тестовая приманка", "test-lure"),
("Катушка", "Тестовая катушка", "test-reel"),
]
def test_rf4db_detail_rejects_unrelated_html() -> None:
with pytest.raises(CommunityParseError, match="detail not found"):
parse_rf4db_detail("<html></html>", source_url="https://rf4db.com/ru/catches/id")
def test_parses_rf4db_waterbody_catalog_without_inventing_image_roles() -> None:
rows = parse_rf4db_waterbodies(fixture("rf4db_waterbodies_sample.html"), expected_count=2)
assert len(rows) == 2
assert (rows[0].source_external_id, rows[0].name) == ("level_001_mosquito", "оз. Комариное")
assert (rows[0].unlock_level, rows[0].unlock_label, rows[0].fish_species_count) == (1, "1", 20)
assert rows[0].source_url == "https://rf4db.com/ru/maps/level_001_mosquito"
assert rows[0].image_url == "https://oss.rf4db.com/game/maps/level_001_mosquito.webp"
assert (rows[1].unlock_level, rows[1].unlock_label) == (None, "Старт")
assert rows[1].image_url == "https://rf4db.com/game/maps/level_000_cottage.webp"
def test_rf4db_waterbody_catalog_rejects_unrelated_html() -> None:
with pytest.raises(CommunityParseError, match="waterbody cards not found"):
parse_rf4db_waterbodies("<html></html>")
def test_rf4db_waterbody_catalog_rejects_partial_results() -> None:
with pytest.raises(CommunityParseError, match="expected 19, got 2"):
parse_rf4db_waterbodies(fixture("rf4db_waterbodies_sample.html"))
def test_parses_rf4stat_fishing_rows() -> None:
row = parse_rf4stat_fishing(
fixture("rf4stat_fishing_sample.html"),
now=datetime(2026, 9, 3, 12, tzinfo=timezone.utc),
)[0]
assert (row.source_system, row.source_external_id) == ("rf4stat-fishing", "123")
assert (row.x, row.y, row.weight_g) == (71, 92, 11_584)
assert row.published_at == datetime(2026, 9, 3, 11, 27, tzinfo=timezone.utc)
assert (row.player_name, row.clip, row.fishing_style) == ("Игрок", "35", "Донная")
def test_parses_rf4stat_posts_without_using_locked_coordinates() -> None:
row = parse_rf4stat_posts(fixture("rf4stat_posts_sample.html"))[0]
assert (row.source_system, row.source_external_id) == ("rf4stat-post", "456:0")
assert (row.x, row.y, row.weight_g) == (None, None, 4_321)
assert row.evidence_urls == ("https://img.example.test/proof.jpg",)
def test_parses_rf4map_individual_observations_into_common_contract() -> None:
rows = parse_rf4map_point(
fixture("rf4map_point_sample.html"), source_url="https://rf4map.ru/points/275",
)
assert len(rows) == 2
assert (rows[0].source_system, rows[0].source_external_id) == ("rf4map", "37100")
assert (rows[0].fish, rows[0].fish_external_id) == ("Линь", "98")
assert (rows[0].waterbody, rows[0].waterbody_external_id) == ("оз. Комариное", "16")
assert (rows[0].x, rows[0].y, rows[0].weight_g, rows[0].clip) == (53, 87, None, "8")
assert rows[1].bait == "Тесто медовое"
assert rows[1].evidence_urls == ("https://img.example.test/proof.jpg",)
def test_parses_rf4posts_aggregate_spot_without_inventing_weights() -> None:
rows = parse_rf4posts_spot(
fixture("rf4posts_spot_sample.html"),
source_url="https://rf4-posts.com/ru/spots/d0c6d9c6-4ebf-49a7-98a8-9a562553a8ee",
)
assert [row.fish for row in rows] == ["Карп чешуйчатый", "Линь"]
assert rows[0].source_external_id.endswith(":carp")
assert (rows[0].x, rows[0].y, rows[0].weight_g) == (132, 147, None)
assert (rows[0].rig_type, rows[0].fishing_style, rows[0].clip) == ("method_popup", "feeder", "8")
assert rows[0].evidence_urls == ("https://img.example.test/spot.png",)
def test_new_sources_keep_source_namespaces_for_compatible_records() -> None:
rf4map = parse_rf4map_point(
fixture("rf4map_point_sample.html"), source_url="https://rf4map.ru/points/275",
)[0]
rf4posts = parse_rf4posts_spot(
fixture("rf4posts_spot_sample.html"), source_url="https://rf4-posts.com/ru/spots/example",
)[1]
assert rf4map.fish == rf4posts.fish == "Линь"
assert rf4map.source_system != rf4posts.source_system
assert rf4map.source_external_id != rf4posts.source_external_id
@pytest.mark.parametrize("parser", [parse_rf4db_catches, parse_rf4stat_fishing, parse_rf4stat_posts])
def test_parsers_reject_unrelated_html(parser) -> None:
with pytest.raises(CommunityParseError):
parser("<html><body>not a source page</body></html>")
@pytest.mark.parametrize("parser", [parse_rf4map_point, parse_rf4posts_spot])
def test_detail_parsers_reject_unrelated_html(parser) -> None:
with pytest.raises(CommunityParseError):
parser("<html><body>not a source page</body></html>", source_url="https://example.test/item/1")