Source.sync(ctx) -> SyncResult пишет только raw_* и возвращает курсор; локи, журнал, ошибки и продвижение курсора берёт на себя worker/runner. ZenMoney читается единственным доступным способом — POST /v8/diff/ по serverTimestamp; токен живёт сутки, поэтому worker ротирует refresh_token через source_credential. Маппер всегда пересобирает core из полных raw_*, так что удаление в ZenMoney исчезает и у нас. ЦБ ходит мимо прокси (trust_env=False) и отдаёт cp1251 с делением на Nominal. Курсы только по рабочим дням — протяжку по календарю делает аналитика. Планировщик — APScheduler в отдельном процессе, на источник advisory-лок sync:<name>, чтобы ручной запуск не пересёкся с плановым.
63 lines
1.8 KiB
Python
63 lines
1.8 KiB
Python
"""Shared helpers for source tests: fixture loading and a SyncContext around a real session.
|
|
|
|
Exposed as fixtures rather than importable functions: `tests/sources` is a package, and a
|
|
module named `sources` on sys.path next to `fintracker.sources` reads like a trap.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
import respx
|
|
|
|
from fintracker.config import Settings
|
|
from fintracker.db import get_sessionmaker
|
|
from fintracker.sources.base import SyncContext, SyncResult
|
|
|
|
FIXTURES = Path(__file__).resolve().parent.parent / "fixtures"
|
|
|
|
|
|
@pytest.fixture
|
|
def fixture_json() -> Callable[..., dict[str, Any]]:
|
|
def _load(*parts: str) -> dict[str, Any]:
|
|
return json.loads(FIXTURES.joinpath(*parts).read_text(encoding="utf-8"))
|
|
|
|
return _load
|
|
|
|
|
|
@pytest.fixture
|
|
def fixture_bytes() -> Callable[..., bytes]:
|
|
def _load(*parts: str) -> bytes:
|
|
return FIXTURES.joinpath(*parts).read_bytes()
|
|
|
|
return _load
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_http():
|
|
"""respx router without `assert_all_called`: some tests deliberately prove that a
|
|
route (e.g. the token endpoint) was NOT hit."""
|
|
with respx.mock(assert_all_called=False) as router:
|
|
yield router
|
|
|
|
|
|
@pytest.fixture
|
|
def run_sync():
|
|
"""Run a source the way the worker does, but without the lock/run-log bookkeeping."""
|
|
|
|
async def _run(source: Any, *, settings: Settings, cursor: str | None = None) -> SyncResult:
|
|
async with get_sessionmaker()() as session:
|
|
ctx = SyncContext(
|
|
session=session,
|
|
settings=settings,
|
|
cursor_before=cursor,
|
|
triggered_by="test",
|
|
)
|
|
return await source.sync(ctx)
|
|
|
|
return _run
|