Skip to content

Commit 729acb7

Browse files
authored
Merge pull request #4 from esecules/esecules-feature2
No repeat transactions when updating transaction list with new items
2 parents 307ff6f + a5e3c24 commit 729acb7

6 files changed

Lines changed: 98 additions & 24 deletions

File tree

src/groupsplit.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pickle
77
import pprint
88
import urllib
9+
import hashlib
910
import logging
1011
import optparse
1112
import requests
@@ -45,6 +46,11 @@ def split(total, num_people):
4546
" something doesnt add up here: %d * %d + %d != %d" %(base, num_people, extra, total)
4647
return base, extra
4748

49+
def do_hash(msg):
50+
m = hashlib.md5()
51+
m.update(msg)
52+
return m.hexdigest()
53+
4854
class Splitwise:
4955
def __init__(self, api_client='oauth_client.pkl'):
5056
if os.path.isfile(api_client):
@@ -132,8 +138,12 @@ def post_expense(self, uri):
132138
def delete_expense(self, expense_id):
133139
return self.api_call("https://secure.splitwise.com/api/v3.0/delete_expense/%s" % expense_id, 'POST')
134140

135-
def get_expenses(self, limit=0):
136-
resp = self.api_call("https://secure.splitwise.com/api/v3.0/get_expenses?limit=%s" % limit, 'GET')
141+
def get_expenses(self, after_date="", limit=0, allow_deleted=True):
142+
params = {'limit': limit, "updated_after": after_date}
143+
paramsStr = urllib.urlencode(params)
144+
resp = self.api_call("https://secure.splitwise.com/api/v3.0/get_expenses?%s" % (paramsStr), 'GET')
145+
if not allow_deleted:
146+
resp['expenses'] = [exp for exp in resp['expenses'] if exp['deleted_at'] is None]
137147
return resp['expenses']
138148

139149
class CsvSettings():
@@ -145,6 +155,7 @@ def __init__(self, rows):
145155
self.amount_col = input("Which column has the amount?")
146156
self.desc_col = input("Which column has the description?")
147157
self.has_title_row = raw_input("Does first row have titles? [Y/n]").lower() != 'n'
158+
self.newest_transaction = ''
148159
while True:
149160
try:
150161
self.local_currency = raw_input("What currency were these transactions made in?").upper()
@@ -155,11 +166,19 @@ def __init__(self, rows):
155166
else:
156167
break
157168
self.remember = raw_input("Remember these settings? [Y/n]").lower() != 'n'
158-
169+
170+
def __del__(self):
159171
if self.remember:
160172
with open("csv_settings.pkl", "wb") as pkl:
161173
pickle.dump(self, pkl)
162174

175+
def record_newest_transaction(self, rows):
176+
if self.has_title_row:
177+
self.newest_transaction = do_hash(str(rows[1]))
178+
else:
179+
self.newest_transaction = do_hash(str(rows[0]))
180+
181+
163182
class SplitGenerator():
164183
def __init__(self, options, args, api):
165184
csv_file = args[0]
@@ -176,20 +195,26 @@ def __init__(self, options, args, api):
176195
self.csv = pickle.load(f)
177196
else:
178197
self.csv = CsvSettings(self.rows)
179-
198+
180199
if self.csv.has_title_row:
181200
self.rows = self.rows[1:]
182201

183202
self.make_transactions()
203+
self.csv.record_newest_transaction(self.rows)
184204
self.get_group(group_name)
185205
self.splits = []
186206
self.ask_for_splits()
187207

188208
def make_transactions(self):
189-
self.transactions = [{"date": datetime.strftime(datetime.strptime(r[self.csv.date_col], "%m/%d/%y"), "%Y-%m-%dT%H:%M:%SZ"),
190-
"amount": -1 * Money(r[self.csv.amount_col], self.csv.local_currency),
191-
"desc": re.sub('\s+',' ', r[self.csv.desc_col])}
192-
for r in self.rows if float(r[self.csv.amount_col]) < 0]
209+
self.transactions = []
210+
for r in self.rows:
211+
if not self.options.try_all and do_hash(str(r)) == self.csv.newest_transaction:
212+
break
213+
if float(r[self.csv.amount_col]) < 0:
214+
self.transactions.append({"date": datetime.strftime(datetime.strptime(r[self.csv.date_col], "%m/%d/%y"), "%Y-%m-%dT%H:%M:%SZ"),
215+
"amount": -1 * Money(r[self.csv.amount_col], self.csv.local_currency),
216+
"desc": re.sub('\s+',' ', r[self.csv.desc_col])}
217+
)
193218

194219
def get_group(self, name):
195220
num_found = 0
@@ -249,6 +274,7 @@ def main():
249274
parser.add_option('-d', '--dryrun', default=False, action='store_true', dest='dryrun', help='prints requests instead of sending them')
250275
parser.add_option('', '--csv-settings', default='csv_settings.pkl', dest='csv_settings', help='supply different csv_settings object (for testing mostly)')
251276
parser.add_option('', '--api-client', default='oauth_client.pkl', dest='api_client', help='supply different splitwise api client (for testing mostly)')
277+
parser.add_option('-a', '--all', default=False, action='store_true', dest='try_all', help='consider all transactions in csv file no matter whether they were already seen')
252278
options, args = parser.parse_args()
253279
logger.setLevel(log_levels[options.verbosity])
254280
splitwise = Splitwise(options.api_client)

