Changeset - 73cd942a4606
[Not reviewed]
1 8 1
Brett Smith - 6 years ago 2018-04-03 20:17:23
brettcsmith@brettcsmith.org
hooks: Ledger output hook uses its own config section.
10 files changed with 120 insertions and 138 deletions:
0 comments (0 inline, 0 general)
import2ledger/config.py
Show inline comments
...
 
@@ -74,20 +74,6 @@ class Configuration:
 
            '--output-path', '-O', metavar='PATH',
 
            help="Path of file to append entries to, or '-' for stdout (default).",
 
        )
 
        out_args.add_argument(
 
            '--signed-currency', '--sign', metavar='CODE',
 
            action='append', dest='signed_currencies',
 
            help="Currency code to use currency sign for in Ledger entry amounts. "
 
            "Can be specified multiple times.",
 
        )
 
        out_args.add_argument(
 
            '--signed-currency-format', '--sign-format', '-S', metavar='FORMAT',
 
            help="Unicode number pattern to use for signed currencies in Ledger entry amounts",
 
        )
 
        out_args.add_argument(
 
            '--unsigned-currency-format', '--unsign-format', '-U', metavar='FORMAT',
 
            help="Unicode number pattern to use for unsigned currencies in Ledger entry amounts",
 
        )
 

	
 
        return parser
 

	
...
 
@@ -97,10 +83,6 @@ class Configuration:
 
            defaults={
 
                'loglevel': 'WARNING',
 
                'output_path': '-',
 
                'signed_currencies': ','.join(babel.numbers.get_territory_currencies(
 
                    self.LOCALE.territory, start_date=self.TODAY)),
 
                'signed_currency_format': '¤#,##0.###;¤-#,##0.###',
 
                'unsigned_currency_format': '#,##0.### ¤¤',
 
            })
 

	
 
    def _read_conffiles(self):
import2ledger/hooks/ledger_entry.py
Show inline comments
...
 
