diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb57a7c51..a681369ec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ ci: skip: ["eslint"] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.0 + rev: v0.8.2 hooks: - id: ruff args: ["--fix"] @@ -23,7 +23,7 @@ repos: files: \.(css|js|mjs|ts|svelte|yaml|json)$ require_serial: true additional_dependencies: - - "prettier@3.4.1" + - "prettier@3.4.2" - "prettier-plugin-svelte@3.3.2" - "svelte@4.2.19" - id: stylelint diff --git a/constraints.txt b/constraints.txt index 79ec1ae36..35ba52de6 100644 --- a/constraints.txt +++ b/constraints.txt @@ -4,7 +4,7 @@ alabaster==0.7.16 # via sphinx altgraph==0.17.4 # via pyinstaller -anyio==4.6.2.post1 +anyio==4.7.0 # via watchfiles astroid==3.3.5 # via pylint @@ -160,7 +160,7 @@ mypy==1.13.0 # via fava (pyproject.toml) mypy-extensions==1.0.0 # via mypy -nh3==0.2.18 +nh3==0.2.19 # via readme-renderer nodeenv==1.9.1 # via pre-commit @@ -176,7 +176,8 @@ packaging==24.2 # sphinx # tox # tox-uv -pkginfo==1.10.0 + # twine +pkginfo==1.12.0 # via twine platformdirs==4.3.6 # via @@ -216,13 +217,13 @@ pyinstaller==6.11.1 # via fava (pyproject.toml) pyinstaller-hooks-contrib==2024.10 # via pyinstaller -pylint==3.3.1 +pylint==3.3.2 # via fava (pyproject.toml) pyproject-api==1.8.0 # via tox pyproject-hooks==1.2.0 # via build -pytest==8.3.3 +pytest==8.3.4 # via # fava (pyproject.toml) # pytest-cov @@ -263,7 +264,7 @@ setuptools==75.6.0 # pyinstaller-hooks-contrib simplejson==3.19.3 # via fava (pyproject.toml) -six==1.16.0 +six==1.17.0 # via python-dateutil sniffio==1.3.1 # via anyio @@ -313,9 +314,9 @@ tox==4.23.2 # via # fava (pyproject.toml) # tox-uv -tox-uv==1.16.0 +tox-uv==1.16.1 # via fava (pyproject.toml) -twine==5.1.1 +twine==6.0.1 # via fava (pyproject.toml) types-setuptools==75.6.0.20241126 # via fava (pyproject.toml) @@ -334,7 +335,7 @@ urllib3==2.2.3 # via # requests # twine -uv==0.5.5 +uv==0.5.6 # via tox-uv virtualenv==20.28.0 # via diff --git a/frontend/css/base.css b/frontend/css/base.css index b2da4d8bc..cd16a83ad 100644 --- a/frontend/css/base.css +++ b/frontend/css/base.css @@ -446,19 +446,3 @@ td .status-indicator { border-bottom: 5px solid var(--text-color-lightest); border-left: 5px solid transparent; } - -/* - * View-specific and tables - */ - -.options td { - text-align: left; -} - -.options td:nth-child(1) { - font-weight: 500; -} - -.options td:nth-child(2) { - white-space: normal; -} diff --git a/frontend/css/journal-table.css b/frontend/css/journal-table.css index 06620439f..f58ae02a1 100644 --- a/frontend/css/journal-table.css +++ b/frontend/css/journal-table.css @@ -130,7 +130,7 @@ } .journal .datecell { - width: 5.5rem; + width: 6rem; white-space: nowrap; } @@ -231,3 +231,54 @@ text-transform: lowercase; border-radius: 20px; } + +@media (width <= 767px) { + .journal p { + flex-wrap: wrap; + } + + /* show a colored top border for all entries */ + .journal > li > p { + border-top: thin solid var(--entry-background); + } + + .journal .head .flag { + flex-grow: 1; + text-align: left; + } + + .journal .description { + flex: inherit; + order: 1; /* push to second row */ + width: 100%; + padding-left: 0; + } + + .journal .indicators { + flex-grow: 1; + } + + .journal .balance .num { + width: 35%; + } + + .journal .balance .change.num { + display: none; + } + + .journal .postings .datecell, + .journal .postings .flag { + display: none; + } + + .journal .postings .description { + flex-grow: 1; + order: inherit; + width: inherit; + } + + .journal .metadata dt, + .journal .metadata dd { + margin-left: initial; + } +} diff --git a/frontend/css/layout.css b/frontend/css/layout.css index 7e03a7bae..5cb21ccaa 100644 --- a/frontend/css/layout.css +++ b/frontend/css/layout.css @@ -64,6 +64,10 @@ article:has(> .fixed-fullsize-container) { } @media (width <= 767px) { + article { + padding: 1em; + } + body { display: block; font-size: 16px; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2d730be63..7acea9b40 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -157,9 +157,9 @@ } }, "node_modules/@codemirror/language": { - "version": "6.10.5", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.5.tgz", - "integrity": "sha512-sECWJyNmwqw6mSO6Qf0IVPHwhEnuYbqHBZaaIbdcXtZ6Y2r5vU/dxgC7K1ppWaJFy8XGtTBC0Pd60qI7NfJreQ==", + "version": "6.10.6", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.6.tgz", + "integrity": "sha512-KrsbdCnxEztLVbB5PycWXFxas4EOyk/fPAfruSOnDDppevQgid2XZ+KbJ9u+fDikP/e7MW7HPBTvTb8JlZK9vA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -738,13 +738,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", - "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", + "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.5", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -753,11 +753,14 @@ } }, "node_modules/@eslint/core": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", - "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", + "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -787,9 +790,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", - "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", + "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", "dev": true, "license": "MIT", "engines": { @@ -797,9 +800,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", + "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -807,9 +810,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", - "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", + "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1258,17 +1261,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", - "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.17.0.tgz", + "integrity": "sha512-HU1KAdW3Tt8zQkdvNoIijfWDMvdSweFYm4hWh+KwhPstv+sCmWb89hCIP8msFm9N1R/ooh9honpSuvqKWlYy3w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/type-utils": "8.16.0", - "@typescript-eslint/utils": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/type-utils": "8.17.0", + "@typescript-eslint/utils": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1292,16 +1295,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", - "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.17.0.tgz", + "integrity": "sha512-Drp39TXuUlD49F7ilHHCG7TTg8IkA+hxCuULdmzWYICxGXvDXmDmWEjJYZQYgf6l/TFfYNE167m7isnc3xlIEg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/typescript-estree": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/typescript-estree": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", "debug": "^4.3.4" }, "engines": { @@ -1321,14 +1324,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", - "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.17.0.tgz", + "integrity": "sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0" + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1339,14 +1342,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", - "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.17.0.tgz", + "integrity": "sha512-q38llWJYPd63rRnJ6wY/ZQqIzPrBCkPdpIsaCfkR3Q4t3p6sb422zougfad4TFW9+ElIFLVDzWGiGAfbb/v2qw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.16.0", - "@typescript-eslint/utils": "8.16.0", + "@typescript-eslint/typescript-estree": "8.17.0", + "@typescript-eslint/utils": "8.17.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1367,9 +1370,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", - "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.17.0.tgz", + "integrity": "sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==", "dev": true, "license": "MIT", "engines": { @@ -1381,14 +1384,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", - "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz", + "integrity": "sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1436,16 +1439,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", - "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.17.0.tgz", + "integrity": "sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/typescript-estree": "8.16.0" + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/typescript-estree": "8.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1464,13 +1467,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", - "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz", + "integrity": "sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/types": "8.17.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2274,9 +2277,9 @@ } }, "node_modules/eslint": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", - "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", + "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", "dev": true, "license": "MIT", "dependencies": { @@ -2285,7 +2288,7 @@ "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.9.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.15.0", + "@eslint/js": "9.16.0", "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2360,9 +2363,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "2.46.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.0.tgz", - "integrity": "sha512-1A7iEMkzmCZ9/Iz+EAfOGYL8IoIG6zeKEq1SmpxGeM5SXmoQq+ZNnCpXFVJpsxPWYx8jIVGMerQMzX20cqUl0g==", + "version": "2.46.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", + "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", "dev": true, "license": "MIT", "dependencies": { @@ -3699,9 +3702,9 @@ } }, "node_modules/prettier": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz", - "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "license": "MIT", "bin": { @@ -4324,9 +4327,9 @@ } }, "node_modules/svelte-check": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.0.tgz", - "integrity": "sha512-AflEZYqI578KuDZcpcorPSf597LStxlkN7XqXi38u09zlHODVKd7c+7OuubGzbhgGRUqNTdQCZ+Ga96iRXEf2g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.1.tgz", + "integrity": "sha512-NfaX+6Qtc8W/CyVGS/F7/XdiSSyXz+WGYA9ZWV3z8tso14V2vzjfXviKaTFEzB7g8TqfgO2FOzP6XT4ApSTUTw==", "dev": true, "license": "MIT", "dependencies": { @@ -4493,9 +4496,9 @@ "dev": true }, "node_modules/table": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz", - "integrity": "sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4588,9 +4591,9 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.2.tgz", - "integrity": "sha512-ZF5gQIQa/UmzfvxbHZI3JXN0/Jt+vnAfAviNRAMc491laiK6YCLpCW9ft8oaCRFOTxCZtUTE6XB0ZQAe3olntw==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "dev": true, "license": "MIT", "engines": { @@ -4628,15 +4631,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.16.0.tgz", - "integrity": "sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.17.0.tgz", + "integrity": "sha512-409VXvFd/f1br1DCbuKNFqQpXICoTB+V51afcwG1pn1a3Cp92MqAUges3YjwEdQ0cMUoCIodjVDAYzyD8h3SYA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.16.0", - "@typescript-eslint/parser": "8.16.0", - "@typescript-eslint/utils": "8.16.0" + "@typescript-eslint/eslint-plugin": "8.17.0", + "@typescript-eslint/parser": "8.17.0", + "@typescript-eslint/utils": "8.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index ff81cf653..f3bb35ebc 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -69,6 +69,7 @@ interface GetAPIParams { trial_balance: FiltersConversionInterval; ledger_data: undefined; move: { filename: string; account: string; new_name: string }; + options: undefined; payee_accounts: { payee: string }; payee_transaction: { payee: string }; query: Filters & { query_string: string }; diff --git a/frontend/src/api/validators.ts b/frontend/src/api/validators.ts index ba74ab348..77d44a8c2 100644 --- a/frontend/src/api/validators.ts +++ b/frontend/src/api/validators.ts @@ -191,6 +191,10 @@ export const getAPIValidators = { income_statement: tree_report, ledger_data: ledgerDataValidator, move: string, + options: object({ + fava_options: record(string), + beancount_options: record(string), + }), payee_accounts: array(string), payee_transaction: Transaction.validator, query: query_validator, diff --git a/frontend/src/journal/index.ts b/frontend/src/journal/index.ts index 6af8ebd15..7d75b613b 100644 --- a/frontend/src/journal/index.ts +++ b/frontend/src/journal/index.ts @@ -5,9 +5,9 @@ import { journalShow } from "../stores/journal"; import JournalFilters from "./JournalFilters.svelte"; /** - * Escape the value to produce a valid regex. + * Escape the value to produce a valid regex for the Fava filter. */ -function escape(value: string): string { +export function escape(value: string): string { return value.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); } diff --git a/frontend/src/lib/fetch.ts b/frontend/src/lib/fetch.ts index 85ebe1b44..45db924f5 100644 --- a/frontend/src/lib/fetch.ts +++ b/frontend/src/lib/fetch.ts @@ -22,33 +22,20 @@ export async function fetch( * Handles JSON content for a Promise returned by fetch, also handling an HTTP * error status. */ -async function handleJSON(response: Response): Promise { +async function handleJSON( + response: Response, +): Promise> { + const data: unknown = await response.json().catch(() => null); if (!response.ok) { - try { - const data: unknown = await response.json(); - if (isJsonObject(data)) { - throw new FetchError( - typeof data.error === "string" - ? data.error - : "Invalid response: missing error", - ); - } - throw new FetchError(response.statusText); - } catch { - throw new FetchError(response.statusText); - } - } - const data: unknown = await response.json(); - if (!isJsonObject(data)) { - throw new FetchError("Invalid response: not an object"); - } - if (data.success !== true) { throw new FetchError( - typeof data.error === "string" + isJsonObject(data) && typeof data.error === "string" ? data.error - : "Invalid response: missing error", + : response.statusText, ); } + if (!isJsonObject(data)) { + throw new FetchError("Invalid response: not a valid JSON object"); + } return data; } @@ -79,8 +66,8 @@ export async function fetchJSON( */ export async function handleText(response: Response): Promise { if (!response.ok) { - const msg = await response.text(); - throw new Error(msg || response.statusText); + const msg = await response.text().catch(() => response.statusText); + throw new FetchError(msg); } return response.text(); } diff --git a/frontend/src/reports/options/Options.svelte b/frontend/src/reports/options/Options.svelte new file mode 100644 index 000000000..2909da4ad --- /dev/null +++ b/frontend/src/reports/options/Options.svelte @@ -0,0 +1,17 @@ + + +

