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,157 @@
|
|
|
1
|
+
"""Some small utility functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import gettext
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
import time
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
from unicodedata import normalize
|
|
13
|
+
from urllib.parse import quote
|
|
14
|
+
|
|
15
|
+
from flask import abort
|
|
16
|
+
from flask import send_file
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from collections.abc import Iterable
|
|
21
|
+
from collections.abc import Mapping
|
|
22
|
+
from typing import Any
|
|
23
|
+
from typing import ParamSpec
|
|
24
|
+
from typing import TypeVar
|
|
25
|
+
from wsgiref.types import StartResponse
|
|
26
|
+
from wsgiref.types import WSGIEnvironment
|
|
27
|
+
|
|
28
|
+
from babel import Locale
|
|
29
|
+
from flask.wrappers import Response
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def filter_api_changed(record: logging.LogRecord) -> bool: # pragma: no cover
|
|
33
|
+
"""Filter out LogRecords for requests that poll for changes."""
|
|
34
|
+
return "/api/changed HTTP" not in record.getMessage()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def setup_logging() -> None:
|
|
38
|
+
"""Set up logging for rustfava."""
|
|
39
|
+
logging.basicConfig(level=logging.WARNING, format="%(message)s")
|
|
40
|
+
logging.getLogger("werkzeug").addFilter(filter_api_changed)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def setup_debug_logging() -> None: # pragma: no cover
|
|
44
|
+
"""Set up debug level logging for rustfava."""
|
|
45
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
46
|
+
logging.getLogger("watchfiles").setLevel(logging.INFO)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_translations(locale: Locale) -> str | None:
|
|
50
|
+
"""Check whether rustfava has translations for the locale.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
locale: The locale to search for
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The path to the found translations or None if none matched.
|
|
57
|
+
"""
|
|
58
|
+
translations_dir = Path(__file__).parent.parent / "translations"
|
|
59
|
+
return gettext.find("messages", str(translations_dir), [str(locale)])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
63
|
+
Item = TypeVar("Item")
|
|
64
|
+
P = ParamSpec("P")
|
|
65
|
+
T = TypeVar("T")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def listify(func: Callable[P, Iterable[Item]]) -> Callable[P, list[Item]]:
|
|
69
|
+
"""Make generator function return a list (decorator)."""
|
|
70
|
+
|
|
71
|
+
@wraps(func)
|
|
72
|
+
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> list[Item]:
|
|
73
|
+
return list(func(*args, **kwargs))
|
|
74
|
+
|
|
75
|
+
return _wrapper
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def timefunc(
|
|
79
|
+
func: Callable[P, T],
|
|
80
|
+
) -> Callable[P, T]: # pragma: no cover - only used for debugging so far
|
|
81
|
+
"""Time function for debugging (decorator)."""
|
|
82
|
+
|
|
83
|
+
@wraps(func)
|
|
84
|
+
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
85
|
+
start = time.time()
|
|
86
|
+
result = func(*args, **kwargs)
|
|
87
|
+
end = time.time()
|
|
88
|
+
print(f"Ran {func.__name__} in {end - start}") # noqa: T201 # ty:ignore[unresolved-attribute]
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
return _wrapper
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def next_key(basekey: str, keys: Mapping[str, Any]) -> str:
|
|
95
|
+
"""Return the next unused key for basekey in the supplied dictionary.
|
|
96
|
+
|
|
97
|
+
The first try is `basekey`, followed by `basekey-2`, `basekey-3`, etc
|
|
98
|
+
until a free one is found.
|
|
99
|
+
"""
|
|
100
|
+
if basekey not in keys:
|
|
101
|
+
return basekey
|
|
102
|
+
i = 2
|
|
103
|
+
while f"{basekey}-{i}" in keys:
|
|
104
|
+
i += 1
|
|
105
|
+
return f"{basekey}-{i}"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def slugify(string: str) -> str:
|
|
109
|
+
"""Slugify a string.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
string: A string.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
A 'slug' of the string suitable for URLs. Retains non-ascii
|
|
116
|
+
characters.
|
|
117
|
+
|
|
118
|
+
"""
|
|
119
|
+
string = normalize("NFKC", string)
|
|
120
|
+
# remove all non-word characters (except '-')
|
|
121
|
+
string = re.sub(r"[^\s\w-]", "", string).strip().lower()
|
|
122
|
+
# replace spaces (or groups of spaces and dashes) with dashes
|
|
123
|
+
return re.sub(r"[-\s]+", "-", string)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def simple_wsgi(
|
|
127
|
+
_: WSGIEnvironment,
|
|
128
|
+
start_response: StartResponse,
|
|
129
|
+
) -> list[bytes]:
|
|
130
|
+
"""Return an empty response (a simple WSGI app)."""
|
|
131
|
+
start_response("200 OK", [("Content-Type", "text/html")])
|
|
132
|
+
return [b""]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def send_file_inline(filename: str) -> Response:
|
|
136
|
+
"""Send a file inline, including the original filename.
|
|
137
|
+
|
|
138
|
+
Ref: http://test.greenbytes.de/tech/tc2231/.
|
|
139
|
+
|
|
140
|
+
Security: Callers must validate that filename is an allowed path.
|
|
141
|
+
This function adds defense-in-depth with path normalization and containment.
|
|
142
|
+
"""
|
|
143
|
+
# Compute base directory from filename's resolved parent
|
|
144
|
+
base_dir = Path(filename).resolve().parent
|
|
145
|
+
# Normalize target path - only allow basename under resolved parent
|
|
146
|
+
full_path = (base_dir / Path(filename).name).resolve()
|
|
147
|
+
# Enforce containment: path must be within base_dir (403 if escape attempt)
|
|
148
|
+
# Note: With current logic this is unreachable (defense-in-depth)
|
|
149
|
+
if not full_path.is_relative_to(base_dir): # pragma: no cover
|
|
150
|
+
return abort(403)
|
|
151
|
+
# Check file exists (404 if not found)
|
|
152
|
+
if not full_path.is_file():
|
|
153
|
+
return abort(404)
|
|
154
|
+
response: Response = send_file(full_path)
|
|
155
|
+
cont_disp = f"inline; filename*=UTF-8''{quote(full_path.name)}"
|
|
156
|
+
response.headers["Content-Disposition"] = cont_disp
|
|
157
|
+
return response
|