Beangulp

Martin Blais,2021 年 1 月

Importing data for Beancount has been supported originally by the LedgerHub project along with a library of importers, then reintegrated as a pure framework library (with some examples) in the Beancount repo as beancount.ingest, and now we're splitting up that repository again and will factor out the importing framework to another repo for easier maintenance and evolution. This document lays out some of the changes desired in this new version.

新仓库

新仓库将位于

http://github.com/beancount/beangulp

Beangulp 将仅兼容 v3 分支的最新 Beancount 发布版本。

Beancount v3 在初期预计会快速演进,因此为了减轻早期使用者的负担,应谨慎使用版本号递增和版本化依赖。理想情况下,Beangulp 应仅依赖 Beancount 的当前次版本,例如,若发布 Beancount 3.0.0,则 Beangulp 应声明

install_requires: beancount >3.0, <3.1

参见 setuptools 文档PEP-440

状态

截至 2022 年 1 月,本提案的大部分内容已完成并实现。(感谢 Daniele Nicolodi 完成了大部分工作。)

变更

仅库

当前实现允许用户使用

  1. bean-identify、bean-extract 和 bean-file 工具对一个作为 Python 代码评估的“配置文件”进行操作,或

  2. 创建脚本并调用单个端点以实现子命令。

为了实现这一点,使用了一个极其复杂的_trampoline_机制,将评估过程跳转回同一段代码。我必须承认,即使是我自己写的这段代码,每次需要进去修改时,都会让我感到头疼。此外,这也使得用户难以在导入过程中添加自定义的前置/后置操作。

下一个版本将仅支持(2)。bean-identify、bean-extract 和 bean-file 程序都将被移除。用户需要自行编写脚本。

单个文件

目前,每个导入器由两个文件组成:实现文件和一个关联的测试文件,例如。

soandso_bank.py
soandso_bank_test.py

测试文件很小,通常调用库函数来查找模型文件和预期输出。由于几乎没有实际的测试代码,我们希望可以将测试调用整合到一个单独的 Python 文件中。

将在导入器实现中新增一个端点,用于定义测试。

自运行

此外,这个新函数还应与用于构建配置脚本的函数完全相同。换句话说,导入器的 main() 函数应当等同于使用测试配置参数调用该导入器时的行为。

这将允许用户直接在特定文件集上“运行导入器”,而无需定义配置,只需使用测试配置即可,例如:

soandso_bank.py extract ~/Downloads/transactions.csv

拥有这一功能,使人们能够轻松共享单个文件并立即测试,无需创建配置文件或主程序。

我认为,除了在导入器定义文件中加入类似以下内容外,没有其他简单干净的实现方式:

if __name__ == '__main__':

            main = Ingest([SoAndSoBankImporter()])
            main()

这目前无需修改即可正常工作。不过,目前大多数导入器仍需要一些配置才能运行(我不确定,因为我并未使用 Beancount 分发的或广泛使用的任何导入器)。

测试 子命令 & 生成

由于导入器是可运行的,并且为 pytest 定义了测试用例,我们还应添加一个名为 "test" 的子命令,以补充 "identify"、"extract" 和 "file"。这个入口也是替换 --generate 选项的理想场所,该选项曾需要丑陋的 pytestconfig 注入;取而代之的是,我们应自行实现,并新增一个子命令:"genexpect",用于生成测试所需的预期文件。

接口将变为:

soandso_bank.py identify ~/Downloads/
soandso_bank.py extract ~/Downloads/
soandso_bank.py file ~/Downloads/ ~/documents
soandso_bank.py test ~/documents/testdocs
soandso_bank.py generate ~/Downloads/transactions.csv

这样我们可以移除对 pytestconfig 的依赖注入,同时简化单元测试的逻辑——此前测试代码必须同时处理检查与生成两种场景。这将使代码更简洁。

单个预期文件

导入器的预期输出目前存储在多个带有 .extract、.file_name、.file_date 等后缀的文件中。如果我们把所有输出都集中在一个文件中,"genexpect" 子命令就可以将所有内容输出到标准输出。这非常方便。

myimporter.py

myimporter.beancount

利用 Beancount 语法存储 "file_name()"、"file_date()" 等输出的预期值。将这些值存储在 Event 或 Custom 指令中,并由测试代码进行断言验证。测试目录的新内容应为简单的配对:(a) 原始下载文件 和 (b) 预期输出文件,其中包含交易记录以及所有其他方法的输出。

重复项识别

这从未真正正常工作过。我认为,如果为每个导入器单独实现此功能——即让每个导入器自行定义如何识别重复项,例如,假设唯一交易 ID 已被用作链接以消除歧义——我们可以做得更清晰。

理想情况下,每个导入器都应在自身中指定重复 ID 检测逻辑。它可以调用一个更通用但可靠性较低的方法,而该代码应位于 Beangulp 中。

CSV 工具

我有许多非常方便的工具,用于处理从丑陋 CSV 下载中提取的数据。CSV 下载通常包含多个表格和不同类型的数据,因此通常需要在这些文件之上进行更高层次的处理。

我的代码到处都是类似这样的逻辑。这值得封装成一个优秀的库。此外,我在 Baskets 项目中还有一个不错的表格处理库,但尚未得到应有的质量优化。

清理我的小型表格库,并将其与所有 CSV 工具合并。将它们放入 beangulp 中,甚至可以考虑将其独立成一个项目,包含一个 CSV 导入器。"CSV 世界。"

在发现 petl 之后,我不确定这是否还那么重要。我们可以依赖 petl。

缓存

在处理大文件的转换任务时,若能提供缓存机制以避免重复运行将非常有用。转换后的小型数据可被缓存并重新加载,从而避免重复执行昂贵的转换过程。一个难点在于,所需的转换取决于导入器的配置,而每个导入器并不了解其他导入器的存在。

应将所有命令行参数以及文件内容的头部至少进行哈希处理。这个库可以相当独立于 Beancount。

API 变更

  • "file_date()" 不够清晰;"get_filing_date()" 更合适。

  • extract() 的额外参数与其他所有方法相比显得不一致。能否找到更好的方式?

  • 我对自己的小缓存文件包装器("cache")并不是 100%满意。将其替换为仅基于磁盘的缓存。

自动插入

新代码应具备的一个非常便捷且易于实现的功能是:在文件中特定字符串标记之前,自动将提取结果插入到现有账本中。将此功能作为库的一部分,作为存储导入器输出的替代方案,例如:

$ ./myimport.py extract | bean-insert ledger.beancount Amex

这也可以作为 "extract" 的一个标志选项

$ ./myimport.py extract -f ledger.beancount -F Amex