Add imap-ingest CLI + ImapProvider: route emails to IE/Schwab parsers
Wires the IE + Schwab email parsers into an actual runnable sync. Walks the IMAP mailbox, routes each message by sender domain: - *@investengine.com → invest_engine.parse_invest_engine_email - *@schwab.com → schwab.parse_schwab_email then pushes the resulting Activities through the shared pipeline. broker-sync imap-ingest — new CLI command taking IMAP_HOST/USER/PASSWORD/ DIRECTORY (mirrors the old wealthfolio-sync image's env shape so the Terraform CronJob's existing env wiring works unchanged). Verified: poetry run pytest -q → 109 passed + 1 skipped; mypy strict clean (37 files); ruff + yapf clean.
This commit is contained in:
parent
f089b8b93a
commit
6efd03570a
6 changed files with 290 additions and 35 deletions
|
|
@ -48,7 +48,10 @@ def _login_ok(req: httpx.Request) -> httpx.Response:
|
|||
assert body == {"password": "hunter2"}
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={"authenticated": True, "expiresIn": 604800},
|
||||
json={
|
||||
"authenticated": True,
|
||||
"expiresIn": 604800
|
||||
},
|
||||
headers={"set-cookie": "wf_token=abc123; Path=/api; HttpOnly"},
|
||||
)
|
||||
|
||||
|
|
@ -219,21 +222,25 @@ async def test_import_dry_run_then_real(tmp_path: Path) -> None:
|
|||
calls.append(req.url.path)
|
||||
if req.url.path == "/api/v1/activities/import/check":
|
||||
# /import/check hydrates and returns a list of ActivityImport.
|
||||
return httpx.Response(200, json=[
|
||||
{
|
||||
"symbol": "VUAG",
|
||||
"isValid": True,
|
||||
"errors": None,
|
||||
"assetId": "enriched-asset-uuid",
|
||||
"exchangeMic": "XLON",
|
||||
},
|
||||
])
|
||||
return httpx.Response(200,
|
||||
json=[
|
||||
{
|
||||
"symbol": "VUAG",
|
||||
"isValid": True,
|
||||
"errors": None,
|
||||
"assetId": "enriched-asset-uuid",
|
||||
"exchangeMic": "XLON",
|
||||
},
|
||||
])
|
||||
if req.url.path == "/api/v1/activities/import":
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"activities": [
|
||||
{"id": "wf-1", "external_id": "t212:1"},
|
||||
{
|
||||
"id": "wf-1",
|
||||
"external_id": "t212:1"
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -86,18 +86,22 @@ async def test_pipeline_skips_dedup_then_imports_new(tmp_path: Path) -> None:
|
|||
body = json.loads(req.content)
|
||||
# Echo each activity back marked valid (mimic Wealthfolio's
|
||||
# hydrate step).
|
||||
return httpx.Response(200, json=[
|
||||
{**a, "isValid": True, "errors": None} for a in body["activities"]
|
||||
])
|
||||
return httpx.Response(200,
|
||||
json=[{
|
||||
**a, "isValid": True,
|
||||
"errors": None
|
||||
} for a in body["activities"]])
|
||||
if req.url.path == "/api/v1/activities/import":
|
||||
body = req.content.decode()
|
||||
posted_batches.append(body)
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={"activities": [
|
||||
{"id": f"wf-{i}", "external_id": ext}
|
||||
for i, ext in enumerate(["a", "b", "c"])
|
||||
]},
|
||||
json={
|
||||
"activities": [{
|
||||
"id": f"wf-{i}",
|
||||
"external_id": ext
|
||||
} for i, ext in enumerate(["a", "b", "c"])]
|
||||
},
|
||||
)
|
||||
return httpx.Response(500)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue