Skip to content

Commit

Permalink
remove migrate (already implemented by YNAB classi)
Browse files Browse the repository at this point in the history
reorgnised the commands, their help
  • Loading branch information
rienafairefr committed Mar 27, 2017
1 parent 277e85e commit b714999
Show file tree
Hide file tree
Showing 21 changed files with 250 additions and 479 deletions.
3 changes: 2 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ source =
pynYNAB

[report]
omit = */tests/*
omit = */tests/*, \
*/pynYNAB/__main__.py
34 changes: 3 additions & 31 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
57 changes: 26 additions & 31 deletions pynYNAB/Client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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 = {}
Expand All @@ -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)
Expand All @@ -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):
Expand All @@ -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')

Expand All @@ -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()
Expand Down Expand Up @@ -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):
Expand Down
89 changes: 49 additions & 40 deletions pynYNAB/ObjClient.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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']))
Expand All @@ -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:
Expand All @@ -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)
Loading

0 comments on commit b714999

Please sign in to comment.