feat(sources): контракт источников, worker и синк ZenMoney + ЦБ
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>, чтобы ручной запуск не пересёкся с плановым.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user