Changeset - cc8c03ab626a
[Not reviewed]
0 5 0
Brett Smith - 6 years ago 2017-12-19 14:13:44
brettcsmith@brettcsmith.org
main: Provide template variables about the file being imported.
5 files changed with 55 insertions and 18 deletions:
0 comments (0 inline, 0 general)
README.rst
Show inline comments
...
 
@@ -73,12 +73,19 @@ amount             The total amount of the transaction, as a simple decimal
 
currency           The three-letter code for the transaction currency
 
------------------ ----------------------------------------------------------
 
date               The date of the transaction, in your configured output
 
                   format
 
------------------ ----------------------------------------------------------
 
payee              The name of the transaction payee
 
------------------ ----------------------------------------------------------
 
source_abspath     The absolute path of the file being imported
 
------------------ ----------------------------------------------------------
 
source_name        The filename of the file being imported
 
------------------ ----------------------------------------------------------
 
source_path        The path of the file being imported, as specified on the
 
                   command line
 
================== ==========================================================
 

	
 
Specific importers and hooks may provide additional variables.
 

	
 
Supported templates
 
~~~~~~~~~~~~~~~~~~~
import2ledger/__main__.py
Show inline comments
 
import collections
 
import contextlib
 
import logging
 
import sys
 

	
 
from . import config, errors, hooks, importers
 

	
...
 
@@ -10,13 +11,15 @@ class FileImporter:
 
    def __init__(self, config, stdout):
 
        self.config = config
 
        self.importers = list(importers.load_all())
 
        self.hooks = [hook(config) for hook in hooks.load_all()]
 
        self.stdout = stdout
 

	
 
    def import_file(self, in_file):
 
    def import_file(self, in_file, in_path=None):
 
        if in_path is None:
 
            in_path = pathlib.Path(in_file.name)
 
        importers = []
 
        for importer in self.importers:
 
            in_file.seek(0)
 
            if importer.can_import(in_file):
 
                try:
 
                    template = self.config.get_template(importer.TEMPLATE_KEY)
...
 
@@ -28,12 +31,17 @@ class FileImporter:
 
                else:
 
                    have_template = not template.is_empty()
 
                if have_template:
 
                    importers.append((importer, template))
 
        if not importers:
 
            raise errors.UserInputFileError("no importers available", in_file.name)
 
        source_vars = {
 
            'source_abspath': in_path.absolute().as_posix(),
 
            'source_name': in_path.name,
 
            'source_path': in_path.as_posix(),
 
        }
 
        with contextlib.ExitStack() as exit_stack:
 
            output_path = self.config.get_output_path()
 
            if output_path is None:
 
                out_file = self.stdout
 
            else:
 
                out_file = exit_stack.enter_context(output_path.open('a'))
...
 
@@ -45,21 +53,22 @@ class FileImporter:
 
                    for hook in self.hooks:
 
                        hook.run(entry_data)
 
                        if entry_data['_hook_cancel']:
 
                            break
 
                    else:
 
                        del entry_data['_hook_cancel']
 
                        print(template.render(**entry_data), file=out_file, end='')
 
                        render_vars = collections.ChainMap(entry_data, source_vars)
 
                        print(template.render(render_vars), file=out_file, end='')
 

	
 
    def import_path(self, in_path):
 
        if in_path is None:
 
            raise errors.UserInputFileError("only seekable files are supported", '<stdin>')
 
        with in_path.open(errors='replace') as in_file:
 
            if not in_file.seekable():
 
                raise errors.UserInputFileError("only seekable files are supported", in_path)
 
            return self.import_file(in_file)
 
            return self.import_file(in_file, in_path)
 

	
 
    def import_paths(self, path_seq):
 
        for in_path in path_seq:
 
            try:
 
                retval = self.import_path(in_path)
 
            except (OSError, errors.UserInputError) as error:
tests/data/test_main.ini
Show inline comments
...
 
@@ -5,8 +5,10 @@ signed_currencies = USD
 

	
 
[One]
 
template patreon cardfees =
 
 Accrued:Accounts Receivable  -{amount}
 
 Expenses:Fees:Credit Card  {amount}
 
