Build Dockerized MVP scaffold and records importer

This commit is contained in:
ik
2026-09-02 20:29:02 +07:00
parent a6f91a1329
commit d3a45248ef
39 changed files with 6638 additions and 13 deletions
+68
View File
@@ -0,0 +1,68 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
from app.database import Base, get_session
from app.main import app
from app.models import Bait, BaitKind, CatchReport, Fish, ModerationStatus, SourceType, Spot, Waterbody
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
Base.metadata.create_all(engine)
def override_session():
with Session(engine) as session:
yield session
app.dependency_overrides[get_session] = override_session
client = TestClient(app)
def setup_module() -> None:
with Session(engine) as db:
waterbody = Waterbody(slug="test-lake", name_ru="Тестовое озеро", unlock_level=1)
fish = Fish(slug="pike", name_ru="Щука", trophy_weight_g=10_000)
bait = Bait(name="Тестовая приманка", normalized_name="тестовая приманка", kind=BaitKind.lure)
spot = Spot(waterbody=waterbody, x=10, y=20, description="Тестовая точка")
db.add_all([waterbody, fish, bait, spot])
now = datetime.now(timezone.utc)
for index in range(3):
db.add(CatchReport(fish=fish, spot=spot, waterbody=waterbody, bait=bait, weight_g=3000 + index * 1000, fishing_method="spinning", reported_at=now - timedelta(hours=index), caught_at=now - timedelta(hours=index), player_name=f"Player {index}", source_type=SourceType.manual_import, source_confidence=90, moderation_status=ModerationStatus.approved))
db.commit()
def test_activity_filters_and_explains_score() -> None:
response = client.get("/api/v1/activity?waterbody=test-lake&fish=pike&hours=24")
assert response.status_code == 200
payload = response.json()
assert len(payload) == 1
assert payload[0]["catches"] == 3
assert payload[0]["unique_players"] == 3
assert "3 свежих уловов" in payload[0]["explanation"]
def test_invalid_period_is_rejected() -> None:
assert client.get("/api/v1/activity?hours=13").status_code == 422
def test_spot_detail_and_catches() -> None:
spot_id = client.get("/api/v1/activity").json()[0]["spot_id"]
detail = client.get(f"/api/v1/spots/{spot_id}")
catches = client.get(f"/api/v1/spots/{spot_id}/catches")
assert detail.status_code == 200
assert detail.json()["catches_24h"] == 3
assert catches.status_code == 200
assert len(catches.json()) == 3
def test_records_list_is_empty_before_import() -> None:
response = client.get("/api/v1/records")
assert response.status_code == 200
assert response.json() == []
+30
View File
@@ -0,0 +1,30 @@
from __future__ import annotations
from pathlib import Path
from sqlalchemy import create_engine, func, select
from sqlalchemy.orm import Session
from app.database import Base
from app.importer import import_records, parse_html
from app.models import CatchReport, OfficialRecordImport, SourceType
FIXTURE = Path(__file__).parents[3] / "tests" / "fixtures" / "records_ru_sample.html"
def test_parser_and_import_are_idempotent() -> None:
html = FIXTURE.read_text(encoding="utf-8")
parsed = parse_html(html, region="RU", category="records")
assert len(parsed) == 2
assert parsed[1].weight_g == 2_519_264
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as db:
first = import_records(db, url="fixture://records", region="RU", category="records", html=html)
second = import_records(db, url="fixture://records", region="RU", category="records", html=html)
assert (first.rows_created, first.rows_updated) == (2, 0)
assert (second.rows_created, second.rows_updated) == (0, 2)
assert db.scalar(select(func.count()).select_from(CatchReport).where(CatchReport.source_type == SourceType.official_record)) == 2
assert db.scalar(select(func.count()).select_from(OfficialRecordImport)) == 2