+ {_("Fava options")} + ({_("help")}) +

+ + +

{_("Beancount options")}

+ diff --git a/frontend/src/reports/options/OptionsTable.svelte b/frontend/src/reports/options/OptionsTable.svelte new file mode 100644 index 000000000..2d12353e0 --- /dev/null +++ b/frontend/src/reports/options/OptionsTable.svelte @@ -0,0 +1,41 @@ + + + + + + {#each columns as column} + + {/each} + + + + {#each sorted_options as [key, value] (key)} + + + + + {/each} + +
{key}
{value}
+ + diff --git a/frontend/src/reports/options/index.ts b/frontend/src/reports/options/index.ts new file mode 100644 index 000000000..a7367afc4 --- /dev/null +++ b/frontend/src/reports/options/index.ts @@ -0,0 +1,11 @@ +import { get } from "../../api"; +import { _ } from "../../i18n"; +import { Route } from "../route"; +import OptionsSvelte from "./Options.svelte"; + +export const options = new Route( + "options", + OptionsSvelte, + async () => get("options"), + () => _("Options"), +); diff --git a/frontend/src/reports/routes.ts b/frontend/src/reports/routes.ts index 3d10ef6ba..234d13324 100644 --- a/frontend/src/reports/routes.ts +++ b/frontend/src/reports/routes.ts @@ -7,6 +7,7 @@ import ErrorsSvelte from "./errors/Errors.svelte"; import { events } from "./events"; import { holdings } from "./holdings"; import { import_report } from "./import"; +import { options } from "./options"; import QuerySvelte from "./query/Query.svelte"; import { noload, Route } from "./route"; import { balance_sheet, income_statement, trial_balance } from "./tree_reports"; @@ -30,6 +31,7 @@ export const frontend_routes: Route[] = [ holdings, import_report, income_statement, + options, new Route("query", QuerySvelte, noload, () => _("Query")), trial_balance, ]; diff --git a/frontend/src/sidebar/FilterForm.svelte b/frontend/src/sidebar/FilterForm.svelte index f642c6a09..de9d39e4c 100644 --- a/frontend/src/sidebar/FilterForm.svelte +++ b/frontend/src/sidebar/FilterForm.svelte @@ -2,13 +2,14 @@ import AutocompleteInput from "../AutocompleteInput.svelte"; import ThemeSwitch from "../components/ThemeSwitch.svelte"; import { _ } from "../i18n"; + import { escape } from "../journal"; import { accounts, links, payees, tags, years } from "../stores"; import { account_filter, fql_filter, time_filter } from "../stores/filters"; $: fql_filter_suggestions = [ ...$tags.map((tag) => `#${tag}`), ...$links.map((link) => `^${link}`), - ...$payees.map((payee) => `payee:"${payee}"`), + ...$payees.map((payee) => `payee:"${escape(payee)}"`), ]; function valueExtractor(value: string, input: HTMLInputElement) { diff --git a/src/fava/application.py b/src/fava/application.py index 486ddd30c..3483399e4 100644 --- a/src/fava/application.py +++ b/src/fava/application.py @@ -14,7 +14,6 @@ import logging import mimetypes -from dataclasses import fields from datetime import date from datetime import datetime from datetime import timezone @@ -77,7 +76,6 @@ SERVER_SIDE_REPORTS = [ "journal", - "options", "statistics", ] @@ -91,6 +89,7 @@ "holdings", "import", "income_statement", + "options", "query", "trial_balance", ] @@ -238,7 +237,6 @@ def _setup_template_config(fava_app: Flask, *, incognito: bool) -> None: fava_app.add_template_filter(template_filters.flag_to_type) fava_app.add_template_filter(template_filters.format_currency) fava_app.add_template_filter(template_filters.meta_items) - fava_app.add_template_filter(fields, "dataclass_fields") fava_app.add_template_filter( template_filters.replace_numbers if incognito diff --git a/src/fava/beans/prices.py b/src/fava/beans/prices.py index 73f54026d..76a5562fd 100644 --- a/src/fava/beans/prices.py +++ b/src/fava/beans/prices.py @@ -6,12 +6,12 @@ from bisect import bisect from collections import Counter from collections import defaultdict -from collections.abc import Sequence from decimal import Decimal from typing import TYPE_CHECKING if TYPE_CHECKING: # pragma: no cover from collections.abc import Iterable + from collections.abc import Sequence from typing import TypeAlias from fava.beans.abc import Price @@ -23,7 +23,7 @@ ONE = Decimal(1) -class DateKeyWrapper(Sequence[datetime.date]): +class DateKeyWrapper(list[datetime.date]): """A class wrapping a list of prices for bisect. This is needed before Python 3.10, which adds the key argument. @@ -31,7 +31,7 @@ class DateKeyWrapper(Sequence[datetime.date]): __slots__ = ("inner",) - def __init__(self, inner: Sequence[PricePoint]) -> None: + def __init__(self, inner: list[PricePoint]) -> None: self.inner = inner def __len__(self) -> int: diff --git a/src/fava/core/filters.py b/src/fava/core/filters.py index d986e752b..3e14228de 100644 --- a/src/fava/core/filters.py +++ b/src/fava/core/filters.py @@ -444,8 +444,8 @@ def __init__(self, value: str) -> None: raise def apply(self, entries: Sequence[Directive]) -> Sequence[Directive]: - _include = self._include - return [entry for entry in entries if _include(entry)] + include = self._include + return [entry for entry in entries if include(entry)] class AccountFilter(EntryFilter): diff --git a/src/fava/json_api.py b/src/fava/json_api.py index 1f8a647df..c925b5beb 100644 --- a/src/fava/json_api.py +++ b/src/fava/json_api.py @@ -10,11 +10,13 @@ import shutil from abc import abstractmethod from dataclasses import dataclass +from dataclasses import fields from functools import wraps from http import HTTPStatus from inspect import Parameter from inspect import signature from pathlib import Path +from pprint import pformat from typing import Any from typing import Callable from typing import TYPE_CHECKING @@ -30,6 +32,7 @@ from fava.context import g from fava.core.documents import filepath_in_document_folder from fava.core.documents import is_document_or_import_file +from fava.core.filters import FilterError from fava.core.ingest import filepath_in_primary_imports_folder from fava.core.misc import align from fava.helpers import FavaAPIError @@ -81,7 +84,7 @@ def __init__(self, param: str, expected: type) -> None: def json_err(msg: str, status: HTTPStatus) -> Response: """Jsonify the error message.""" - res = jsonify({"success": False, "error": msg}) + res = jsonify({"error": msg}) res.status = status # type: ignore[assignment] return res @@ -89,7 +92,7 @@ def json_err(msg: str, status: HTTPStatus) -> Response: def json_success(data: Any) -> Response: """Jsonify the response.""" return jsonify( - {"success": True, "data": data, "mtime": str(g.ledger.mtime)}, + {"data": data, "mtime": str(g.ledger.mtime)}, ) @@ -157,24 +160,29 @@ def __init__(self, filename: str) -> None: @json_api.errorhandler(FavaAPIError) -def _json_api_fava_api_error(error: FavaAPIError) -> Response: +def _(error: FavaAPIError) -> Response: log.error("Encountered FavaAPIError.", exc_info=error) return json_err(error.message, HTTPStatus.INTERNAL_SERVER_ERROR) @json_api.errorhandler(FavaJSONAPIError) -def _json_api_fava_json_api(error: FavaJSONAPIError) -> Response: +def _(error: FavaJSONAPIError) -> Response: return json_err(error.message, error.status) +@json_api.errorhandler(FilterError) +def _(error: FilterError) -> Response: + return json_err(error.message, HTTPStatus.BAD_REQUEST) + + @json_api.errorhandler(OSError) -def _json_api_oserror(error: OSError) -> Response: # pragma: no cover +def _(error: OSError) -> Response: # pragma: no cover log.error("Encountered OSError.", exc_info=error) return json_err(error.strerror, HTTPStatus.INTERNAL_SERVER_ERROR) @json_api.errorhandler(ValidationError) -def _json_api_validation_error(error: ValidationError) -> Response: +def _(error: ValidationError) -> Response: return json_err(f"Invalid API request: {error!s}", HTTPStatus.BAD_REQUEST) @@ -498,6 +506,32 @@ def get_documents() -> Sequence[Document]: ] +@dataclass(frozen=True) +class Options: + """Fava and Beancount options as strings.""" + + fava_options: Mapping[str, str] + beancount_options: Mapping[str, str] + + +@api_endpoint +def get_options() -> Options: + """Get all options, rendered to strings for displaying in the frontend.""" + g.ledger.changed() + + fava_options = g.ledger.fava_options + pprinted_fava_options = { + field.name.replace("_", "-"): pformat( + getattr(fava_options, field.name) + ) + for field in fields(fava_options) + } + return Options( + pprinted_fava_options, + {key: str(value) for key, value in g.ledger.options.items()}, + ) + + @dataclass(frozen=True) class CommodityPairWithPrices: """A pair of commodities and prices for them.""" diff --git a/src/fava/templates/options.html b/src/fava/templates/options.html deleted file mode 100644 index db77fcc02..000000000 --- a/src/fava/templates/options.html +++ /dev/null @@ -1,40 +0,0 @@ -{% extends "_layout.html" %} - -{% set page_title = _('Options') %} - -{% block content %} -

{{ _('Fava options') }} ({{ _('help') }})

- - - - - - - - - {% for field in ledger.fava_options|dataclass_fields %} - - - - - {% endfor %} - -
{{ _('Key') }}{{ _('Value') }}
{{ field.name|replace('_', '-') }}
{{ ledger.fava_options|attr(field.name)|pprint }}
-

{{ _('Beancount options') }}

- - - - - - - - - {% for key, value in ledger.options|dictsort %} - - - - - {% endfor %} - -
{{ _('Key') }}{{ _('Value') }}
{{ key }}
{{ value }}
-{% endblock %} diff --git a/tests/__snapshots__/test_json_api-test_api_context-2.json b/tests/__snapshots__/test_json_api-test_api_context-2.json index cb92e0e44..677f41256 100644 --- a/tests/__snapshots__/test_json_api-test_api_context-2.json +++ b/tests/__snapshots__/test_json_api-test_api_context-2.json @@ -12,6 +12,6 @@ }, "t": "Open" }, - "sha256sum": "ENTRY_HASH260d384e80c9af182390e419d4a0bbc8", + "sha256sum": "7198a732bba60e03bfebbf4368637a66260d384e80c9af182390e419d4a0bbc8", "slice": "1980-05-12 open Expenses:Food:Alcohol" } \ No newline at end of file diff --git a/tests/__snapshots__/test_json_api-test_api_context.json b/tests/__snapshots__/test_json_api-test_api_context.json index 3adf80cd5..1f21beb50 100644 --- a/tests/__snapshots__/test_json_api-test_api_context.json +++ b/tests/__snapshots__/test_json_api-test_api_context.json @@ -121,6 +121,6 @@ "t": "Transaction", "tags": [] }, - "sha256sum": "ENTRY_HASH045ed99d2af148ec815727c3b136906e", + "sha256sum": "b8d184b8ffadddbdc915a9dbf7e4d94a045ed99d2af148ec815727c3b136906e", "slice": "2016-05-09 * \"Investing 40% of cash in VBMPX\"\n Assets:US:Vanguard:VBMPX 15.957 VBMPX {30.08 USD}\n Assets:US:Vanguard:Cash -479.99 USD" } \ No newline at end of file diff --git a/tests/__snapshots__/test_json_api-test_api-documents.json b/tests/__snapshots__/test_json_api-test_api_unix_only-documents.json similarity index 84% rename from tests/__snapshots__/test_json_api-test_api-documents.json rename to tests/__snapshots__/test_json_api-test_api_unix_only-documents.json index 61490aa77..c43ab9fad 100644 --- a/tests/__snapshots__/test_json_api-test_api-documents.json +++ b/tests/__snapshots__/test_json_api-test_api_unix_only-documents.json @@ -2,7 +2,7 @@ { "account": "Expenses:Food", "date": "2012-12-15", - "filename": "TEST_DATA_DIR/import.csv", + "filename": "/tmp/import.csv", "links": [ "link1" ], @@ -18,7 +18,7 @@ { "account": "Expenses:Food", "date": "2012-12-15", - "filename": "TEST_DATA_DIR/receipt.pdf", + "filename": "/tmp/receipt.pdf", "links": [], "meta": { "filename": "TEST_DATA_DIR/example.beancount", diff --git a/tests/__snapshots__/test_json_api-test_api_unix_only-options.json b/tests/__snapshots__/test_json_api-test_api_unix_only-options.json new file mode 100644 index 000000000..821a7ed4d --- /dev/null +++ b/tests/__snapshots__/test_json_api-test_api_unix_only-options.json @@ -0,0 +1,62 @@ +{ + "beancount_options": { + "account_current_conversions": "Conversions:Current", + "account_current_earnings": "Earnings:Current", + "account_previous_balances": "Opening-Balances", + "account_previous_conversions": "Conversions:Previous", + "account_previous_earnings": "Earnings:Previous", + "account_rounding": "None", + "account_unrealized_gains": "Earnings:Unrealized", + "allow_deprecated_none_for_tags_and_links": "False", + "allow_pipe_separator": "False", + "booking_method": "Booking.STRICT", + "commodities": "set()", + "conversion_currency": "NOTHING", + "dcontext": "ABC : sign=0 integer_max=1 fractional_common=0 fractional_max=0 \"0\" \"0\"\nGLD : sign=1 integer_max=2 fractional_common=0 fractional_max=0 \"-00\" \"-00\"\nIRAUSD : sign=1 integer_max=5 fractional_common=2 fractional_max=2 \"-00000.00\" \"-00000.00\"\nITOT : sign=1 integer_max=2 fractional_common=0 fractional_max=0 \"-00\" \"-00\"\nRGAGX : sign=0 integer_max=1 fractional_common=3 fractional_max=3 \"0.000\" \"0.000\"\nUSD : sign=1 integer_max=4 fractional_common=2 fractional_max=2 \"-0000.00\" \"-0000.00\"\nVACHR : sign=1 integer_max=3 fractional_common=0 fractional_max=0 \"-000\" \"-000\"\nVBMPX : sign=0 integer_max=2 fractional_common=3 fractional_max=3 \"00.000\" \"00.000\"\nVEA : sign=1 integer_max=2 fractional_common=0 fractional_max=0 \"-00\" \"-00\"\nVHT : sign=1 integer_max=2 fractional_common=0 fractional_max=0 \"-00\" \"-00\"\nXYZ : sign=0 integer_max=1 fractional_common=0 fractional_max=0 \"0\" \"0\"\n__default__ : sign=0 integer_max=1 fractional_common=_ fractional_max=_ \"0.*\" \"0.*\"\n", + "documents": "[]", + "filename": "TEST_DATA_DIR/long-example.beancount", + "include": "['TEST_DATA_DIR/long-example.beancount']", + "infer_tolerance_from_cost": "False", + "inferred_tolerance_default": "{}", + "inferred_tolerance_multiplier": "0.5", + "input_hash": "ENTRY_HASH", + "insert_pythonpath": "False", + "long_string_maxlines": "64", + "name_assets": "Assets", + "name_equity": "Equity", + "name_expenses": "Expenses", + "name_income": "Income", + "name_liabilities": "Liabilities", + "operating_currency": "['USD']", + "plugin": "[]", + "plugin_processing_mode": "default", + "pythonpath": "[]", + "render_commas": "False", + "title": "Long Example" + }, + "fava_options": { + "account-journal-include-children": "True", + "auto-reload": "False", + "collapse-pattern": "[]", + "conversion-currencies": "()", + "currency-column": "61", + "default-file": "None", + "default-page": "'income_statement/'", + "fiscal-year-end": "FiscalYearEnd(month=12, day=31)", + "import-config": "None", + "import-dirs": "()", + "indent": "2", + "insert-entry": "[]", + "invert-income-liabilities-equity": "False", + "language": "None", + "locale": "None", + "show-accounts-with-zero-balance": "False", + "show-accounts-with-zero-transactions": "True", + "show-closed-accounts": "False", + "sidebar-show-queries": "5", + "unrealized": "'Unrealized'", + "upcoming-events": "7", + "uptodate-indicator-grey-lookback-days": "60", + "use-external-editor": "False" + } +} \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 0945e1263..dd4086cf9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -121,8 +121,11 @@ def snapshot_data( today = local_today() out = out.replace(str(today), "TODAY") # replace entry hashes - out = re.sub(r'"[0-9a-f]{32}', '"ENTRY_HASH', out) - out = re.sub(r"context-[0-9a-f]{32}", "context-ENTRY_HASH", out) + out = re.sub(r'_hash": "[0-9a-f]+', '_hash": "ENTRY_HASH', out) + out = re.sub(r"context-[0-9a-f]+", "context-ENTRY_HASH", out) + out = re.sub( + r"data-entry=\\\"[0-9a-f]+", 'data-entry=\\"ENTRY_HASH', out + ) # replace mtimes out = re.sub(r"mtime=\d+", "mtime=MTIME", out) out = re.sub(r'id="ledger-mtime">\d+', 'id="ledger-mtime">MTIME', out) diff --git a/tests/data/example.beancount b/tests/data/example.beancount index 26c145d77..f8bd9762c 100644 --- a/tests/data/example.beancount +++ b/tests/data/example.beancount @@ -75,7 +75,7 @@ plugin "beancount.plugins.auto_accounts" test: TRUE account: Expenses:Food -2012-12-15 document Expenses:Food "import.csv" #tag ^link1 -2012-12-15 document Expenses:Food "receipt.pdf" #discovered +2012-12-15 document Expenses:Food "/tmp/import.csv" #tag ^link1 +2012-12-15 document Expenses:Food "/tmp/receipt.pdf" #discovered 2014-01-01 close Assets:Account1 diff --git a/tests/data/off-by-one.beancount b/tests/data/off-by-one.beancount index 1703e639f..2eb904397 100644 --- a/tests/data/off-by-one.beancount +++ b/tests/data/off-by-one.beancount @@ -11,7 +11,7 @@ option "operating_currency" "USD" 2022-01-01 * "Buy" Assets:Cash -100 USD - Assets:Commodity 1 COM {{100 USD}} + Assets:Commodity 1 COM {100 USD} 2022-01-01 price COM 101 USD 2022-01-02 price COM 102 USD diff --git a/tests/test_core_charts.py b/tests/test_core_charts.py index c148f3d08..a6ae0de55 100644 --- a/tests/test_core_charts.py +++ b/tests/test_core_charts.py @@ -75,6 +75,8 @@ def test_net_worth_off_by_one( ) -> None: off_by_one = get_ledger("off-by-one") off_by_one_filtered = off_by_one.get_filtered() + assert not off_by_one.errors + assert len(off_by_one_filtered.entries) == 9 for interval in [Interval.DAY, Interval.MONTH]: data = off_by_one.charts.net_worth( @@ -82,6 +84,7 @@ def test_net_worth_off_by_one( interval, "at_value", ) + assert len(data) == 4 if interval == Interval.DAY else 1 snapshot(data, json=True) diff --git a/tests/test_json_api.py b/tests/test_json_api.py index 4a70afcde..766713a83 100644 --- a/tests/test_json_api.py +++ b/tests/test_json_api.py @@ -1,6 +1,7 @@ from __future__ import annotations import datetime +import sys from http import HTTPStatus from io import BytesIO from pathlib import Path @@ -52,7 +53,6 @@ def assert_api_error( """Asserts that the response errored and contains the message.""" assert response.status_code == status.value assert response.json - assert not response.json["success"], response.json err_msg = response.json["error"] assert isinstance(err_msg, str) if msg: @@ -64,7 +64,6 @@ def assert_api_success(response: TestResponse, data: Any | None = None) -> Any: """Asserts that the request was successful and contains the data.""" assert response.status_code == HTTPStatus.OK.value assert response.json - assert response.json["success"], response.json if data is not None: assert data == response.json["data"] return response.json["data"] @@ -654,11 +653,39 @@ def test_api_commodities_empty( assert not data +def test_api_filter_error( + test_client: FlaskClient, +) -> None: + response = test_client.get( + "/long-example/api/commodities?time=20", + ) + assert_api_error(response, status=HTTPStatus.BAD_REQUEST) + + +@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") @pytest.mark.parametrize( ("name", "url"), [ - ("commodities", "/long-example/api/commodities"), ("documents", "/example/api/documents"), + ("options", "/long-example/api/options"), + ], +) +def test_api_unix_only( + test_client: FlaskClient, + snapshot: SnapshotFunc, + name: str, + url: str, +) -> None: + response = test_client.get(url) + data = assert_api_success(response) + assert data + snapshot(data, name=name, json=True) + + +@pytest.mark.parametrize( + ("name", "url"), + [ + ("commodities", "/long-example/api/commodities"), ("events", "/long-example/api/events"), ("income_statement", "/long-example/api/income_statement?time=2014"), ("trial_balance", "/long-example/api/trial_balance?time=2014"),