diff --git a/CODE.rst b/CODE.rst new file mode 100644 index 0000000..9a67339 --- /dev/null +++ b/CODE.rst @@ -0,0 +1,93 @@ +import2ledger code structure +============================ + +Concepts +-------- + +The main workflow of the program passes through three different types with different responsibilities. + +Entry data +~~~~~~~~~~ + +Data for an output entry is kept and passed around in a dict with the following contents: + +``date`` + A datetime.date object (if this is omitted, the ``default_date`` hook will fill in the default date from the user's configuration) + +``payee`` + A string + +``amount`` + A string or other object that can be safely converted to a decimal.Decimal + +``currency`` + A string with a three-letter code, uppercase, identifying the transaction currency + +It can optionally include additional keys for use as template variables. + +Importers +~~~~~~~~~ + +At a high level, importers read a source file, and generate data for output entries. + +Class method ``can_handle(source_file)`` + Returns true if the importer can generate entries from the given source file object, false otherwise. + +``__init__(source_file)`` + Initializes an importer to generate entries from the given source file object. + +``__iter__()`` + Returns a iterator of entry data dicts. + +Class attribute ``TEMPLATE_KEY`` + A string with the full key to load the corresponding template from the user's configuration (e.g., ``'template patreon income'``). + +Hooks +~~~~~ + +Hooks make arbitrary transformations to entry data dicts. Every entry data dict generated by an importer is run through every hook before being output. + +``__init__(config)`` + Initializes the hook with the user's configuration. + +``run(entry_data)`` + This method makes the hook's transformations to the entry data dict, if any. If this method clears the entry data dict, that entry will not be output. + +Templates +~~~~~~~~~ + +Templates receive entry data dicts and format them into final output entries. + +``__init__(template_str)`` + Initializes the template from a single string, as read from the user's configuration. + +``render(payee, amount, currency, date=None, **template_vars)`` + Returns a string with the output entry, using the given entry data. This is expected to be called as ``render(**entry_data)``; the arguments take the same types as entry data dicts. + +Loading importers and hooks +--------------------------- + +Importers and hooks are both loaded and found dynamically when the program starts. This makes it easy to extend the program: you just need to write the class following the established pattern, no registration needed. + +import2ledger finds importers by looking at all ``.py`` files in the ``importers/`` directory, skipping files whose names start with ``.`` (hidden) or ``_`` (private). It tries to import that file as a module. If it succeeds, it looks for things in the module named ``*Importer``, and adds those to the list of importers. + +Hooks follow the same pattern, searching the ``hooks/`` directory and looking for things named ``*Hook``. + +Technically this is done by ``importers.load_all()`` and ``hooks.load_all()`` functions, but most of the code to do this is in the ``util`` module. + +Main loop +--------- + +At a high level, import2ledger handles each input file this way:: + + usable_importers = importers where can_handle(input_file) returns true + for importer_class in usable_importers: + template = built from importer_class.TEMPLATE_KEY + input_file.seek(0) + for entry_data in importer_class(input_file): + for hook in all_hooks: + hook.run(entry_data) + if entry_data: + template.render(**entry_data) + +Note in particular that multiple importers can handle the same input file. This helps support inputs like Patreon's earnings CSV, where completely different transactions are generated from the same source.