src/server.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ def shutdown_server():
88
raise RuntimeError('Not running with the Werkzeug Server')
99
func()
1010

11-
@app.route('/', defaults={'path': ''})
12-
@app.route('/<path:path>')
13-
def authorize( path ):
11+
@app.route('/')
12+
def authorize():
1413
print request.args['oauth_verifier']
1514
shutdown_server()
1615
return "Thank you, you can close the tab"
1716

17+
@app.route('/test')
18+
def test():
19+
return "Hello!"
20+
1821
if __name__ == "__main__":
1922
app.run()

test/csv_settings.pkl

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,22 @@ p2
77
I01
88
sS'has_title_row'
99
p3
10-
I01
11-
sS'amount_col'
10+
I00
11+
sS'newest_transaction'
1212
p4
13+
S'220ef2686a3923ceb06e2596ba8b7c4d'
14+
p5
15+
sS'amount_col'
16+
p6
1317
I1
1418
sS'desc_col'
15-
p5
19+
p7
1620
I4
1721
sS'local_currency'
18-
p6
22+
p8
1923
S'CAD'
20-
p7
24+
p9
2125
sS'date_col'
22-
p8
26+
p10
2327
I0
2428
sb.

test/oauth_client.pkl.enc

0 Bytes
Binary file not shown.

test/testServer.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,24 @@
33
import subprocess
44
from time import sleep
55
class ServerTest(unittest.TestCase):
6+
def check_server(self):
7+
polls = 0
8+
while True:
9+
try:
10+
resp = requests.get("http://localhost:5000/test")
11+
except requests.ConnectionError as e:
12+
resp = None
13+
pass
14+
if resp and resp.text == "Hello!":
15+
break
16+
if polls > 10:
17+
self.fail("could not connect to server")
18+
polls += 1
19+
sleep(1)
20+
621
def test_request(self):
722
serverProc = subprocess.Popen(['python', '../src/server.py'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
8-
sleep(1)
9-
self.assertEqual(None, serverProc.poll())
23+
self.check_server()
1024
test_data = "1234567890asdf"
1125
resp = requests.get("http://localhost:5000?oauth_verifier=%s" % test_data)
1226
assert resp.status_code == 200

test/testSplit.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import unittest
22
import sys
3+
import csv
34
import subprocess
5+
from datetime import datetime
46
from money import Money
57
sys.path.append("../src")
68
from groupsplit import split, Splitwise
@@ -31,21 +33,46 @@ def test_get_groups(self):
3133
self.assertGreater(len(self.api.get_groups()), 0)
3234

3335
class SystemTests(unittest.TestCase):
34-
def __del__(self):
35-
api = Splitwise()
36-
for expense in api.get_expenses():
37-
api.delete_expense(expense['id'])
36+
def __init__(self, *args, **kwargs):
37+
super(SystemTests, self).__init__(*args, **kwargs)
38+
self.api = Splitwise()
39+
self.start_date = datetime.strftime(datetime.utcnow(), "%Y-%m-%dT%H:%M:%SZ")
40+
with open('transactions.csv', 'rb') as csvfile:
41+
reader = csv.reader(csvfile)
42+
self.num_expenses = len([x for x in reader if float(x[1]) < 0])
43+
44+
def tearDown(self):
45+
for expense in self.api.get_expenses(allow_deleted=False):
46+
assert(expense['created_by']['last_name'] == 'Sample')
47+
self.api.delete_expense(expense['id'])
3848

49+
def verify_num_expenses(self, num=None):
50+
if num is None:
51+
num = self.num_expenses
52+
self.assertEqual(len(self.api.get_expenses(allow_deleted=False,after_date=self.start_date)), num)
53+
3954
def test_group_of_2(self):
4055
proc = subprocess.Popen(['python', '../src/groupsplit.py', 'transactions.csv', 'group_of_2',
4156
'--csv-settings=csv_settings.pkl', '--api-client=oauth_client.pkl',
42-
'-y'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
57+
'-ya'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
4358
stdout, stderr = proc.communicate()
4459
self.assertEqual(stderr, '')
60+
self.verify_num_expenses()
4561

4662
def test_group_of_3(self):
63+
proc = subprocess.Popen(['python', '../src/groupsplit.py', 'transactions.csv', 'group_of_3',
64+
'--csv-settings=csv_settings.pkl', '--api-client=oauth_client.pkl',
65+
'-ya'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
66+
stdout, stderr = proc.communicate()
67+
self.assertEqual(stderr, '')
68+
self.verify_num_expenses()
69+
70+
def test_no_double_upload(self):
71+
self.test_group_of_3()
4772
proc = subprocess.Popen(['python', '../src/groupsplit.py', 'transactions.csv', 'group_of_3',
4873
'--csv-settings=csv_settings.pkl', '--api-client=oauth_client.pkl',
4974
'-y'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
5075
stdout, stderr = proc.communicate()
5176
self.assertEqual(stderr, '')
77+
self.verify_num_expenses()
78+

0 commit comments

Comments
 (0)