rustfava 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rustfava/__init__.py +30 -0
- rustfava/_ctx_globals_class.py +55 -0
- rustfava/api_models.py +36 -0
- rustfava/application.py +534 -0
- rustfava/beans/__init__.py +6 -0
- rustfava/beans/abc.py +327 -0
- rustfava/beans/account.py +79 -0
- rustfava/beans/create.py +377 -0
- rustfava/beans/flags.py +20 -0
- rustfava/beans/funcs.py +38 -0
- rustfava/beans/helpers.py +52 -0
- rustfava/beans/ingest.py +75 -0
- rustfava/beans/load.py +31 -0
- rustfava/beans/prices.py +151 -0
- rustfava/beans/protocols.py +82 -0
- rustfava/beans/str.py +454 -0
- rustfava/beans/types.py +63 -0
- rustfava/cli.py +187 -0
- rustfava/context.py +13 -0
- rustfava/core/__init__.py +729 -0
- rustfava/core/accounts.py +161 -0
- rustfava/core/attributes.py +145 -0
- rustfava/core/budgets.py +207 -0
- rustfava/core/charts.py +301 -0
- rustfava/core/commodities.py +37 -0
- rustfava/core/conversion.py +229 -0
- rustfava/core/documents.py +87 -0
- rustfava/core/extensions.py +132 -0
- rustfava/core/fava_options.py +255 -0
- rustfava/core/file.py +542 -0
- rustfava/core/filters.py +484 -0
- rustfava/core/group_entries.py +97 -0
- rustfava/core/ingest.py +509 -0
- rustfava/core/inventory.py +167 -0
- rustfava/core/misc.py +105 -0
- rustfava/core/module_base.py +18 -0
- rustfava/core/number.py +106 -0
- rustfava/core/query.py +180 -0
- rustfava/core/query_shell.py +301 -0
- rustfava/core/tree.py +265 -0
- rustfava/core/watcher.py +219 -0
- rustfava/ext/__init__.py +232 -0
- rustfava/ext/auto_commit.py +61 -0
- rustfava/ext/portfolio_list/PortfolioList.js +34 -0
- rustfava/ext/portfolio_list/__init__.py +29 -0
- rustfava/ext/portfolio_list/templates/PortfolioList.html +15 -0
- rustfava/ext/rustfava_ext_test/RustfavaExtTest.js +42 -0
- rustfava/ext/rustfava_ext_test/__init__.py +207 -0
- rustfava/ext/rustfava_ext_test/templates/RustfavaExtTest.html +45 -0
- rustfava/ext/rustfava_ext_test/templates/RustfavaExtTestInclude.html +1 -0
- rustfava/help/__init__.py +15 -0
- rustfava/help/_index.md +29 -0
- rustfava/help/beancount_syntax.md +156 -0
- rustfava/help/budgets.md +31 -0
- rustfava/help/conversion.md +29 -0
- rustfava/help/extensions.md +111 -0
- rustfava/help/features.md +179 -0
- rustfava/help/filters.md +103 -0
- rustfava/help/import.md +27 -0
- rustfava/help/options.md +289 -0
- rustfava/helpers.py +30 -0
- rustfava/internal_api.py +221 -0
- rustfava/json_api.py +952 -0
- rustfava/plugins/__init__.py +3 -0
- rustfava/plugins/link_documents.py +107 -0
- rustfava/plugins/tag_discovered_documents.py +44 -0
- rustfava/py.typed +0 -0
- rustfava/rustledger/__init__.py +31 -0
- rustfava/rustledger/constants.py +76 -0
- rustfava/rustledger/engine.py +485 -0
- rustfava/rustledger/loader.py +273 -0
- rustfava/rustledger/options.py +202 -0
- rustfava/rustledger/query.py +331 -0
- rustfava/rustledger/types.py +830 -0
- rustfava/serialisation.py +220 -0
- rustfava/static/app.css +2988 -0
- rustfava/static/app.css.map +7 -0
- rustfava/static/app.js +12854 -0
- rustfava/static/app.js.map +7 -0
- rustfava/static/beancount-JFV44ZVZ.css +5 -0
- rustfava/static/beancount-JFV44ZVZ.css.map +7 -0
- rustfava/static/beancount-VTTKRGSK.js +4642 -0
- rustfava/static/beancount-VTTKRGSK.js.map +7 -0
- rustfava/static/bql-MGFRUMBP.js +333 -0
- rustfava/static/bql-MGFRUMBP.js.map +7 -0
- rustfava/static/chunk-E7ZF4ASL.js +23061 -0
- rustfava/static/chunk-E7ZF4ASL.js.map +7 -0
- rustfava/static/chunk-V24TLQHT.js +12673 -0
- rustfava/static/chunk-V24TLQHT.js.map +7 -0
- rustfava/static/favicon.ico +0 -0
- rustfava/static/fira-mono-cyrillic-400-normal-BLAGXRCE.woff2 +0 -0
- rustfava/static/fira-mono-cyrillic-500-normal-EN7JUAAW.woff2 +0 -0
- rustfava/static/fira-mono-cyrillic-ext-400-normal-EX7VARTS.woff2 +0 -0
- rustfava/static/fira-mono-cyrillic-ext-500-normal-ZDPTUPRR.woff2 +0 -0
- rustfava/static/fira-mono-greek-400-normal-COGHKMOA.woff2 +0 -0
- rustfava/static/fira-mono-greek-500-normal-4EN2PKZT.woff2 +0 -0
- rustfava/static/fira-mono-greek-ext-400-normal-DYEQIJH7.woff2 +0 -0
- rustfava/static/fira-mono-greek-ext-500-normal-SG73CVKQ.woff2 +0 -0
- rustfava/static/fira-mono-latin-400-normal-NA3VLV7E.woff2 +0 -0
- rustfava/static/fira-mono-latin-500-normal-YC77GFWD.woff2 +0 -0
- rustfava/static/fira-mono-latin-ext-400-normal-DIKTZ5PW.woff2 +0 -0
- rustfava/static/fira-mono-latin-ext-500-normal-ZWY4UO4V.woff2 +0 -0
- rustfava/static/fira-mono-symbols2-400-normal-UITXT77Q.woff2 +0 -0
- rustfava/static/fira-mono-symbols2-500-normal-VWPC2EFN.woff2 +0 -0
- rustfava/static/fira-sans-cyrillic-400-normal-KLQMBCA6.woff2 +0 -0
- rustfava/static/fira-sans-cyrillic-500-normal-NFG7UD6J.woff2 +0 -0
- rustfava/static/fira-sans-cyrillic-ext-400-normal-GWO44OPC.woff2 +0 -0
- rustfava/static/fira-sans-cyrillic-ext-500-normal-SP47E5SC.woff2 +0 -0
- rustfava/static/fira-sans-greek-400-normal-UMQBTLC3.woff2 +0 -0
- rustfava/static/fira-sans-greek-500-normal-4ZKHN4FQ.woff2 +0 -0
- rustfava/static/fira-sans-greek-ext-400-normal-O2DVJAJZ.woff2 +0 -0
- rustfava/static/fira-sans-greek-ext-500-normal-SK6GNWGO.woff2 +0 -0
- rustfava/static/fira-sans-latin-400-normal-OYYTPMAV.woff2 +0 -0
- rustfava/static/fira-sans-latin-500-normal-SMQPZW5A.woff2 +0 -0
- rustfava/static/fira-sans-latin-ext-400-normal-OAUP3WK5.woff2 +0 -0
- rustfava/static/fira-sans-latin-ext-500-normal-LY3YDR5Y.woff2 +0 -0
- rustfava/static/fira-sans-vietnamese-400-normal-OBMQ72MR.woff2 +0 -0
- rustfava/static/fira-sans-vietnamese-500-normal-Y4NZR5EU.woff2 +0 -0
- rustfava/static/source-code-pro-cyrillic-400-normal-TO22V6M3.woff2 +0 -0
- rustfava/static/source-code-pro-cyrillic-500-normal-OGBWWWYW.woff2 +0 -0
- rustfava/static/source-code-pro-cyrillic-ext-400-normal-XH44UCIA.woff2 +0 -0
- rustfava/static/source-code-pro-cyrillic-ext-500-normal-3Z6MMVM6.woff2 +0 -0
- rustfava/static/source-code-pro-greek-400-normal-OUXXUQWK.woff2 +0 -0
- rustfava/static/source-code-pro-greek-500-normal-JA2Z5UXO.woff2 +0 -0
- rustfava/static/source-code-pro-greek-ext-400-normal-WCDKMX7U.woff2 +0 -0
- rustfava/static/source-code-pro-greek-ext-500-normal-ZHVI4VKW.woff2 +0 -0
- rustfava/static/source-code-pro-latin-400-normal-QOGTXED5.woff2 +0 -0
- rustfava/static/source-code-pro-latin-500-normal-X57QEOLQ.woff2 +0 -0
- rustfava/static/source-code-pro-latin-ext-400-normal-QXC74NBF.woff2 +0 -0
- rustfava/static/source-code-pro-latin-ext-500-normal-QGOY7MTT.woff2 +0 -0
- rustfava/static/source-code-pro-vietnamese-400-normal-NPDCDTBA.woff2 +0 -0
- rustfava/static/source-code-pro-vietnamese-500-normal-M6PJKTR5.woff2 +0 -0
- rustfava/static/tree-sitter-beancount-MLXFQBZ5.wasm +0 -0
- rustfava/static/web-tree-sitter-RNOQ6E74.wasm +0 -0
- rustfava/template_filters.py +64 -0
- rustfava/templates/_journal_table.html +156 -0
- rustfava/templates/_layout.html +26 -0
- rustfava/templates/_query_table.html +88 -0
- rustfava/templates/beancount_file +18 -0
- rustfava/templates/help.html +23 -0
- rustfava/templates/macros/_account_macros.html +5 -0
- rustfava/templates/macros/_commodity_macros.html +13 -0
- rustfava/translations/bg/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/bg/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/ca/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/ca/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/de/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/de/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/es/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/es/LC_MESSAGES/messages.po +619 -0
- rustfava/translations/fa/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/fa/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/fr/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/fr/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/ja/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/ja/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/nl/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/nl/LC_MESSAGES/messages.po +617 -0
- rustfava/translations/pt/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/pt/LC_MESSAGES/messages.po +617 -0
- rustfava/translations/pt_BR/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/pt_BR/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/ru/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/ru/LC_MESSAGES/messages.po +617 -0
- rustfava/translations/sk/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/sk/LC_MESSAGES/messages.po +623 -0
- rustfava/translations/sv/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/sv/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/uk/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/uk/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/zh/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/zh/LC_MESSAGES/messages.po +618 -0
- rustfava/translations/zh_Hant_TW/LC_MESSAGES/messages.mo +0 -0
- rustfava/translations/zh_Hant_TW/LC_MESSAGES/messages.po +618 -0
- rustfava/util/__init__.py +157 -0
- rustfava/util/date.py +576 -0
- rustfava/util/excel.py +118 -0
- rustfava/util/ranking.py +79 -0
- rustfava/util/sets.py +18 -0
- rustfava/util/unreachable.py +20 -0
- rustfava-0.1.0.dist-info/METADATA +102 -0
- rustfava-0.1.0.dist-info/RECORD +187 -0
- rustfava-0.1.0.dist-info/WHEEL +5 -0
- rustfava-0.1.0.dist-info/entry_points.txt +2 -0
- rustfava-0.1.0.dist-info/licenses/AUTHORS +11 -0
- rustfava-0.1.0.dist-info/licenses/LICENSE +21 -0
- rustfava-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Loader functions for rustledger - replaces beancount.loader."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from rustfava.rustledger.engine import RustledgerEngine
|
|
10
|
+
from rustfava.rustledger.options import options_from_json
|
|
11
|
+
from rustfava.rustledger.types import directives_from_json
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from rustfava.beans.abc import Directive
|
|
18
|
+
from rustfava.beans.types import BeancountOptions
|
|
19
|
+
from rustfava.helpers import BeancountError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _compute_display_precision(entries_json: list[dict[str, Any]]) -> dict[str, int]:
|
|
23
|
+
"""Compute display precision from entries.
|
|
24
|
+
|
|
25
|
+
This is a workaround until rustledger FFI returns display_precision
|
|
26
|
+
from load-full command.
|
|
27
|
+
"""
|
|
28
|
+
# Track precision counts per currency: {currency: {precision: count}}
|
|
29
|
+
precision_counts: dict[str, Counter[int]] = {}
|
|
30
|
+
|
|
31
|
+
def track_amount(amt: dict[str, Any] | None) -> None:
|
|
32
|
+
if not amt:
|
|
33
|
+
return
|
|
34
|
+
number_str = amt.get("number", "")
|
|
35
|
+
currency = amt.get("currency", "")
|
|
36
|
+
if not number_str or not currency:
|
|
37
|
+
return
|
|
38
|
+
# Calculate precision from decimal places
|
|
39
|
+
if "." in str(number_str):
|
|
40
|
+
precision = len(str(number_str).split(".")[-1])
|
|
41
|
+
else:
|
|
42
|
+
precision = 0
|
|
43
|
+
if currency not in precision_counts:
|
|
44
|
+
precision_counts[currency] = Counter()
|
|
45
|
+
precision_counts[currency][precision] += 1
|
|
46
|
+
|
|
47
|
+
for entry in entries_json:
|
|
48
|
+
entry_type = entry.get("type", "")
|
|
49
|
+
|
|
50
|
+
if entry_type == "transaction":
|
|
51
|
+
for posting in entry.get("postings", []):
|
|
52
|
+
track_amount(posting.get("units"))
|
|
53
|
+
if posting.get("cost"):
|
|
54
|
+
cost = posting["cost"]
|
|
55
|
+
track_amount({"number": cost.get("number"), "currency": cost.get("currency")})
|
|
56
|
+
track_amount(posting.get("price"))
|
|
57
|
+
|
|
58
|
+
elif entry_type == "balance":
|
|
59
|
+
track_amount(entry.get("amount"))
|
|
60
|
+
|
|
61
|
+
elif entry_type == "price":
|
|
62
|
+
track_amount(entry.get("amount"))
|
|
63
|
+
|
|
64
|
+
# Get most common precision for each currency
|
|
65
|
+
result = {}
|
|
66
|
+
for currency, counts in precision_counts.items():
|
|
67
|
+
if counts:
|
|
68
|
+
result[currency] = counts.most_common(1)[0][0]
|
|
69
|
+
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _errors_from_json(
|
|
74
|
+
errors_json: list[dict[str, Any]],
|
|
75
|
+
filename: str = "<unknown>",
|
|
76
|
+
) -> list[BeancountError]:
|
|
77
|
+
"""Convert rustledger errors to Fava BeancountError format."""
|
|
78
|
+
from rustfava.helpers import BeancountError
|
|
79
|
+
|
|
80
|
+
result = []
|
|
81
|
+
for err in errors_json:
|
|
82
|
+
# Handle both old format (source dict) and new format (line field)
|
|
83
|
+
if "source" in err:
|
|
84
|
+
source = err["source"]
|
|
85
|
+
err_filename = source.get("filename", filename)
|
|
86
|
+
err_lineno = source.get("lineno", 0)
|
|
87
|
+
else:
|
|
88
|
+
err_filename = err.get("filename", filename)
|
|
89
|
+
err_lineno = err.get("line", 0)
|
|
90
|
+
|
|
91
|
+
result.append(
|
|
92
|
+
BeancountError(
|
|
93
|
+
source={
|
|
94
|
+
"filename": err_filename,
|
|
95
|
+
"lineno": err_lineno,
|
|
96
|
+
},
|
|
97
|
+
message=err.get("message", "Unknown error"),
|
|
98
|
+
entry=None,
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _run_plugins(
|
|
105
|
+
entries: list[Directive],
|
|
106
|
+
plugins: list[dict[str, Any]],
|
|
107
|
+
options: BeancountOptions,
|
|
108
|
+
) -> tuple[list[Directive], list[BeancountError]]:
|
|
109
|
+
"""Run Python plugins on entries.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
entries: List of parsed entries
|
|
113
|
+
plugins: List of plugin specs from rustledger ({"name": "...", "config": "..."})
|
|
114
|
+
options: Beancount options dict
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Tuple of (processed entries, plugin errors)
|
|
118
|
+
"""
|
|
119
|
+
import importlib
|
|
120
|
+
|
|
121
|
+
from rustfava.helpers import BeancountError
|
|
122
|
+
|
|
123
|
+
all_errors: list[BeancountError] = []
|
|
124
|
+
|
|
125
|
+
for plugin_spec in plugins:
|
|
126
|
+
plugin_name = plugin_spec.get("name", "")
|
|
127
|
+
plugin_config = plugin_spec.get("config")
|
|
128
|
+
|
|
129
|
+
if not plugin_name:
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# Skip beancount.plugins.auto_accounts - handled natively by rustledger
|
|
133
|
+
if plugin_name == "beancount.plugins.auto_accounts":
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
module = importlib.import_module(plugin_name)
|
|
138
|
+
except ImportError as e:
|
|
139
|
+
all_errors.append(
|
|
140
|
+
BeancountError(
|
|
141
|
+
source={"filename": "<plugin>", "lineno": 0},
|
|
142
|
+
message=f"Failed to import plugin '{plugin_name}': {e}",
|
|
143
|
+
entry=None,
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# Get plugin functions from __plugins__ attribute
|
|
149
|
+
plugin_funcs = getattr(module, "__plugins__", [])
|
|
150
|
+
if not plugin_funcs:
|
|
151
|
+
# Try using the module name as the function name
|
|
152
|
+
func_name = plugin_name.split(".")[-1]
|
|
153
|
+
if hasattr(module, func_name):
|
|
154
|
+
plugin_funcs = [func_name]
|
|
155
|
+
|
|
156
|
+
for func_name in plugin_funcs:
|
|
157
|
+
plugin_fn = getattr(module, func_name, None)
|
|
158
|
+
if plugin_fn is None:
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
# Call plugin: (entries, options) -> (entries, errors)
|
|
163
|
+
if plugin_config is not None:
|
|
164
|
+
entries, plugin_errors = plugin_fn(entries, plugin_config)
|
|
165
|
+
else:
|
|
166
|
+
entries, plugin_errors = plugin_fn(entries, options)
|
|
167
|
+
|
|
168
|
+
# Convert any plugin errors to our error format
|
|
169
|
+
for err in plugin_errors:
|
|
170
|
+
if isinstance(err, BeancountError):
|
|
171
|
+
all_errors.append(err)
|
|
172
|
+
else:
|
|
173
|
+
all_errors.append(
|
|
174
|
+
BeancountError(
|
|
175
|
+
source=getattr(err, "source", {"filename": "<plugin>", "lineno": 0}),
|
|
176
|
+
message=getattr(err, "message", str(err)),
|
|
177
|
+
entry=getattr(err, "entry", None),
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
all_errors.append(
|
|
182
|
+
BeancountError(
|
|
183
|
+
source={"filename": "<plugin>", "lineno": 0},
|
|
184
|
+
message=f"Plugin '{plugin_name}.{func_name}' raised: {e}",
|
|
185
|
+
entry=None,
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return list(entries), all_errors
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def load_string(
|
|
193
|
+
value: str,
|
|
194
|
+
filename: str = "<string>",
|
|
195
|
+
) -> tuple[
|
|
196
|
+
Sequence[Directive],
|
|
197
|
+
Sequence[BeancountError],
|
|
198
|
+
BeancountOptions,
|
|
199
|
+
]:
|
|
200
|
+
"""Load a Beancount string.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
value: Beancount source code
|
|
204
|
+
filename: Filename to use in metadata
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Tuple of (entries, errors, options)
|
|
208
|
+
"""
|
|
209
|
+
engine = RustledgerEngine.get_instance()
|
|
210
|
+
result = engine.load(value, filename)
|
|
211
|
+
|
|
212
|
+
entries = list(directives_from_json(result.get("entries", [])))
|
|
213
|
+
errors = list(_errors_from_json(result.get("errors", []), filename))
|
|
214
|
+
options = options_from_json(result.get("options", {}))
|
|
215
|
+
|
|
216
|
+
# Run Python plugins if any are specified
|
|
217
|
+
plugins = result.get("plugins", [])
|
|
218
|
+
if plugins:
|
|
219
|
+
entries, plugin_errors = _run_plugins(entries, plugins, options)
|
|
220
|
+
errors.extend(plugin_errors)
|
|
221
|
+
|
|
222
|
+
return entries, errors, options
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def load_uncached(
|
|
226
|
+
beancount_file_path: str,
|
|
227
|
+
*,
|
|
228
|
+
is_encrypted: bool = False,
|
|
229
|
+
) -> tuple[
|
|
230
|
+
Sequence[Directive],
|
|
231
|
+
Sequence[BeancountError],
|
|
232
|
+
BeancountOptions,
|
|
233
|
+
]:
|
|
234
|
+
"""Load a Beancount file.
|
|
235
|
+
|
|
236
|
+
Uses rustledger's load-full command which handles:
|
|
237
|
+
- Include resolution with cycle detection
|
|
238
|
+
- Path security (prevents path traversal)
|
|
239
|
+
- GPG decryption for encrypted files
|
|
240
|
+
- Native plugin execution (auto_accounts)
|
|
241
|
+
- Entry sorting
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
beancount_file_path: Path to the main beancount file
|
|
245
|
+
is_encrypted: Ignored - rustledger handles GPG decryption
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Tuple of (entries, errors, options)
|
|
249
|
+
"""
|
|
250
|
+
del is_encrypted # Rustledger handles GPG decryption automatically
|
|
251
|
+
|
|
252
|
+
main_path = Path(beancount_file_path)
|
|
253
|
+
engine = RustledgerEngine.get_instance()
|
|
254
|
+
|
|
255
|
+
# Use load_full with auto_accounts plugin for sorting and account generation
|
|
256
|
+
result = engine.load_full(str(main_path), plugins=["auto_accounts"])
|
|
257
|
+
|
|
258
|
+
entries_json = result.get("entries", [])
|
|
259
|
+
|
|
260
|
+
# Compute display_precision if not provided by FFI (workaround)
|
|
261
|
+
options_json = result.get("options", {})
|
|
262
|
+
if not options_json.get("display_precision"):
|
|
263
|
+
options_json["display_precision"] = _compute_display_precision(entries_json)
|
|
264
|
+
|
|
265
|
+
entries = directives_from_json(entries_json)
|
|
266
|
+
errors = list(_errors_from_json(result.get("errors", []), str(main_path)))
|
|
267
|
+
options = options_from_json(options_json)
|
|
268
|
+
|
|
269
|
+
# Set include list and filename in options
|
|
270
|
+
options["include"] = result.get("loaded_files", [str(main_path)])
|
|
271
|
+
options["filename"] = str(main_path)
|
|
272
|
+
|
|
273
|
+
return entries, errors, options
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Options adapter for rustledger JSON to Fava's BeancountOptions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from rustfava.beans.types import BeancountOptions
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _RLCurrencyContext:
|
|
15
|
+
"""Minimal CurrencyContext for a single currency."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, precision: int) -> None:
|
|
18
|
+
self._precision = precision
|
|
19
|
+
|
|
20
|
+
def get_fractional(self, _precision_type: Any = None) -> int:
|
|
21
|
+
"""Return the fractional precision (beancount-compatible API)."""
|
|
22
|
+
return self._precision
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RLDisplayContext:
|
|
26
|
+
"""Minimal DisplayContext implementation for rustledger.
|
|
27
|
+
|
|
28
|
+
This replaces beancount.core.display_context.DisplayContext.
|
|
29
|
+
Provides beancount-compatible `ccontexts` property.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, options: dict[str, Any]) -> None:
|
|
33
|
+
"""Initialize from rustledger options."""
|
|
34
|
+
self._precision = options.get("display_precision", {})
|
|
35
|
+
self._render_commas = options.get("render_commas", True)
|
|
36
|
+
# Beancount-compatible ccontexts mapping
|
|
37
|
+
self.ccontexts = {
|
|
38
|
+
currency: _RLCurrencyContext(prec)
|
|
39
|
+
for currency, prec in self._precision.items()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def build(self) -> RLDisplayFormatter:
|
|
43
|
+
"""Build a formatter from this context."""
|
|
44
|
+
return RLDisplayFormatter(self._precision, self._render_commas)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RLDisplayFormatter:
|
|
48
|
+
"""Formatter for decimal numbers."""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
precision: dict[str, int],
|
|
53
|
+
render_commas: bool,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Initialize formatter."""
|
|
56
|
+
self._precision = precision
|
|
57
|
+
self._render_commas = render_commas
|
|
58
|
+
|
|
59
|
+
def format(self, number: Decimal, currency: str) -> str:
|
|
60
|
+
"""Format a decimal number for a currency."""
|
|
61
|
+
prec = self._precision.get(currency, 2)
|
|
62
|
+
formatted = f"{number:.{prec}f}"
|
|
63
|
+
if self._render_commas:
|
|
64
|
+
# Add thousand separators
|
|
65
|
+
parts = formatted.split(".")
|
|
66
|
+
parts[0] = "{:,}".format(int(parts[0]))
|
|
67
|
+
formatted = ".".join(parts)
|
|
68
|
+
return formatted
|
|
69
|
+
|
|
70
|
+
def quantize(
|
|
71
|
+
self,
|
|
72
|
+
number: Decimal,
|
|
73
|
+
currency: str = "__default__",
|
|
74
|
+
) -> Decimal:
|
|
75
|
+
"""Quantize a number to the precision for a currency.
|
|
76
|
+
|
|
77
|
+
This matches beancount's DisplayFormatter.quantize interface.
|
|
78
|
+
"""
|
|
79
|
+
prec = self._precision.get(currency, 2)
|
|
80
|
+
# Create a Decimal with the right number of decimal places
|
|
81
|
+
quantizer = Decimal(10) ** -prec
|
|
82
|
+
return number.quantize(quantizer)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RLBooking:
|
|
86
|
+
"""Booking method enum compatible with beancount.core.data.Booking."""
|
|
87
|
+
|
|
88
|
+
STRICT = "STRICT"
|
|
89
|
+
FIFO = "FIFO"
|
|
90
|
+
LIFO = "LIFO"
|
|
91
|
+
HIFO = "HIFO"
|
|
92
|
+
AVERAGE = "AVERAGE"
|
|
93
|
+
NONE = "NONE"
|
|
94
|
+
|
|
95
|
+
def __init__(self, value: str) -> None:
|
|
96
|
+
"""Initialize from string value."""
|
|
97
|
+
self.value = value.upper() if value else "STRICT"
|
|
98
|
+
|
|
99
|
+
def __str__(self) -> str:
|
|
100
|
+
"""Return string representation."""
|
|
101
|
+
return self.value
|
|
102
|
+
|
|
103
|
+
def __eq__(self, other: object) -> bool:
|
|
104
|
+
"""Check equality."""
|
|
105
|
+
if isinstance(other, str):
|
|
106
|
+
return self.value == other.upper()
|
|
107
|
+
if isinstance(other, RLBooking):
|
|
108
|
+
return self.value == other.value
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def options_from_json(data: dict[str, Any]) -> BeancountOptions:
|
|
113
|
+
"""Convert rustledger options JSON to Fava's BeancountOptions.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
data: JSON dict of options from rustledger
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
BeancountOptions TypedDict
|
|
120
|
+
"""
|
|
121
|
+
# Create display context
|
|
122
|
+
dcontext = RLDisplayContext(data)
|
|
123
|
+
|
|
124
|
+
# Parse display_precision
|
|
125
|
+
display_precision = {
|
|
126
|
+
k: Decimal(str(v))
|
|
127
|
+
for k, v in data.get("display_precision", {}).items()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Parse inferred_tolerance_default
|
|
131
|
+
inferred_tolerance_default = {
|
|
132
|
+
k: Decimal(str(v))
|
|
133
|
+
for k, v in data.get("inferred_tolerance_default", {}).items()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
options: BeancountOptions = {
|
|
137
|
+
"title": data.get("title", ""),
|
|
138
|
+
"filename": data.get("filename", ""),
|
|
139
|
+
# Root account names
|
|
140
|
+
"name_assets": data.get("name_assets", "Assets"),
|
|
141
|
+
"name_liabilities": data.get("name_liabilities", "Liabilities"),
|
|
142
|
+
"name_equity": data.get("name_equity", "Equity"),
|
|
143
|
+
"name_income": data.get("name_income", "Income"),
|
|
144
|
+
"name_expenses": data.get("name_expenses", "Expenses"),
|
|
145
|
+
# Special accounts
|
|
146
|
+
"account_current_conversions": data.get(
|
|
147
|
+
"account_current_conversions", "Equity:Conversions:Current"
|
|
148
|
+
),
|
|
149
|
+
"account_current_earnings": data.get(
|
|
150
|
+
"account_current_earnings", "Equity:Earnings:Current"
|
|
151
|
+
),
|
|
152
|
+
"account_previous_balances": data.get(
|
|
153
|
+
"account_previous_balances", "Equity:Opening-Balances"
|
|
154
|
+
),
|
|
155
|
+
"account_previous_conversions": data.get(
|
|
156
|
+
"account_previous_conversions", "Equity:Conversions:Previous"
|
|
157
|
+
),
|
|
158
|
+
"account_previous_earnings": data.get(
|
|
159
|
+
"account_previous_earnings", "Equity:Earnings:Previous"
|
|
160
|
+
),
|
|
161
|
+
"account_rounding": data.get("account_rounding"),
|
|
162
|
+
"account_unrealized_gains": data.get(
|
|
163
|
+
"account_unrealized_gains", "Income:Unrealized"
|
|
164
|
+
),
|
|
165
|
+
# Booking and commodities
|
|
166
|
+
"booking_method": RLBooking(data.get("booking_method", "STRICT")),
|
|
167
|
+
"commodities": set(data.get("commodities", [])),
|
|
168
|
+
"conversion_currency": data.get("conversion_currency", ""),
|
|
169
|
+
"dcontext": dcontext,
|
|
170
|
+
"display_precision": display_precision,
|
|
171
|
+
# File handling
|
|
172
|
+
"documents": list(data.get("documents", [])),
|
|
173
|
+
"include": list(data.get("include", [])),
|
|
174
|
+
# Tolerances
|
|
175
|
+
"infer_tolerance_from_cost": data.get("infer_tolerance_from_cost", False),
|
|
176
|
+
"inferred_tolerance_default": inferred_tolerance_default,
|
|
177
|
+
"inferred_tolerance_multiplier": Decimal(
|
|
178
|
+
str(data.get("inferred_tolerance_multiplier", "0.5"))
|
|
179
|
+
),
|
|
180
|
+
"input_hash": data.get("input_hash", ""),
|
|
181
|
+
"insert_pythonpath": data.get("insert_pythonpath", False),
|
|
182
|
+
"operating_currency": list(data.get("operating_currency", [])),
|
|
183
|
+
# Plugins (won't work with rustledger, but keep for compatibility)
|
|
184
|
+
"plugin": list(
|
|
185
|
+
tuple(p) if isinstance(p, list) else (p, None)
|
|
186
|
+
for p in data.get("plugin", [])
|
|
187
|
+
),
|
|
188
|
+
"plugin_processing_mode": data.get("plugin_processing_mode", ""),
|
|
189
|
+
"pythonpath": list(data.get("pythonpath", [])),
|
|
190
|
+
"render_commas": data.get("render_commas", False),
|
|
191
|
+
"tolerance_multiplier": Decimal(
|
|
192
|
+
str(data.get("tolerance_multiplier", "1.0"))
|
|
193
|
+
),
|
|
194
|
+
# Deprecated options (for compatibility)
|
|
195
|
+
"allow_deprecated_none_for_tags_and_links": data.get(
|
|
196
|
+
"allow_deprecated_none_for_tags_and_links", False
|
|
197
|
+
),
|
|
198
|
+
"allow_pipe_separator": data.get("allow_pipe_separator", False),
|
|
199
|
+
"long_string_maxlines": data.get("long_string_maxlines", 64),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return options
|