diff --git a/.coveragerc b/.coveragerc index 843b03d..9da8eef 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,4 +3,5 @@ source = pynYNAB [report] -omit = */tests/* \ No newline at end of file +omit = */tests/*, \ + */pynYNAB/__main__.py \ No newline at end of file diff --git a/README.rst b/README.rst index 6b18550..105f6d6 100644 --- a/README.rst +++ b/README.rst @@ -36,38 +36,9 @@ See appropriate `README`_ API Documentation ----------------- -nYNAB is organised around a root object, here it is a nYnabClient object. It is created by giving it a connection object, -which handles the requests to the server (app.youneedabudget.com and its api endpoint /api/v1/catalog), -using the same commands that the Javascript app at app.youneedabudget.com uses internally. +See the wiki 'WIKI'_ for an extended explanation and usage examples -The connection object needs email and password for the nYNAB account - -Once you have created your nYnabClient object, all data should have already been synced up with YNAB's servers. If you -need the updated data from the server, call sync on the nYnabClient. - -All the entity handling is done through the Budget and Catalog objects, they contain collections such -as be_accounts, be_transactions, ce_user_settings, etc. Look into the budget/catalog for the schema. - -In order to write some data to YNAB servers for your budget, you just need to modify those Budget/Catalog -objects then call nYnabobject.push . all the subcollections like ce_budget_transactions support append, remove, etc, -like normal lists. - -push takes an expected_delta argument, this is to safeguard modifying too much of your data on the server by error. -Examples: - -* if you add two matched transactions to be_transactions, then the expected delta is 2 -* If you add a single payee, then the expected delta is 1 - - -I've provided some tested methods e.g. add_account, add_transaction, in the nYnabClient class to -add/delete accounts and transactions as examples. Some actions are not always simple (e.g., to add an account, -you need to add a transfer payee, a starting balance transaction, otherwise the server refuses the request). You're welcome -to contribute new actions e.g. by catching the requests done in the UI with Fiddler to see what's done. - -Caution with add_transaction, it works even for large amount of transactions (tested up to 3000), but please -don't stress test the YNAB servers with it... - -Approach of preventing Harm +Preventing harm to nYnab servers --------------------------- I've taken all precautionary steps so that this python Client can't affect YNAB even if used too widely. @@ -77,3 +48,4 @@ I've taken all precautionary steps so that this python Client can't affect YNAB * It clearly identifies itself by User-Agent > Easy to lock it out if it causes trouble .. _README: https://github.com/rienafairefr/nYNABapi/blob/master/scripts/README.rst +.. _WIKI https://github.com/rienafairefr/pynYNAB/wiki diff --git a/pynYNAB/Client.py b/pynYNAB/Client.py index 1b13406..f7984d2 100644 --- a/pynYNAB/Client.py +++ b/pynYNAB/Client.py @@ -32,6 +32,9 @@ def __init__(self, expected_delta, delta): def msg(self): return self.string % (self.delta, self.expected_delta) + def __repr__(self): + return self.msg + def operation(expected_delta): def operation_decorator(fn): @@ -44,6 +47,21 @@ def wrapped(self, *args, **kwargs): return operation_decorator +class CatalogClient(RootObjClient): + @property + def extra(self): + return dict(user_id=self.client.connection.user_id) + + opname = 'syncCatalogData' + + +class BudgetClient(RootObjClient): + @property + def extra(self): + return dict(calculated_entities_included=False, budget_version_id=self.client.budget_version_id) + + opname = 'syncBudgetData' + class nYnabClient(object): def __init__(self, **kwargs): self.server_entities = {} @@ -63,7 +81,7 @@ def __init__(self, **kwargs): self.starting_device_knowledge = 0 self.ending_device_knowledge = 0 - engine = kwargs.get('engine', create_engine('sqlite://')) + engine = create_engine(kwargs.get('engine', 'sqlite://')) Base.metadata.create_all(engine) self.Session = sessionmaker(bind=engine) @@ -74,8 +92,8 @@ def __init__(self, **kwargs): self.session.commit() self.online = self.connection is not None - self.catalogClient = RootObjClient(self.catalog, self, 'syncCatalogData') - self.budgetClient = RootObjClient(self.budget, self, 'syncBudgetData') + self.catalogClient = CatalogClient(self.catalog, self) + self.budgetClient = BudgetClient(self.budget, self) @staticmethod def from_obj(args, reset=False, sync=True, **kwargs): @@ -98,41 +116,17 @@ def from_obj(args, reset=False, sync=True, **kwargs): print('No budget by the name %s found in nYNAB' % args.budgetname) exit(-1) - @property - def extra_catalog(self): - return dict(user_id=self.connection.user_id) - - @property - def extra_budget(self): - return dict(calculated_entities_included=False, budget_version_id=self.budget_version_id) - - def sync_catalog(self): - self.catalogClient.sync(extra=self.extra_catalog) - - def sync_budget(self): - self.budgetClient.sync(extra=self.extra_budget) - def sync(self): - if self.connection is None: - return LOG.debug('Client.sync') - self.sync_catalog() + self.catalogClient.sync() self.select_budget(self.budget_name) - self.sync_budget() + self.budgetClient.sync() if self.budget_version_id is None and self.budget_name is not None: raise BudgetNotFound() - def push_budget(self): - self.budgetClient.push(extra=self.extra_budget) - - def push_catalog(self): - self.catalogClient.push(extra=self.extra_catalog) - def push(self, expected_delta=1): - if self.connection is None: - return # ending-starting represents the number of modifications that have been done to the data ? LOG.debug('Client.push') @@ -148,8 +142,8 @@ def push(self, expected_delta=1): if any(catalog_changed_entities) or any(budget_changed_entities): self.ending_device_knowledge = self.starting_device_knowledge + 1 - self.push_catalog() - self.push_budget() + self.catalogClient.push() + self.budgetClient.push() self.starting_device_knowledge = self.ending_device_knowledge self.session.commit() @@ -185,6 +179,7 @@ def add_account(self, account, balance, balance_date): self.budget.be_accounts.append(account) self.budget.be_payees.append(payee) self.budget.be_transactions.append(transaction) + pass @operation(1) def delete_account(self, account): diff --git a/pynYNAB/ObjClient.py b/pynYNAB/ObjClient.py index f7c3237..ce77e0c 100644 --- a/pynYNAB/ObjClient.py +++ b/pynYNAB/ObjClient.py @@ -1,14 +1,24 @@ import logging - +from abc import abstractproperty,ABCMeta LOG = logging.getLogger(__name__) -class RootObjClient(object): - def __init__(self, obj, client, opname): + +class RootObjClient(): + __metaclass__ = ABCMeta + + @abstractproperty + def extra(self): + return {} + + @abstractproperty + def opname(self): + return '' + + def __init__(self, obj, client): self.obj = obj self.client = client self.connection = client.connection self.session = client.session - self.opname = opname def update_from_api_changed_entities(self, changed_entities): for name in self.obj.listfields: @@ -51,8 +61,8 @@ def update_from_changed_entities(self, changed_entities): def update_from_sync_data(self, sync_data): self.update_from_api_changed_entities(sync_data['changed_entities']) - def sync(self, extra=None): - sync_data = self.get_sync_data_obj(extra) + def sync(self): + sync_data = self.get_sync_data_obj() self.client.server_entities[self.opname] = sync_data['changed_entities'] LOG.debug('server_knowledge_of_device ' + str(sync_data['server_knowledge_of_device'])) @@ -77,46 +87,45 @@ def sync(self, extra=None): LOG.debug('current_device_knowledge %s' % self.client.current_device_knowledge[self.opname]) LOG.debug('device_knowledge_of_server %s' % self.client.device_knowledge_of_server[self.opname]) - def push(self, extra=None): - if self.connection is None: - return - if extra is None: - extra = {} - + def push(self): changed_entities = self.obj.get_changed_apidict() request_data = dict(starting_device_knowledge=self.client.starting_device_knowledge, ending_device_knowledge=self.client.ending_device_knowledge, device_knowledge_of_server=self.client.device_knowledge_of_server[self.opname], changed_entities=changed_entities) - request_data.update(extra) - sync_data = self.connection.dorequest(request_data, self.opname) - LOG.debug('server_knowledge_of_device ' + str(sync_data['server_knowledge_of_device'])) - LOG.debug('current_server_knowledge ' + str(sync_data['current_server_knowledge'])) - self.update_from_sync_data(sync_data) - self.session.commit() - self.obj.clear_changed_entities() - - server_knowledge_of_device = sync_data['server_knowledge_of_device'] - current_server_knowledge = sync_data['current_server_knowledge'] - - change = current_server_knowledge - self.client.device_knowledge_of_server[self.opname] - if change > 0: - LOG.debug('Server knowledge has gone up by ' + str( - change) + '. We should be getting back some entities from the server') - if self.client.current_device_knowledge[self.opname] < server_knowledge_of_device: - if self.client.current_device_knowledge[self.opname] != 0: - LOG.error('The server knows more about this device than we know about ourselves') - self.client.current_device_knowledge[self.opname] = server_knowledge_of_device - self.client.device_knowledge_of_server[self.opname] = current_server_knowledge - - LOG.debug('current_device_knowledge %s' % self.client.current_device_knowledge[self.opname]) - LOG.debug('device_knowledge_of_server %s' % self.client.device_knowledge_of_server[self.opname]) - - def get_sync_data_obj(self, extra=None): + request_data.update(self.extra) + + def validate(): + self.session.commit() + self.obj.clear_changed_entities() + if self.connection is not None: + sync_data = self.connection.dorequest(request_data, self.opname) + LOG.debug('server_knowledge_of_device ' + str(sync_data['server_knowledge_of_device'])) + LOG.debug('current_server_knowledge ' + str(sync_data['current_server_knowledge'])) + self.update_from_sync_data(sync_data) + validate() + + server_knowledge_of_device = sync_data['server_knowledge_of_device'] + current_server_knowledge = sync_data['current_server_knowledge'] + + change = current_server_knowledge - self.client.device_knowledge_of_server[self.opname] + if change > 0: + LOG.debug('Server knowledge has gone up by ' + str( + change) + '. We should be getting back some entities from the server') + if self.client.current_device_knowledge[self.opname] < server_knowledge_of_device: + if self.client.current_device_knowledge[self.opname] != 0: + LOG.error('The server knows more about this device than we know about ourselves') + self.client.current_device_knowledge[self.opname] = server_knowledge_of_device + self.client.device_knowledge_of_server[self.opname] = current_server_knowledge + + LOG.debug('current_device_knowledge %s' % self.client.current_device_knowledge[self.opname]) + LOG.debug('device_knowledge_of_server %s' % self.client.device_knowledge_of_server[self.opname]) + else: + validate() + + def get_sync_data_obj(self): if self.connection is None: return - if extra is None: - extra = {} if self.opname not in self.client.current_device_knowledge: self.client.current_device_knowledge[self.opname] = 0 if self.opname not in self.client.device_knowledge_of_server: @@ -127,6 +136,6 @@ def get_sync_data_obj(self, extra=None): device_knowledge_of_server=self.client.device_knowledge_of_server[self.opname], changed_entities={}) - request_data.update(extra) + request_data.update(self.extra) return self.connection.dorequest(request_data, self.opname) \ No newline at end of file diff --git a/pynYNAB/__main__.py b/pynYNAB/__main__.py index ab3fdc5..d2a7f77 100644 --- a/pynYNAB/__main__.py +++ b/pynYNAB/__main__.py @@ -19,6 +19,23 @@ LOG = logging.getLogger(__name__) +parser = configargparse.getArgumentParser('pynYNAB', default_config_files=[configfile], + add_env_var_help=True, + add_config_file_help=True, + auto_env_var_prefix='NYNAB_') + +parser.add_argument('--email', metavar='Email', type=str, required=False, + help='The Email User ID for nYNAB') +parser.add_argument('--password', metavar='Password', type=str, required=False, + help='The Password for nYNAB') +parser.add_argument('--budgetname', metavar='BudgetName', type=str, required=False, + help='The nYNAB budget to use') + +class classproperty(object): + def __init__(self, f): + self.f = f + def __get__(self, obj, owner): + return self.f(owner) class MainCommands(object): def __init__(self): @@ -39,21 +56,27 @@ def __init__(self): # use dispatch pattern to invoke method with same name getattr(self, args.command)() - def csvimport(self): - print('pynYNAB CSV import') + @classproperty + def csvimport_parser(cls): + csv_parser = argparse.ArgumentParser(parents=[parser],add_help=False) + csv_parser.description = inspect.getdoc(cls.csvimport) + csv_parser.add_argument('csvfile', metavar='CSVpath', type=str, + help='The CSV file to import') + csv_parser.add_argument('schema', metavar='schemaName', type=str, + help='The CSV schema to use (see csv_schemas directory)') + csv_parser.add_argument('accountname', metavar='AccountName', type=str, nargs='?', + help='The nYNAB account name to use') + csv_parser.add_argument('-import-duplicates', action='store_true', + help='Forces the import even if a duplicate (same date, account, amount, memo, payee) is found') + return csv_parser + + + @classmethod + def csvimport(cls): """Manually import a CSV into a nYNAB budget""" - parser = configargparse.getArgumentParser('pynYNAB') - parser.description = inspect.getdoc(self.csvimport) - parser.add_argument('csvfile', metavar='CSVpath', type=str, - help='The CSV file to import') - parser.add_argument('schema', metavar='schemaName', type=str, - help='The CSV schema to use (see csv_schemas directory)') - parser.add_argument('accountname', metavar='AccountName', type=str, nargs='?', - help='The nYNAB account name to use') - parser.add_argument('-import-duplicates', action='store_true', - help='Forces the import even if a duplicate (same date, account, amount, memo, payee) is found') - - args = parser.parse_args() + print('pynYNAB CSV import') + + args = cls.csvimport_parser.parse_args() verify_common_args(args) if not os.path.exists(args.csvfile): @@ -64,17 +87,20 @@ def csvimport(self): delta = do_csvimport(args,client) client.push(expected_delta=delta) + @classproperty + def ofximport_parser(cls): + ofx_parser = argparse.ArgumentParser(parents=[parser],add_help=False) + ofx_parser.description = inspect.getdoc(cls.ofximport) + ofx_parser.add_argument('ofxfile', metavar='OFXPath', type=str, + help='The OFX file to import') + return ofx_parser - def ofximport(self): - print('pynYNAB OFX import') + @classmethod + def ofximport(cls): """Manually import an OFX into a nYNAB budget""" + print('pynYNAB OFX import') - parser = configargparse.getArgumentParser('pynYNAB') - parser.description = inspect.getdoc(self.ofximport) - parser.add_argument('ofxfile', metavar='OFXPath', type=str, - help='The OFX file to import') - - args = parser.parse_args() + args = cls.ofximport_parser.parse_args() verify_common_args(args) client = clientfromargs(args) delta = do_ofximport(args,client) @@ -93,17 +119,5 @@ def verify_common_args(args): exit(-1) -parser = configargparse.getArgumentParser('pynYNAB', default_config_files=[configfile], - add_env_var_help=True, - add_config_file_help=True, - auto_env_var_prefix='NYNAB_') - -parser.add_argument('--email', metavar='Email', type=str, required=False, - help='The Email User ID for nYNAB') -parser.add_argument('--password', metavar='Password', type=str, required=False, - help='The Password for nYNAB') -parser.add_argument('--budgetname', metavar='BudgetName', type=str, required=False, - help='The nYNAB budget to use') - def main(): MainCommands() \ No newline at end of file diff --git a/pynYNAB/scripts/README.rst b/pynYNAB/scripts/README.rst index 3a40f8a..4702d7d 100644 --- a/pynYNAB/scripts/README.rst +++ b/pynYNAB/scripts/README.rst @@ -1,80 +1,17 @@ - -csvimport.py ------------- -.. code-block:: - - usage: csvimport.py [-h] [--email Email] [--password Password] - [--level LoggingLevel] [--budgetname BudgetName] - CSVpath schemaName [AccountName] - - Manually import a CSV into a nYNAB budget - - positional arguments: - CSVpath The CSV file to import - schemaName The CSV schema to use (see csv_schemas directory) - AccountName The nYNAB account name to use (default: None) - - optional arguments: - -h, --help show this help message and exit - - command line or config file arguments: - --email Email The Email User ID for nYNAB (default: None) - --password Password The Password for nYNAB (default: None) - --level LoggingLevel Logging Level (default: error) - --budgetname BudgetName - The nYNAB budget to use (default: None) - -migrate.py ----------- -.. code-block:: - - usage: migrate.py [-h] [--email Email] [--password Password] - [--level LoggingLevel] [--budgetname BudgetName] - BudgetPath - - Migrate a YNAB4 budget transaction history to nYNAB, using pyynab - - positional arguments: - BudgetPath The budget .ynab4 directory - - optional arguments: - -h, --help show this help message and exit - - command line or config file arguments: - --email Email The Email User ID for nYNAB (default: None) - --password Password The Password for nYNAB (default: None) - --level LoggingLevel Logging Level (default: error) - --budgetname BudgetName - The nYNAB budget to use (default: None) - -ofximport.py ------------- -.. code-block:: - - usage: ofximport.py [-h] [--email Email] [--password Password] - [--level LoggingLevel] [--budgetname BudgetName] - OFXPath - - Manually import an OFX into a nYNAB budget - - positional arguments: - OFXPath The OFX file to import - - optional arguments: - -h, --help show this help message and exit - - command line or config file arguments: - --email Email The Email User ID for nYNAB (default: None) - --password Password The Password for nYNAB (default: None) - --level LoggingLevel Logging Level (default: error) - --budgetname BudgetName - The nYNAB budget to use (default: None) - -Command Line / Config File Arguments -==================================== -Args that start with '--' (eg. --email) can also be set in the config file -(ynab.conf). The recognized syntax for setting (key, value) pairs is based -on the INI and YAML formats (e.g. key=value or foo=TRUE). For full -documentation of the differences from the standards please refer to the -ConfigArgParse documentation. If an arg is specified in more than one -place, then commandline values override config file values. +csvimport.py +------------ +.. code-block:: + usage: generate_doc.py [-h] [--email Email] [--password Password] [--budgetname BudgetName] [-import-duplicates] CSVpath schemaName [AccountName] Manually import a CSV into a nYNAB budget positional arguments: CSVpath The CSV file to import schemaName The CSV schema to use (see csv_schemas directory) AccountName The nYNAB account name to use optional arguments: -h, --help show this help message and exit --email Email The Email User ID for nYNAB --password Password The Password for nYNAB --budgetname BudgetName The nYNAB budget to use -import-duplicates Forces the import even if a duplicate (same date, account, amount, memo, payee) is found +ofximport.py +------------ +.. code-block:: + usage: generate_doc.py [-h] [--email Email] [--password Password] [--budgetname BudgetName] OFXPath Manually import an OFX into a nYNAB budget positional arguments: OFXPath The OFX file to import optional arguments: -h, --help show this help message and exit --email Email The Email User ID for nYNAB --password Password The Password for nYNAB --budgetname BudgetName The nYNAB budget to use + +Command Line / Config File Arguments +==================================== +Args that start with '--' (eg. --email) can also be set in the config file +(ynab.conf). The recognized syntax for setting (key, value) pairs is based +on the INI and YAML formats (e.g. key=value or foo=TRUE). For full +documentation of the differences from the standards please refer to the +ConfigArgParse documentation. If an arg is specified in more than one +place, then commandline values override config file values. diff --git a/pynYNAB/scripts/csvimport.py b/pynYNAB/scripts/csvimport.py index e341f5e..85d2cf4 100644 --- a/pynYNAB/scripts/csvimport.py +++ b/pynYNAB/scripts/csvimport.py @@ -160,5 +160,6 @@ def getsubcategory(categoryname): if __name__ == "__main__": - from pynYNAB.entrypoints import csvimport_main - csvimport_main() + from pynYNAB.__main__ import MainCommands + MainCommands.csvimport() + diff --git a/pynYNAB/scripts/generate_doc.py b/pynYNAB/scripts/generate_doc.py index 16bdb26..6c85e0a 100644 --- a/pynYNAB/scripts/generate_doc.py +++ b/pynYNAB/scripts/generate_doc.py @@ -1,16 +1,21 @@ import os import subprocess +from pynYNAB.__main__ import MainCommands + with open('README.rst', 'w') as readme: - for module in os.listdir(os.path.dirname(__file__)): - if module == os.path.basename(__file__) or module == '__init__.py' or module[-3:] != '.py': - continue - readme.write('\n') - readme.writelines('\n'.join([module, '-' * len(module), '.. code-block:: ', ''])) - process = subprocess.Popen([module, '-h'], shell=True, stdout=subprocess.PIPE) - readme.write('\n') - for line in process.stdout: - readme.write(' ' + line) + + + module = 'csvimport.py' + readme.writelines('\n'.join([module, '-' * len(module), '.. code-block:: ', ''])) + readme.writelines(' ' + line+'\r' for line in MainCommands.csvimport_parser.format_help().splitlines()) + + readme.write('\n') + module = 'ofximport.py' + readme.writelines('\n'.join([module, '-' * len(module), '.. code-block:: ', ''])) + readme.writelines(' ' + line+'\r' for line in MainCommands.ofximport_parser.format_help().splitlines()) + + readme.write('\n') readme.writelines(""" Command Line / Config File Arguments diff --git a/pynYNAB/scripts/migrate.csv.format b/pynYNAB/scripts/migrate.csv.format deleted file mode 100644 index 5b62adc..0000000 --- a/pynYNAB/scripts/migrate.csv.format +++ /dev/null @@ -1,2 +0,0 @@ -# Checking,Savings,CreditCard,Cash,LineOfCredit,Paypal,MerchantAccount,InvestmentAccount,Mortgage,OtherAsset,OtherLiability -AccountName, AccountType, OnBudget diff --git a/pynYNAB/scripts/migrate.py b/pynYNAB/scripts/migrate.py deleted file mode 100644 index cae3143..0000000 --- a/pynYNAB/scripts/migrate.py +++ /dev/null @@ -1,233 +0,0 @@ -import inspect -import os -import random -import re - -import configargparse -from ynab import YNAB - -from pynYNAB.Client import clientfromargs -from pynYNAB.schema.budget import MasterCategory, SubCategory, Account, Payee, Transaction - - -def migrate_main(): - print('migrate YNAB4 to pynYNAB') - """Migrate a YNAB4 budget transaction history to nYNAB, using pyynab""" - - parser = configargparse.getArgumentParser('pynYNAB') - parser.description=inspect.getdoc(migrate_main) - parser.add_argument('budget', metavar='BudgetPath', type=str, - help='The budget .ynab4 directory') - args = parser.parse_args() - - budget_base_name=os.path.basename(args.budget) - budget_path=os.path.dirname(args.budget) - budget_name=re.match(r"(?P.*)~[A-Z0-9]{8}\.ynab4",budget_base_name).groupdict().get('budget_name') - - if args.budgetname is not None: - budget_name=args.budgetname - - thisynab = YNAB(budget_path,budget_name) - - client = clientfromargs(args, reset=True) - - - - for ynab4_account in thisynab.accounts: - account=Account( - name=ynab4_account.name, - account_type=ynab4_account.type.value, - on_budget=ynab4_account.on_budget, - sortable_index=random.randint(-50000, 50000), - ) - mindate=min([ynab4transaction.date for ynab4transaction in thisynab.transactions if ynab4transaction.account == ynab4_account]) - client.add_account(account, 0, mindate) - - for master_category in thisynab.master_categories: - master_entity = MasterCategory( - name=master_category.name, - sortable_index=random.randint(-50000, 50000) - ) - client.budget.be_master_categories.append(master_entity) - for category in master_category.categories: - - entity = SubCategory( - name=category.name, - entities_master_category_id=master_entity.id, - sortable_index=random.randint(-50000, 50000) - ) - client.budget.be_subcategories.append(entity) - client.sync() - - for ynab4_payee in thisynab.payees: - payee=Payee( - name=ynab4_payee.name - ) - client.budget.be_payees.append(payee) - client.sync() - - #for ynab4transaction in thisynab.transactions: - # transaction=Transaction( - - # ) - # pass - # - # transactions = [] - # subtransactions = [] - # accumulatedSplits = [] - # transfers = [] - # reTransfer = re.compile('.*?Transfer : (?P.*)') - # reSplit = re.compile('\(Split\ (?P\d+)\/(?P\d+)\)') - # - # split_id = next(x.id for x in nYNABobject.budget.be_subcategories if x.internal_name == 'Category/__Split__') - # - # for register_row in RegisterRows: - # try: - # payee_id = payeemapping[register_row.PayeeName] - # except KeyError: - # payee_id = None - # resultSplit = reSplit.match(register_row.Memo) - # if resultSplit: - # accumulatedSplits.append(register_row) - # - # if resultSplit.group('num1') == resultSplit.group('num2'): - # total = sum(map(lambda x: x.Inflow - x.Outflow, accumulatedSplits)) - # try: - # payee_id = payeemapping[next(x.PayeeName for x in accumulatedSplits if x.PayeeName in payeemapping)] - # except StopIteration: - # payee_id = None - # transaction = Transaction( - # amount=total, - # date=register_row.Date, - # entities_payee_id=payee_id, - # entities_account_id=accountmapping[register_row.AccountName], - # entities_subcategory_id=split_id, - # flag=register_row.Flag - # ) - # transactions.append(transaction) - # for split in accumulatedSplits: - # result = reTransfer.match(split.PayeeName) - # if result is not None: - # otheraccount = result.group('account') - # payee_id = payeemapping['Transfer : ' + otheraccount] - # - # # this => other - # subtransaction = Subtransaction( - # amount=split.Inflow - split.Outflow, - # date=split.Date, - # entities_payee_id=payee_id, - # entities_account_id=accountmapping[split.AccountName], - # transfer_account_id=accountmapping[otheraccount], - # flag=split.Flag, - # entities_transaction_id=transaction.id - # ) - # subtransactions.append(subtransaction) - # transfers.append(subtransaction) - # else: - # if split.MasterCategoryName == '': - # # an out of budget transaction most probably - # entities_subcategory_id = None - # else: - # entities_subcategory_id = submapping[split.MasterCategoryName][split.SubcategoryName] - # subtransactions.append(Subtransaction( - # amount=split.Inflow - split.Outflow, - # date=split.Date, - # entities_account_id=accountmapping[split.AccountName], - # entities_payee_id=payee_id, - # entities_subcategory_id=entities_subcategory_id, - # flag=split.Flag, - # entities_transaction_id=transaction.id - # )) - # accumulatedSplits = [] - # else: - # result = reTransfer.match(register_row.PayeeName) - # if result is not None: - # payee_id = payeemapping[register_row.PayeeName] - # otheraccount = result.group('account') - # # this => other - # transaction = Transaction( - # amount=register_row.Inflow - register_row.Outflow, - # date=register_row.Date, - # entities_payee_id=payee_id, - # entities_account_id=accountmapping[register_row.AccountName], - # transfer_account_id=accountmapping[otheraccount], - # flag=register_row.Flag - # ) - # transfers.append(transaction) - # else: - # if register_row.MasterCategoryName == '': - # # an out of budget transaction most probably - # entities_subcategory_id = None - # else: - # entities_subcategory_id = submapping[register_row.MasterCategoryName][register_row.SubcategoryName] - # transactions.append(Transaction( - # amount=register_row.Inflow - register_row.Outflow, - # date=register_row.Date, - # entities_account_id=accountmapping[register_row.AccountName], - # entities_payee_id=payee_id, - # entities_subcategory_id=entities_subcategory_id, - # flag=register_row.Flag - # )) - # - # unsplittransactions=[transaction for transaction in transactions if transaction.entities_subcategory_id != split_id] - # splittransactions=[transaction for transaction in transactions if transaction.entities_subcategory_id == split_id] - # - # transactions_dict={transaction.id:transaction for transaction in transactions} - # subtransactions_dict={subtransaction.id:subtransaction for subtransaction in subtransactions} - # - # transfers_dict = {tr.id: tr for tr in transfers} - # random.shuffle(transfers) - # - # for i1 in range(len(transfers)): - # tr1 = transfers[i1] - # for i2 in range(len(transfers)): - # if i2 > i1: - # tr2 = transfers[i2] - # if isinstance(tr1, Transaction) and isinstance(tr2, Transaction): - # if tr1.entities_account_id == tr2.transfer_account_id \ - # and tr1.date == tr2.date \ - # and tr1.amount == -tr2.amount: - # - # tr1.transfer_transaction_id = tr2.id - # tr2.transfer_transaction_id = tr1.id - # - # transactions.append(tr1) - # transactions.append(tr2) - # elif isinstance(tr1, Transaction) and isinstance(tr2, Subtransaction): - # tr2parent = next(x for x in transactions if x.id == tr2.entities_transaction_id) - # if tr1.entities_account_id == tr2.transfer_account_id \ - # and tr1.date == tr2parent.date \ - # and tr1.amount == -tr2.amount: - # siblings = [subtransaction for subtransaction in subtransactions if subtransaction.entities_transaction_id == tr2parent.id] - # - # tr1.transfer_subtransaction_id = tr2.id - # tr2.transfer_transaction_id = tr1.id - # - # transactions.append(tr1) - # subtransactions.append(tr2) - # elif isinstance(tr1, Subtransaction) and isinstance(tr2, Transaction): - # tr1parent = next(x for x in transactions if x.id == tr1.entities_transaction_id) - # if tr1parent.entities_account_id == tr2.transfer_account_id \ - # and tr1parent.date == tr2.date \ - # and tr1.amount == -tr2.amount: - # siblings = [subtransaction for subtransaction in subtransactions if subtransaction.entities_transaction_id == tr1parent.id] - # - # tr1.transfer_transaction_id = tr2.id - # tr2.transfer_subtransaction_id = tr1.id - # - # subtransactions.append(tr1) - # transactions.append(tr2) - # - # - # - # nYNABobject.budget.be_transactions.extend(transactions) - # nYNABobject.budget.be_subtransactions.extend(subtransactions) - # - # nYNABobject.sync() - # - # - # - # pass - -if __name__ == "__main__": - migrate_main() \ No newline at end of file diff --git a/pynYNAB/scripts/ofximport.py b/pynYNAB/scripts/ofximport.py index 279bce5..9e8eccd 100644 --- a/pynYNAB/scripts/ofximport.py +++ b/pynYNAB/scripts/ofximport.py @@ -74,5 +74,5 @@ def do_ofximport(args, client=None): if __name__ == "__main__": - from pynYNAB.entrypoints import ofximport_main - ofximport_main() + from pynYNAB.__main__ import MainCommands + MainCommands.ofximport() diff --git a/pynYNAB/scripts/test.csv b/pynYNAB/scripts/test.csv deleted file mode 100644 index 2ea04bd..0000000 --- a/pynYNAB/scripts/test.csv +++ /dev/null @@ -1,3 +0,0 @@ -Date,Payee,Amount,Memo,Account -2016-01-01,GreatPants Inc.,-40,Pants,Checking -2016-01-01,,20,Saving! ,Savings \ No newline at end of file diff --git a/test_live/test_dbquery.py b/test_live/test_dbquery.py new file mode 100644 index 0000000..8bdeda7 --- /dev/null +++ b/test_live/test_dbquery.py @@ -0,0 +1,16 @@ +from sqlalchemy import extract + +from pynYNAB.Client import clientfromargs +from pynYNAB.__main__ import parser +from pynYNAB.schema.budget import Transaction + +args = parser.parse_known_args()[0] +client = clientfromargs(args) +client.sync() + + +session = client.session +march_payees = session.query(Transaction).filter(extract('month',Transaction.date.month)==3).all() +pass + + diff --git a/test_live/test_scaling.py b/test_live/test_scaling.py index 168d5b3..04d0f97 100644 --- a/test_live/test_scaling.py +++ b/test_live/test_scaling.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import unittest from pynYNAB.Client import clientfromargs -from pynYNAB.entrypoints import parser +from pynYNAB.__main__ import parser test_budget_name = 'Test Budget' diff --git a/test_live/testsync.py b/test_live/testsync.py index 2211e4f..26b86e5 100644 --- a/test_live/testsync.py +++ b/test_live/testsync.py @@ -1,7 +1,7 @@ import unittest from pynYNAB.Client import clientfromargs -from pynYNAB.entrypoints import parser +from pynYNAB.__main__ import parser # used to ping the nYNAB API to check that the sync works diff --git a/tests/barebones.py b/tests/barebones.py new file mode 100644 index 0000000..29b95b7 --- /dev/null +++ b/tests/barebones.py @@ -0,0 +1,9 @@ +from pynYNAB.Client import nYnabClient +from pynYNAB.connection import nYnabConnection + +email = "############" +password = "######" + +connection = nYnabConnection(email, password) +client = nYnabClient(nynabconnection=connection,budgetname='TestBudget') +client.sync() \ No newline at end of file diff --git a/tests/common_mock.py b/tests/common_mock.py index be841db..04a2473 100644 --- a/tests/common_mock.py +++ b/tests/common_mock.py @@ -2,14 +2,52 @@ from pynYNAB.Client import nYnabClient from pynYNAB.schema.budget import SubCategory, Payee, MasterCategory +from pynYNAB.schema.catalog import BudgetVersion +from pynYNAB.schema.roots import Catalog, Budget + + +class MockConnection(object): + def __init__(self): + self.user_id = '1234' + self.catalog = Catalog() + self.budget = Budget() + + def dorequest(self, request_dic, opname): + if opname == 'syncCatalogData': + return {'changed_entities':{k:[] for k in self.catalog.listfields},'server_knowledge_of_device':0,'current_server_knowledge':123} + if opname == 'syncBudgetData': + return {'changed_entities':{k:[] for k in self.budget.listfields},'server_knowledge_of_device':0,'current_server_knowledge':123} + class TestCommonMock(unittest.TestCase): def setUp(self): - self.client = nYnabClient(budgetname='') - master_category=MasterCategory(name='master') + self.client = nYnabClient(budgetname='TestBudget',nynabconnection=MockConnection()) + + session = self.client.session + + budget_version = BudgetVersion(version_name='TestBudget') + master_category = MasterCategory(name='master') + subcategory = SubCategory(name='Immediate Income', + internal_name='Category/__ImmediateIncome__', + entities_master_category=master_category) + payee = Payee(name='Starting Balance Payee',internal_name='StartingBalancePayee') + session.add(master_category) + session.add(subcategory) + session.add(payee) + + self.client.catalog.ce_budget_versions.append(budget_version) self.client.budget.be_master_categories.append(master_category) - self.client.budget.be_subcategories.append(SubCategory(name ='Immediate Income', - internal_name='Category/__ImmediateIncome__', - entities_master_category=master_category)) - self.client.budget.be_payees.append(Payee(name='Starting Balance Payee',internal_name='StartingBalancePayee')) + self.client.budget.be_subcategories.append(subcategory) + self.client.budget.be_payees.append(payee) + session.commit() + self.client.budget.clear_changed_entities() + self.client.catalog.clear_changed_entities() + + self.client.device_knowledge_of_server[self.client.budgetClient.opname] = 0 + self.client.device_knowledge_of_server[self.client.catalogClient.opname] = 0 + + self.client.current_device_knowledge[self.client.budgetClient.opname] = 0 + self.client.current_device_knowledge[self.client.catalogClient.opname] = 0 + + pass diff --git a/tests/test3.py b/tests/test3.py new file mode 100644 index 0000000..823043f --- /dev/null +++ b/tests/test3.py @@ -0,0 +1,12 @@ +import unittest + +from pynYNAB.Client import nYnabClient, WrongPushException +from pynYNAB.schema.budget import Transaction + + +class TestWrongPush(unittest.TestCase): + def TestWrongPush(self): + client = nYnabClient(budgetname='Test') + client.budget.be_transactions.append(Transaction()) + client.budget.be_transactions.append(Transaction()) + self.assertRaises(WrongPushException,lambda: client.push(expected_delta=1)) diff --git a/testscripts/test_knowledge.py b/testscripts/test_knowledge.py index f79c115..de36278 100644 --- a/testscripts/test_knowledge.py +++ b/testscripts/test_knowledge.py @@ -3,8 +3,8 @@ from datetime import datetime from pynYNAB.Client import clientfromargs +from pynYNAB.__main__ import parser from pynYNAB.schema.budget import Transaction -from pynYNAB.entrypoints import parser # used to ping the nYNAB API to check that the sync works diff --git a/testscripts/test_matched.py b/testscripts/test_matched.py index 0cfc676..f8a271d 100644 --- a/testscripts/test_matched.py +++ b/testscripts/test_matched.py @@ -2,7 +2,7 @@ from pynYNAB.Client import clientfromargs from pynYNAB.schema.budget import Transaction -from pynYNAB.entrypoints import parser +from pynYNAB.__main__ import parser from dotenv import load_dotenv,find_dotenv from pynYNAB.utils import get_or_create_payee diff --git a/testscripts/test_sync.py b/testscripts/test_sync.py index a3a0253..6acf8f0 100644 --- a/testscripts/test_sync.py +++ b/testscripts/test_sync.py @@ -1,5 +1,5 @@ from pynYNAB.Client import clientfromargs -from pynYNAB.entrypoints import parser +from pynYNAB.__main__ import parser from dotenv import load_dotenv,find_dotenv load_dotenv(find_dotenv())