@@ -277,46 +277,52 @@ class LedgerEntryHook:
 

	
 
    def __init__(self, config):
 
        self.config = config
 
        self.config_section = config.get_section('Ledger output')
 
        if not any(value and not value.isspace()
 
                   for key, value in self.config_section.items()
 
                   if key.endswith(' ledger entry')):
 
            raise errors.NotConfiguredError("no Ledger entries in config", None)
 
        try:
 
            signed_currencies = (code.strip().upper() for code in
 
                                 self.config_section['signed currencies'].split(','))
 
        except KeyError:
 
            territory = self.config.LOCALE.territory
 
            if territory is None:
 
                signed_currencies = []
 
            else:
 
                signed_currencies = babel.numbers.get_territory_currencies(
 
                    territory, start_date=self.config.TODAY)
 
        self.template_kwargs = {
 
            'date_fmt': self.config_section['date format'],
 
            'signed_currencies': frozenset(signed_currencies),
 
            'signed_currency_fmt': self.config_section.get(
 
                'signed currency format', Template.SIGNED_CURRENCY_FMT),
 
            'unsigned_currency_fmt': self.config_section.get(
 
                'unsigned currency format', Template.UNSIGNED_CURRENCY_FMT),
 
        }
 

	
 
    @staticmethod
 
    @functools.lru_cache()
 
    def _load_template(config, section_name, config_key):
 
        section_config = config.get_section(section_name)
 
        try:
 
            template_s = section_config[config_key]
 
        except KeyError:
 
            raise errors.UserInputConfigurationError(
 
                "Ledger template not defined in [{}]".format(section_name),
 
                config_key,
 
            )
 
        return Template(
 
            template_s,
 
            date_fmt=section_config['date format'],
 
            signed_currencies=[code.strip().upper() for code in section_config['signed_currencies'].split(',')],
 
            signed_currency_fmt=section_config['signed_currency_format'],
 
            unsigned_currency_fmt=section_config['unsigned_currency_format'],
 
            template_name=config_key,
 
        )
 
    def _load_template(template_s, signed_currencies,
 
                       date_fmt,
 
                       signed_currency_fmt,
 
                       unsigned_currency_fmt,
 
                       template_name):
 
        return Template(**locals())
 

	
 
    def run(self, entry_data):
 
        try:
 
            template_key = entry_data['ledger template']
 
            template_name = entry_data['ledger template']
 
        except KeyError:
 
            template_key = '{} {} ledger entry'.format(
 
            template_name = '{} {} ledger entry'.format(
 
                strparse.rslice_words(entry_data['importer_module'], -1, '.', 1),
 
                entry_data['importer_class'][:-8].lower(),
 
            )
 
        try:
 
            template = self._load_template(self.config, None, template_key)
 
        except errors.UserInputConfigurationError as error:
 
            if error.strerror.startswith('Ledger template not defined '):
 
                have_template = False
 
            else:
 
                raise
 
        else:
 
            have_template = not template.is_empty()
 
        if not have_template:
 
            logger.warning("no Ledger template defined as %r", template_key)
 
        template_s = self.config_section.get(template_name, '')
 
        template = self._load_template(
 
            template_s, template_name=template_name, **self.template_kwargs)
 
        if template.is_empty():
 
            logger.warning("no Ledger template defined as %r", template_name)
 
        else:
 
            with self.config.open_output_file() as out_file:
 
                print(template.render(entry_data), file=out_file, end='')
tests/__init__.py
Show inline comments
 
import collections
 
import configparser
 
import datetime
 
import decimal
 
import operator
 
import pathlib
 
import re
 

	
 
import babel.core
 
from import2ledger import __main__ as i2lmain
 

	
 
decimal.setcontext(i2lmain.decimal_context())
 

	
 
DATA_DIR = pathlib.Path(__file__).with_name('data')
 
START_DATE = datetime.date.today()
 

	
 
def normalize_whitespace(s):
 
    return re.sub(r'(\t| {3,})', '  ', s)
 

	
 
class Config:
 
    LOCALE = babel.core.Locale('en_US_POSIX')
 
    TODAY = START_DATE
 

	
 
    def __init__(self, options_dict=None):
 
        self.config = configparser.ConfigParser(
 
            defaults={
...
 
@@ -29,3 +36,5 @@ class Config:
 
            return self.config[section_name]
 
        except KeyError:
 
            return self.config[configparser.DEFAULTSECT]
 

	
 
    __getitem__ = property(operator.attrgetter('config.__getitem__'))
tests/data/templates.ini
Show inline comments
 
deleted file
tests/data/templates.yml
Show inline comments
 
new file 100644
 
---
 
DEFAULT:
 
  date format: "%%Y-%%m-%%d"
 
Ledger output:
 
  signed currencies: USD, CAD
 
  signed currency format: "¤#,##0.###"
 
  Simplest ledger entry: |
 
    Accrued:Accounts Receivable  {amount}
 
    Income:Donations  -{amount}
 
  FiftyFifty ledger entry: |
 
    Accrued:Accounts Receivable  {amount}
 
    Income:Donations  -.5 * {amount}
 
    Income:Sales  -.5*{amount}
 
  Complex ledger entry: |
 
    ;Tag: Value
 
    ;TransactionID: {txid}
 
    Accrued:Accounts Receivable  {amount}
 
    ;Entity: Supplier
 
    Income:Donations:{program}    -.955*  {amount}
 
    ;Program: {program}
 
    ;Entity: {entity}
 
    Income:Donations:General     -.045  * {amount}
 
    ;Entity: {entity}
 
  Multivalue ledger entry: |
 
    Expenses:Taxes  {tax}
 
    ;TaxAuthority: IRS
 
    Accrued:Accounts Receivable  {amount} - {tax}
 
    Income:RBI         -.1*{amount}
 
    Income:Donations   -.9*{amount}
 
  Custom Payee ledger entry: |
 
    {custom_date}  {payee} - Custom
 
    Accrued:Accounts Receivable  {amount}
 
    Income:Donations  -{amount}
 
  Multisplit ledger entry: |
 
    Assets:Cash  {amount}
 
    Income:Sales  -{amount} + {item_sales}
 
    ; :NonItem:
 
    Income:Sales  -{item_sales}
 
    ; :Item:
 
  Empty ledger entry: ""
tests/data/test_main.ini
Show inline comments
 
[DEFAULT]
 
date format = %%Y/%%m/%%d
 
loglevel = critical
 
signed_currencies = USD
 
signed currencies = USD
 

	
 
[One]
 
[Ledger output]
 
patreon cardfees ledger entry =
 
 Accrued:Accounts Receivable  -{amount}
 
 Expenses:Fees:Credit Card  {amount}
tests/test_config.py
Show inline comments
...
 
@@ -5,14 +5,12 @@ import logging
 
import os
 
import pathlib
 

	
 
START_DATE = datetime.date.today()
 

	
 
from unittest import mock
 

	
 
import pytest
 
from import2ledger import config, errors, strparse
 

	
 
from . import DATA_DIR
 
from . import DATA_DIR, START_DATE
 

	
 
def config_from_file(path, arglist=[], stdout=None, stderr=None, *, init=True):
 
    path = pathlib.Path(path)
tests/test_hooks.py
Show inline comments
...
 
@@ -17,7 +17,9 @@ def test_load_no_config_needed():
 

	
 
def test_load_all():
 
    config_dict = date_hooks.DateHookTestBase().new_config()
 
    return _run_order_test(Config(config_dict), [
 
    config = Config(config_dict)
 
    config['DEFAULT']['test ledger entry'] = 'Income  {amount}'
 
    return _run_order_test(config, [
 
        default_date.DefaultDateHook,
 
        add_entity.AddEntityHook,
 
        filter_by_date.FilterByDateHook,
tests/test_hooks_ledger_entry.py
Show inline comments
 
import collections
 
import configparser
 
import contextlib
 
import datetime
 
import decimal
...
 
@@ -7,19 +6,31 @@ import io
 
import pathlib
 

	
 
import pytest
 
import yaml
 
from import2ledger import errors
 
from import2ledger.hooks import ledger_entry
 

	
 
from . import DATA_DIR, normalize_whitespace
 
from . import DATA_DIR, normalize_whitespace, Config as BaseConfig
 

	
 
DATE = datetime.date(2015, 3, 14)
 
with pathlib.Path(DATA_DIR, 'templates.yml').open() as conffile:
 
    _config_dict = yaml.load(conffile)
 

	
 
class Config(BaseConfig):
 
    def __init__(self, options_dict=_config_dict):
 
        super().__init__(options_dict)
 
        self.stdout = io.StringIO()
 

	
 
config = configparser.ConfigParser(comment_prefixes='#')
 
with pathlib.Path(DATA_DIR, 'templates.ini').open() as conffile:
 
    config.read_file(conffile)
 
    @contextlib.contextmanager
 
    def open_output_file(self):
 
        yield self.stdout
 

	
 

	
 
DATE = datetime.date(2015, 3, 14)
 

	
 
def template_from(section_name, *args, **kwargs):
 
    return ledger_entry.Template(config[section_name]['template'], *args, **kwargs)
 
def template_from(template_name, *args, **kwargs):
 
    section = Config().get_section('Ledger output')
 
    template_s = section['{} ledger entry'.format(template_name)]
 
    return ledger_entry.Template(template_s, *args, **kwargs)
 

	
 
def template_vars(payee, amount, currency='USD', date=DATE, other_vars=None):
 
    call_vars = {
...
 
@@ -27,7 +38,6 @@ def template_vars(payee, amount, currency='USD', date=DATE, other_vars=None):
 
        'currency': currency,
 
        'date': date,
 
        'payee': payee,
 
        'ledger template': 'template',
 
    }
 
    if other_vars is None:
 
        return call_vars
...
 
@@ -204,24 +214,12 @@ def test_bad_amount_expression(amount_expr):
 
    with pytest.raises(errors.UserInputError):
 
        ledger_entry.Template(" Income  " + amount_expr)
 

	
 
class Config:
 
    def __init__(self, use_section):
 
        self.section_name = use_section
 
        self.stdout = io.StringIO()
 

	
 
    @contextlib.contextmanager
 
    def open_output_file(self):
 
        yield self.stdout
 

	
 
    def get_section(self, name=None):
 
        return config[self.section_name]
 

	
 

	
 
def run_hook(entry_data, config_section):
 
    hook_config = Config(config_section)
 
    hook = ledger_entry.LedgerEntryHook(hook_config)
 
def run_hook(entry_data, template_name):
 
    config = Config()
 
    entry_data['ledger template'] = '{} ledger entry'.format(template_name)
 
    hook = ledger_entry.LedgerEntryHook(config)
 
    assert hook.run(entry_data) is None
 
    stdout = hook_config.stdout.getvalue()
 
    stdout = config.stdout.getvalue()
 
    return normalize_whitespace(stdout).splitlines()
 

	
 
def test_hook_renders_template():
...
 
@@ -242,3 +240,7 @@ def test_hook_handles_template_undefined():
 
    entry_data = template_vars('DD', 1)
 
    assert not run_hook(entry_data, 'Nonexistent')
 

	
 
def test_unconfigured():
 
    config = Config({})
 
    with pytest.raises(errors.NotConfiguredError):
 
        ledger_entry.LedgerEntryHook(config)
tests/test_main.py
Show inline comments
...
 
@@ -54,10 +54,7 @@ def path_vars(path):
 

	
 
def test_fees_import():
 
    source_path = pathlib.Path(DATA_DIR, 'PatreonEarnings.csv')
 
    arglist = ARGLIST + [
 
        '-c', 'One',
 
        source_path.as_posix(),
 
    ]
 
    arglist = ARGLIST + [source_path.as_posix()]
 
    exitcode, stdout, _ = run_main(arglist)
 
    assert exitcode == 0
 
    actual = list(format_entries(stdout))
...
 
@@ -67,7 +64,6 @@ def test_fees_import():
 
def test_date_range_import():
 
    source_path = pathlib.Path(DATA_DIR, 'PatreonEarnings.csv')
 
    arglist = ARGLIST + [
 
        '-c', 'One',
 
        '--date-range', '2017/10/01-',
 
        source_path.as_posix(),
 
    ]
0 comments (0 inline, 0 general)