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.
Files changed (187) hide show
  1. rustfava/__init__.py +30 -0
  2. rustfava/_ctx_globals_class.py +55 -0
  3. rustfava/api_models.py +36 -0
  4. rustfava/application.py +534 -0
  5. rustfava/beans/__init__.py +6 -0
  6. rustfava/beans/abc.py +327 -0
  7. rustfava/beans/account.py +79 -0
  8. rustfava/beans/create.py +377 -0
  9. rustfava/beans/flags.py +20 -0
  10. rustfava/beans/funcs.py +38 -0
  11. rustfava/beans/helpers.py +52 -0
  12. rustfava/beans/ingest.py +75 -0
  13. rustfava/beans/load.py +31 -0
  14. rustfava/beans/prices.py +151 -0
  15. rustfava/beans/protocols.py +82 -0
  16. rustfava/beans/str.py +454 -0
  17. rustfava/beans/types.py +63 -0
  18. rustfava/cli.py +187 -0
  19. rustfava/context.py +13 -0
  20. rustfava/core/__init__.py +729 -0
  21. rustfava/core/accounts.py +161 -0
  22. rustfava/core/attributes.py +145 -0
  23. rustfava/core/budgets.py +207 -0
  24. rustfava/core/charts.py +301 -0
  25. rustfava/core/commodities.py +37 -0
  26. rustfava/core/conversion.py +229 -0
  27. rustfava/core/documents.py +87 -0
  28. rustfava/core/extensions.py +132 -0
  29. rustfava/core/fava_options.py +255 -0
  30. rustfava/core/file.py +542 -0
  31. rustfava/core/filters.py +484 -0
  32. rustfava/core/group_entries.py +97 -0
  33. rustfava/core/ingest.py +509 -0
  34. rustfava/core/inventory.py +167 -0
  35. rustfava/core/misc.py +105 -0
  36. rustfava/core/module_base.py +18 -0
  37. rustfava/core/number.py +106 -0
  38. rustfava/core/query.py +180 -0
  39. rustfava/core/query_shell.py +301 -0
  40. rustfava/core/tree.py +265 -0
  41. rustfava/core/watcher.py +219 -0
  42. rustfava/ext/__init__.py +232 -0
  43. rustfava/ext/auto_commit.py +61 -0
  44. rustfava/ext/portfolio_list/PortfolioList.js +34 -0
  45. rustfava/ext/portfolio_list/__init__.py +29 -0
  46. rustfava/ext/portfolio_list/templates/PortfolioList.html +15 -0
  47. rustfava/ext/rustfava_ext_test/RustfavaExtTest.js +42 -0
  48. rustfava/ext/rustfava_ext_test/__init__.py +207 -0
  49. rustfava/ext/rustfava_ext_test/templates/RustfavaExtTest.html +45 -0
  50. rustfava/ext/rustfava_ext_test/templates/RustfavaExtTestInclude.html +1 -0
  51. rustfava/help/__init__.py +15 -0
  52. rustfava/help/_index.md +29 -0
  53. rustfava/help/beancount_syntax.md +156 -0
  54. rustfava/help/budgets.md +31 -0
  55. rustfava/help/conversion.md +29 -0
  56. rustfava/help/extensions.md +111 -0
  57. rustfava/help/features.md +179 -0
  58. rustfava/help/filters.md +103 -0
  59. rustfava/help/import.md +27 -0
  60. rustfava/help/options.md +289 -0
  61. rustfava/helpers.py +30 -0
  62. rustfava/internal_api.py +221 -0
  63. rustfava/json_api.py +952 -0
  64. rustfava/plugins/__init__.py +3 -0
  65. rustfava/plugins/link_documents.py +107 -0
  66. rustfava/plugins/tag_discovered_documents.py +44 -0
  67. rustfava/py.typed +0 -0
  68. rustfava/rustledger/__init__.py +31 -0
  69. rustfava/rustledger/constants.py +76 -0
  70. rustfava/rustledger/engine.py +485 -0
  71. rustfava/rustledger/loader.py +273 -0
  72. rustfava/rustledger/options.py +202 -0
  73. rustfava/rustledger/query.py +331 -0
  74. rustfava/rustledger/types.py +830 -0
  75. rustfava/serialisation.py +220 -0
  76. rustfava/static/app.css +2988 -0
  77. rustfava/static/app.css.map +7 -0
  78. rustfava/static/app.js +12854 -0
  79. rustfava/static/app.js.map +7 -0
  80. rustfava/static/beancount-JFV44ZVZ.css +5 -0
  81. rustfava/static/beancount-JFV44ZVZ.css.map +7 -0
  82. rustfava/static/beancount-VTTKRGSK.js +4642 -0
  83. rustfava/static/beancount-VTTKRGSK.js.map +7 -0
  84. rustfava/static/bql-MGFRUMBP.js +333 -0
  85. rustfava/static/bql-MGFRUMBP.js.map +7 -0
  86. rustfava/static/chunk-E7ZF4ASL.js +23061 -0
  87. rustfava/static/chunk-E7ZF4ASL.js.map +7 -0
  88. rustfava/static/chunk-V24TLQHT.js +12673 -0
  89. rustfava/static/chunk-V24TLQHT.js.map +7 -0
  90. rustfava/static/favicon.ico +0 -0
  91. rustfava/static/fira-mono-cyrillic-400-normal-BLAGXRCE.woff2 +0 -0
  92. rustfava/static/fira-mono-cyrillic-500-normal-EN7JUAAW.woff2 +0 -0
  93. rustfava/static/fira-mono-cyrillic-ext-400-normal-EX7VARTS.woff2 +0 -0
  94. rustfava/static/fira-mono-cyrillic-ext-500-normal-ZDPTUPRR.woff2 +0 -0
  95. rustfava/static/fira-mono-greek-400-normal-COGHKMOA.woff2 +0 -0
  96. rustfava/static/fira-mono-greek-500-normal-4EN2PKZT.woff2 +0 -0
  97. rustfava/static/fira-mono-greek-ext-400-normal-DYEQIJH7.woff2 +0 -0
  98. rustfava/static/fira-mono-greek-ext-500-normal-SG73CVKQ.woff2 +0 -0
  99. rustfava/static/fira-mono-latin-400-normal-NA3VLV7E.woff2 +0 -0
  100. rustfava/static/fira-mono-latin-500-normal-YC77GFWD.woff2 +0 -0
  101. rustfava/static/fira-mono-latin-ext-400-normal-DIKTZ5PW.woff2 +0 -0
  102. rustfava/static/fira-mono-latin-ext-500-normal-ZWY4UO4V.woff2 +0 -0
  103. rustfava/static/fira-mono-symbols2-400-normal-UITXT77Q.woff2 +0 -0
  104. rustfava/static/fira-mono-symbols2-500-normal-VWPC2EFN.woff2 +0 -0
  105. rustfava/static/fira-sans-cyrillic-400-normal-KLQMBCA6.woff2 +0 -0
  106. rustfava/static/fira-sans-cyrillic-500-normal-NFG7UD6J.woff2 +0 -0
  107. rustfava/static/fira-sans-cyrillic-ext-400-normal-GWO44OPC.woff2 +0 -0
  108. rustfava/static/fira-sans-cyrillic-ext-500-normal-SP47E5SC.woff2 +0 -0
  109. rustfava/static/fira-sans-greek-400-normal-UMQBTLC3.woff2 +0 -0
  110. rustfava/static/fira-sans-greek-500-normal-4ZKHN4FQ.woff2 +0 -0
  111. rustfava/static/fira-sans-greek-ext-400-normal-O2DVJAJZ.woff2 +0 -0
  112. rustfava/static/fira-sans-greek-ext-500-normal-SK6GNWGO.woff2 +0 -0
  113. rustfava/static/fira-sans-latin-400-normal-OYYTPMAV.woff2 +0 -0
  114. rustfava/static/fira-sans-latin-500-normal-SMQPZW5A.woff2 +0 -0
  115. rustfava/static/fira-sans-latin-ext-400-normal-OAUP3WK5.woff2 +0 -0
  116. rustfava/static/fira-sans-latin-ext-500-normal-LY3YDR5Y.woff2 +0 -0
  117. rustfava/static/fira-sans-vietnamese-400-normal-OBMQ72MR.woff2 +0 -0
  118. rustfava/static/fira-sans-vietnamese-500-normal-Y4NZR5EU.woff2 +0 -0
  119. rustfava/static/source-code-pro-cyrillic-400-normal-TO22V6M3.woff2 +0 -0
  120. rustfava/static/source-code-pro-cyrillic-500-normal-OGBWWWYW.woff2 +0 -0
  121. rustfava/static/source-code-pro-cyrillic-ext-400-normal-XH44UCIA.woff2 +0 -0
  122. rustfava/static/source-code-pro-cyrillic-ext-500-normal-3Z6MMVM6.woff2 +0 -0
  123. rustfava/static/source-code-pro-greek-400-normal-OUXXUQWK.woff2 +0 -0
  124. rustfava/static/source-code-pro-greek-500-normal-JA2Z5UXO.woff2 +0 -0
  125. rustfava/static/source-code-pro-greek-ext-400-normal-WCDKMX7U.woff2 +0 -0
  126. rustfava/static/source-code-pro-greek-ext-500-normal-ZHVI4VKW.woff2 +0 -0
  127. rustfava/static/source-code-pro-latin-400-normal-QOGTXED5.woff2 +0 -0
  128. rustfava/static/source-code-pro-latin-500-normal-X57QEOLQ.woff2 +0 -0
  129. rustfava/static/source-code-pro-latin-ext-400-normal-QXC74NBF.woff2 +0 -0
  130. rustfava/static/source-code-pro-latin-ext-500-normal-QGOY7MTT.woff2 +0 -0
  131. rustfava/static/source-code-pro-vietnamese-400-normal-NPDCDTBA.woff2 +0 -0
  132. rustfava/static/source-code-pro-vietnamese-500-normal-M6PJKTR5.woff2 +0 -0
  133. rustfava/static/tree-sitter-beancount-MLXFQBZ5.wasm +0 -0
  134. rustfava/static/web-tree-sitter-RNOQ6E74.wasm +0 -0
  135. rustfava/template_filters.py +64 -0
  136. rustfava/templates/_journal_table.html +156 -0
  137. rustfava/templates/_layout.html +26 -0
  138. rustfava/templates/_query_table.html +88 -0
  139. rustfava/templates/beancount_file +18 -0
  140. rustfava/templates/help.html +23 -0
  141. rustfava/templates/macros/_account_macros.html +5 -0
  142. rustfava/templates/macros/_commodity_macros.html +13 -0
  143. rustfava/translations/bg/LC_MESSAGES/messages.mo +0 -0
  144. rustfava/translations/bg/LC_MESSAGES/messages.po +618 -0
  145. rustfava/translations/ca/LC_MESSAGES/messages.mo +0 -0
  146. rustfava/translations/ca/LC_MESSAGES/messages.po +618 -0
  147. rustfava/translations/de/LC_MESSAGES/messages.mo +0 -0
  148. rustfava/translations/de/LC_MESSAGES/messages.po +618 -0
  149. rustfava/translations/es/LC_MESSAGES/messages.mo +0 -0
  150. rustfava/translations/es/LC_MESSAGES/messages.po +619 -0
  151. rustfava/translations/fa/LC_MESSAGES/messages.mo +0 -0
  152. rustfava/translations/fa/LC_MESSAGES/messages.po +618 -0
  153. rustfava/translations/fr/LC_MESSAGES/messages.mo +0 -0
  154. rustfava/translations/fr/LC_MESSAGES/messages.po +618 -0
  155. rustfava/translations/ja/LC_MESSAGES/messages.mo +0 -0
  156. rustfava/translations/ja/LC_MESSAGES/messages.po +618 -0
  157. rustfava/translations/nl/LC_MESSAGES/messages.mo +0 -0
  158. rustfava/translations/nl/LC_MESSAGES/messages.po +617 -0
  159. rustfava/translations/pt/LC_MESSAGES/messages.mo +0 -0
  160. rustfava/translations/pt/LC_MESSAGES/messages.po +617 -0
  161. rustfava/translations/pt_BR/LC_MESSAGES/messages.mo +0 -0
  162. rustfava/translations/pt_BR/LC_MESSAGES/messages.po +618 -0
  163. rustfava/translations/ru/LC_MESSAGES/messages.mo +0 -0
  164. rustfava/translations/ru/LC_MESSAGES/messages.po +617 -0
  165. rustfava/translations/sk/LC_MESSAGES/messages.mo +0 -0
  166. rustfava/translations/sk/LC_MESSAGES/messages.po +623 -0
  167. rustfava/translations/sv/LC_MESSAGES/messages.mo +0 -0
  168. rustfava/translations/sv/LC_MESSAGES/messages.po +618 -0
  169. rustfava/translations/uk/LC_MESSAGES/messages.mo +0 -0
  170. rustfava/translations/uk/LC_MESSAGES/messages.po +618 -0
  171. rustfava/translations/zh/LC_MESSAGES/messages.mo +0 -0
  172. rustfava/translations/zh/LC_MESSAGES/messages.po +618 -0
  173. rustfava/translations/zh_Hant_TW/LC_MESSAGES/messages.mo +0 -0
  174. rustfava/translations/zh_Hant_TW/LC_MESSAGES/messages.po +618 -0
  175. rustfava/util/__init__.py +157 -0
  176. rustfava/util/date.py +576 -0
  177. rustfava/util/excel.py +118 -0
  178. rustfava/util/ranking.py +79 -0
  179. rustfava/util/sets.py +18 -0
  180. rustfava/util/unreachable.py +20 -0
  181. rustfava-0.1.0.dist-info/METADATA +102 -0
  182. rustfava-0.1.0.dist-info/RECORD +187 -0
  183. rustfava-0.1.0.dist-info/WHEEL +5 -0
  184. rustfava-0.1.0.dist-info/entry_points.txt +2 -0
  185. rustfava-0.1.0.dist-info/licenses/AUTHORS +11 -0
  186. rustfava-0.1.0.dist-info/licenses/LICENSE +21 -0
  187. rustfava-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,220 @@