template patreon svcfees =
 
 ;SourcePath: {source_abspath}
 
 ;SourceName: {source_name}
 
 Accrued:Accounts Receivable  -{amount}
 
 Expenses:Fundraising  {amount}
tests/data/test_main_fees_import.ledger
Show inline comments
 
2017/09/01 Patreon
 
  Accrued:Accounts Receivable  $-52.47
 
  Expenses:Fees:Credit Card  $52.47
 

	
 
2017/09/01 Patreon
 
  Accrued:Accounts Receivable  $-61.73
 
  Expenses:Fundraising  $61.73
 

	
 
2017/10/01 Patreon
 
  Accrued:Accounts Receivable  $-99.47
 
  Expenses:Fees:Credit Card  $99.47
 

	
 
2017/09/01 Patreon
 
  ;SourcePath: {source_abspath}
 
  ;SourceName: {source_name}
 
  Accrued:Accounts Receivable  $-61.73
 
  Expenses:Fundraising  $61.73
 

	
 
2017/10/01 Patreon
 
  ;SourcePath: {source_abspath}
 
  ;SourceName: {source_name}
 
  Accrued:Accounts Receivable  $-117.03
 
  Expenses:Fundraising  $117.03
tests/test_main.py
Show inline comments
...
 
@@ -27,38 +27,53 @@ def iter_entries(in_file):
 
            lines = []
 
        else:
 
            lines.append(line)
 
    if lines:
 
        yield ''.join(lines)
 

	
 
def entries2set(in_file):
 
    return set(normalize_whitespace(e) for e in iter_entries(in_file))
 
def format_entry(entry_s, format_vars):
 
    return normalize_whitespace(entry_s).format_map(format_vars)
 

	
 
def expected_entries(path):
 
def format_entries(source, format_vars=None):
 
    if format_vars is None:
 
        format_vars = {}
 
    return (format_entry(e, format_vars) for e in iter_entries(source))
 

	
 
def expected_entries(path, format_vars=None):
 
    path = pathlib.Path(path)
 
    if not path.is_absolute():
 
        path = DATA_DIR / path
 
    with path.open() as in_file:
 
        return entries2set(in_file)
 
        return list(format_entries(in_file, format_vars))
 

	
 
def path_vars(path):
 
    return {
 
        'source_abspath': str(path),
 
        'source_name': path.name,
 
        'source_path': str(path),
 
    }
 

	
 
def test_fees_import():
 
    source_path = pathlib.Path(DATA_DIR, 'PatreonEarnings.csv')
 
    arglist = ARGLIST + [
 
        '-c', 'One',
 
        pathlib.Path(DATA_DIR, 'PatreonEarnings.csv').as_posix(),
 
        source_path.as_posix(),
 
    ]
 
    exitcode, stdout, _ = run_main(arglist)
 
    assert exitcode == 0
 
    actual = entries2set(stdout)
 
    assert actual == expected_entries('test_main_fees_import.ledger')
 
    actual = list(format_entries(stdout))
 
    expected = expected_entries('test_main_fees_import.ledger', path_vars(source_path))
 
    assert actual == expected
 

	
 
def test_date_range_import():
 
    source_path = pathlib.Path(DATA_DIR, 'PatreonEarnings.csv')
 
    arglist = ARGLIST + [
 
        '-c', 'One',
 
        '--date-range', '2017/10/01-',
 
        pathlib.Path(DATA_DIR, 'PatreonEarnings.csv').as_posix(),
 
        source_path.as_posix(),
 
    ]
 
    exitcode, stdout, _ = run_main(arglist)
 
    assert exitcode == 0
 
    actual = entries2set(stdout)
 
    expected = {entry for entry in expected_entries('test_main_fees_import.ledger')
 
                if entry.startswith('2017/10/')}
 
    actual = list(format_entries(stdout))
 
    valid = expected_entries('test_main_fees_import.ledger', path_vars(source_path))
 
    expected = [entry for entry in valid if entry.startswith('2017/10/')]
 
    assert actual == expected
0 comments (0 inline, 0 general)