Ctrl K
ComunidadGuías…

utils/make_v1_fixtures.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Make a Wally 1.x database for every 1.x release, for the upgrade tests (tests/test_upgrade.py).

Each release's own published image (poliuscorp/wally:1.0 to 1.16) is started, the same household
is added through its own API, and its database is kept as tests/fixtures/v1.<n>.db. So each
fixture is exactly what that release wrote, with the library versions it shipped with.

    python utils/make_v1_fixtures.py [1.0 1.5 ...]      needs Docker and the network

The household has the awkward cases: tags, an income, a three-decimal amount, a name with
quotes and accents, a 29 February, a recurring rule that 1.x filled in up to its end date
(including charges after today), one that ended, a yearly one, a new category and tag, and,
from 1.10, another currency. The household password is set last.
"""
import json
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
FIXTURES = ROOT / "tests" / "fixtures"
RELEASES = ["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "1.8", "1.9", "1.10", "1.11", "1.12", "1.13", "1.14", "1.15", "1.16"]
PORT = 8960
PASSWORD = "household-pass-1"

TRANSACTIONS = [
    {"name": "Mercadona", "category": "Groceries", "tags": ["weekly"], "amount": 23.45, "type": "expense", "date": "2026-10-02"},
    {"name": "Café Nómada ☕", "category": "Food", "tags": [], "amount": 3.5, "type": "expense", "date": "2026-10-03"},
    {"name": "Salary", "category": "Income", "tags": [], "amount": 2450, "type": "income", "date": "2026-10-01"},
    {"name": "Three decimals", "category": "Shopping", "tags": [], "amount": "12.345", "type": "expense", "date": "2026-09-30"},
    {"name": "Kids clothes", "category": "Kids", "tags": ["holiday", "kids"], "amount": 40, "type": "expense", "date": "2026-08-20"},
    {"name": "L'Àvia \"Rosa\"", "category": "Food", "tags": ["holiday"], "amount": 61.2, "type": "expense", "date": "2024-02-29"},
    {"name": "Refund", "category": "Shopping", "tags": [], "amount": 9.99, "type": "income", "date": "2026-09-12"},
]
RULES = [
    # 1.x adds every charge up to the end date at once, so this one has charges after today
    {"name": "Netflix", "category": "Entertainment", "tags": [], "amount": 13.99, "type": "expense", "startDate": "2026-06-15", "endDate": "2027-03-15", "frequency": "monthly"},
    {"name": "Gym", "category": "Healthcare", "tags": ["sport"], "amount": 2, "type": "expense", "startDate": "2026-09-01", "endDate": "2026-09-05", "frequency": "daily"},
    {"name": "Insurance", "category": "Utilities", "tags": [], "amount": 384, "type": "expense", "startDate": "2025-01-31", "endDate": "2028-01-31", "frequency": "yearly"},
]

def call(method: str, path: str, body=None, ok=(200,)):
    req = urllib.request.Request(f"http://localhost:{PORT}/api{path}", method=method,
                                 data=None if body is None else json.dumps(body).encode(), headers={"content-type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=20) as r:
            return r.status, r.read().decode()
    except urllib.error.HTTPError as e:
        if e.code in ok:
            return e.code, e.read().decode()
        raise RuntimeError(f"{method} {path}: {e.code} {e.read().decode()[:300]}")

def build(release: str) -> Path:
    data = Path(tempfile.mkdtemp(prefix=f"wally-{release}-"))
    name = f"wally-fixture-{release.replace('.', '-')}"
    subprocess.run(["docker", "rm", "-f", name], capture_output=True)
    subprocess.run(["docker", "run", "-d", "--name", name, "-p", f"{PORT}:80", "-v", f"{data}:/wally/data", f"poliuscorp/wally:{release}"], check=True, capture_output=True)
    try:
        for _ in range(120):
            try:
                if call("GET", "/categories")[0] == 200:
                    break
            except Exception:
                time.sleep(0.5)
        else:
            raise RuntimeError(f"{release} did not start")
        call("POST", "/categories", {"name": "Kids"})
        call("POST", "/tags", {"name": "holiday"}, ok=(200, 400, 404, 405))
        for t in TRANSACTIONS:
            call("POST", "/transactions", t)
        for r in RULES:
            call("POST", "/recurring", r)
        call("PUT", "/currency/USD", ok=(200, 404, 405))
        call("PUT", "/currency/position/left", ok=(200, 404, 405, 422))
        call("PUT", "/language/es", ok=(200, 404, 405))
        call("POST", "/login/password", {"password": PASSWORD})
    finally:
        subprocess.run(["docker", "stop", name], capture_output=True)
        subprocess.run(["docker", "rm", "-f", name], capture_output=True)
    target = FIXTURES / f"v1.{release.split('.', 1)[1]}.db"
    shutil.copy(data / "wally.db", target)
    return target

if __name__ == "__main__":
    for release in sys.argv[1:] or RELEASES:
        print(release, "->", build(release).relative_to(ROOT))