1
+ """(De)serialisation of entries.
2
+
3
+ When adding entries, these are saved via the JSON API - using the functionality
4
+ of this module to obtain the appropriate data structures from
5
+ `beancount.core.data`. Similarly, for the full entry completion, a JSON
6
+ representation of the entry is provided.
7
+
8
+ This is not intended to work well enough for full roundtrips yet.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import datetime
14
+ from collections.abc import Mapping
15
+ from copy import copy
16
+ from decimal import Decimal
17
+ from functools import singledispatch
18
+ from typing import Any
19
+
20
+ from rustfava.beans import create
21
+ from rustfava.beans.load import load_string
22
+ from rustfava.beans.abc import Balance
23
+ from rustfava.beans.abc import Custom
24
+ from rustfava.beans.abc import Directive
25
+ from rustfava.beans.abc import Posting
26
+ from rustfava.beans.abc import Price
27
+ from rustfava.beans.abc import Transaction
28
+ from rustfava.beans.funcs import hash_entry
29
+ from rustfava.beans.helpers import replace
30
+ from rustfava.beans.str import to_string
31
+ from rustfava.helpers import RustfavaAPIError
32
+ from rustfava.util.date import parse_date
33
+
34
+
35
+ class InvalidAmountError(RustfavaAPIError):
36
+ """Invalid amount."""
37
+
38
+ def __init__(self, amount: str) -> None:
39
+ super().__init__(f"Invalid amount: {amount}")
40
+
41
+
42
+ # Internal meta fields that should not be serialised to JSON
43
+ _INTERNAL_META_KEYS = {"filename", "lineno", "hash", "__tolerances__"}
44
+
45
+ # Map rustledger type names to standard beancount type names
46
+ _TYPE_NAME_MAP = {
47
+ "RLDocument": "Document",
48
+ "RLNote": "Note",
49
+ "RLEvent": "Event",
50
+ "RLQuery": "Query",
51
+ "RLCommodity": "Commodity",
52
+ "RLOpen": "Open",
53
+ "RLClose": "Close",
54
+ "RLPad": "Pad",
55
+ }
56
+
57
+
58
+ def _get_entry_type_name(entry: Directive) -> str:
59
+ """Get the canonical entry type name for serialisation."""
60
+ name = entry.__class__.__name__
61
+ return _TYPE_NAME_MAP.get(name, name)
62
+
63
+
64
+ def _clean_meta(meta: dict[str, object] | Mapping[str, object]) -> dict[str, object]:
65
+ """Remove internal meta fields from a copy of the metadata."""
66
+ result = dict(meta)
67
+ for key in _INTERNAL_META_KEYS:
68
+ result.pop(key, None)
69
+ return result
70
+
71
+
72
+ @singledispatch
73
+ def serialise(entry: Directive | Posting) -> Any:
74
+ """Serialise an entry or posting."""
75
+ if not isinstance(entry, Directive): # pragma: no cover
76
+ msg = f"Unsupported object {entry}"
77
+ raise TypeError(msg)
78
+ ret = entry._asdict() # type: ignore[attr-defined]
79
+ ret["meta"] = _clean_meta(ret.get("meta", {}))
80
+ ret["entry_hash"] = hash_entry(entry)
81
+ ret["t"] = _get_entry_type_name(entry)
82
+ return ret
83
+
84
+
85
+ @serialise.register(Transaction)
86
+ def _(entry: Transaction) -> Any:
87
+ ret = entry._asdict() # type: ignore[attr-defined]
88
+ ret["meta"] = _clean_meta(entry.meta)
89
+ ret["t"] = "Transaction"
90
+ ret["entry_hash"] = hash_entry(entry)
91
+ ret["payee"] = entry.payee or ""
92
+ ret["postings"] = list(map(serialise, entry.postings))
93
+ return ret
94
+
95
+
96
+ @serialise.register(Custom)
97
+ def _(entry: Custom) -> Any:
98
+ ret = entry._asdict() # type: ignore[attr-defined]
99
+ ret["meta"] = _clean_meta(ret.get("meta", {}))
100
+ ret["t"] = "Custom"
101
+ ret["entry_hash"] = hash_entry(entry)
102
+ ret["values"] = [v.value for v in entry.values]
103
+ return ret
104
+
105
+
106
+ @serialise.register(Balance)
107
+ def _(entry: Balance) -> Any:
108
+ ret = entry._asdict() # type: ignore[attr-defined]
109
+ ret["meta"] = _clean_meta(ret.get("meta", {}))
110
+ ret["t"] = "Balance"
111
+ ret["entry_hash"] = hash_entry(entry)
112
+ amt = ret["amount"]
113
+ ret["amount"] = {"number": str(amt.number), "currency": amt.currency}
114
+ return ret
115
+
116
+
117
+ @serialise.register(Price)
118
+ def _(entry: Price) -> Any:
119
+ ret = entry._asdict() # type: ignore[attr-defined]
120
+ ret["meta"] = _clean_meta(ret.get("meta", {}))
121
+ ret["t"] = "Price"
122
+ ret["entry_hash"] = hash_entry(entry)
123
+ amt = ret["amount"]
124
+ ret["amount"] = {"number": str(amt.number), "currency": amt.currency}
125
+ return ret
126
+
127
+
128
+ @serialise.register(Posting)
129
+ def _(posting: Posting) -> Any:
130
+ position_str = to_string(posting) if posting.units is not None else ""
131
+
132
+ if posting.price is not None:
133
+ position_str += f" @ {to_string(posting.price)}"
134
+
135
+ ret: dict[str, Any] = {"account": posting.account, "amount": position_str}
136
+ if posting.meta:
137
+ ret["meta"] = copy(posting.meta)
138
+ return ret
139
+
140
+
141
+ _DUMMY_DATE = datetime.date(2000, 1, 1)
142
+
143
+
144
+ def deserialise_posting(posting: Any) -> Posting:
145
+ """Parse JSON to a Beancount Posting."""
146
+ amount = posting.get("amount", "")
147
+ entries, errors, _ = load_string(
148
+ f'2000-01-01 * "" ""\n Assets:Account {amount}',
149
+ )
150
+ # Raise error if:
151
+ # - No entries were parsed at all
152
+ # - Amount was provided but there's a parse error (not inference warning)
153
+ has_parse_error = any("parse error" in str(e.message).lower() for e in errors)
154
+ if not entries or (amount and has_parse_error):
155
+ raise InvalidAmountError(amount)
156
+ txn = entries[0]
157
+ if not isinstance(txn, Transaction): # pragma: no cover
158
+ msg = "Expected transaction"
159
+ raise TypeError(msg)
160
+ pos = txn.postings[0]
161
+ # Strip dummy date from cost if present (booking assigns transaction date)
162
+ cost = pos.cost
163
+ if cost is not None and getattr(cost, "date", None) == _DUMMY_DATE:
164
+ cost = replace(cost, date=None) # type: ignore[type-var]
165
+ return replace(
166
+ pos,
167
+ account=posting["account"],
168
+ meta=posting.get("meta", {}) or None,
169
+ cost=cost,
170
+ )
171
+
172
+
173
+ def deserialise(json_entry: Any) -> Directive:
174
+ """Parse JSON to a Beancount entry.
175
+
176
+ Args:
177
+ json_entry: The entry.
178
+
179
+ Raises:
180
+ KeyError: if one of the required entry fields is missing.
181
+ RustfavaAPIError: if the type of the given entry is not supported.
182
+ """
183
+ date = parse_date(json_entry.get("date", ""))[0]
184
+ if not isinstance(date, datetime.date):
185
+ msg = "Invalid entry date."
186
+ raise RustfavaAPIError(msg)
187
+ if json_entry["t"] == "Transaction":
188
+ postings = [deserialise_posting(pos) for pos in json_entry["postings"]]
189
+ return create.transaction(
190
+ meta=json_entry["meta"],
191
+ date=date,
192
+ flag=json_entry.get("flag", ""),
193
+ payee=json_entry.get("payee", ""),
194
+ narration=json_entry["narration"] or "",
195
+ tags=frozenset(json_entry["tags"]),
196
+ links=frozenset(json_entry["links"]),
197
+ postings=postings,
198
+ )
199
+ if json_entry["t"] == "Balance":
200
+ raw_amount = json_entry["amount"]
201
+ amount = create.amount(
202
+ Decimal(raw_amount["number"]), raw_amount["currency"]
203
+ )
204
+
205
+ return create.balance(
206
+ meta=json_entry["meta"],
207
+ date=date,
208
+ account=json_entry["account"],
209
+ amount=amount,
210
+ )
211
+ if json_entry["t"] == "Note":
212
+ comment = json_entry["comment"].replace('"', "")
213
+ return create.note(
214
+ meta=json_entry["meta"],
215
+ date=date,
216
+ account=json_entry["account"],
217
+ comment=comment,
218
+ )
219
+ msg = "Unsupported entry type."
220
+ raise RustfavaAPIError(msg)