Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

alpha_0.1 to dev #2

Merged
merged 26 commits into from
May 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# bt_expense folders and files to ignore
*.xlsx
!Expenses.xlsx
!Expenses_Template.xlsx

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down Expand Up @@ -110,3 +110,7 @@ ENV/
*.PNG

*.pdf

\.pytest_cache/v/cache/

*.jpg
4 changes: 3 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ env:
- CC_TEST_REPORTER_ID=6ea71d63c94102cdc54e78a76a58e3803e36f5247d1ba4cc04a1ed9af2e5623b
language: python
python:
- "3.4"
- "3.5"
- "3.6"
# - "3.7-dev"
Expand All @@ -20,8 +21,9 @@ before_script:
- ./cc-test-reporter before-build
# command to run tests
script:
- pwd
- ls
- pytest --cov-report term --cov-report xml --cov=bt_expense tests/
- pytest -v --cov-report term --cov-report xml --cov=bt_expense tests/
after_script:
- ./cc-test-reporter after-build -t coverage.py --exit-code $TRAVIS_TEST_RESULT
after_success:
Expand Down
Binary file removed bt_expense/Expenses.xlsx
Binary file not shown.
Binary file added bt_expense/Expenses_Template.xlsx
Binary file not shown.
148 changes: 125 additions & 23 deletions bt_expense/bt_expense.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
Pull expenses from Excel Spreadsheet and upload to BigTime via REST HTTP call.
"""
import os
import json
from pprint import pprint as pp
# import openpyxl as opxl
# import requests as r
# from pprint import pformat as pf
import requests as r
from openpyxl import load_workbook

# CD
os.chdir('bt_expense')
# os.chdir('bt_expense')

# Constants
BASE = 'https://iq.bigtime.net/BigtimeData/api/v2'
Expand All @@ -20,8 +21,107 @@
'cat': {}, }


class Authorizer(object):
"""authorizes a BitTime REST API session.
Can authorize using a user login and password or an API key.

User login and password will be used to obtain an API key.
If API key is provided, skip the step of obtaining API key"""

def __init__(self, workbook_filename='Expenses.xlsx'):
self.wb_name = workbook_filename
self.staffsid = None
self.auth_header = self._build_credentials()
self.userid = self.auth_header['userid']
self.userpwd = self.auth_header['pwd']
self.api_key = None
self._authorized = False
self.header = self.authorize_session()

def _build_credentials(self):
"""Pulls Login information from the `Setup` worksheet. Return dictionary
for Auth Header."""
keys = get_values('Setup', 'A1', 'A4', workbook_name=self.wb_name)
values = get_values('Setup', 'B1', 'B4', workbook_name=self.wb_name)
header = {k: v for (k, v) in zip(keys, values)}
header['Content-Type'] = 'application/json'
return header

def authorize_session(self):
response = r.post('{}/session'.format(BASE),
headers={'Content-Type': 'application/json'},
data=json.dumps(self.auth_header).encode('utf-8'))
if str(response.status_code)[0] is not '2':
# TODO Raise Requests HTTP Error
raise(ConnectionRefusedError)
response_dict = json.loads(response.text)
self.api_key = response_dict['token']
self.staffsid = response_dict['staffsid']
header = {'X-Auth-Token': self.api_key,
'X-Auth-Realm': self.auth_header['Firm'],
'Content-Type': self.auth_header['Content-Type']}
self._authorized = True
# print('Session Header\n', header)
return header


class Expensor(Authorizer):

def prep_expenses(self, save=True):
pnames = get_values('Expenses', 'A2', 'A102')
projs = get_values('Expenses', 'F2', 'F102')
cats = get_values('Expenses', 'G2', 'G102')
dates = get_values('Expenses', 'C2', 'C102')
costs = get_values('Expenses', 'D2', 'D102')
notes = get_values('Expenses', 'E2', 'E102')
expense_entries = []
total_cost = 0
for proj, cat, date, cost, note, pname in zip(projs, cats,
dates, costs,
notes, pnames):
if cost and date:
content = {'staffsid': int(self.staffsid),
'projectsid': int(proj),
'catsid': int(cat),
'dt': str(date)[:10],
'CostIN': float('{0:.2f}'.format(cost)),
'Nt': note,
# 'ProjectNm': pname,
'ApprovalStatus': 0}
total_cost += float('{0:.2f}'.format(cost))
expense_entries.append(content)
if save:
json_to_file(expense_entries, 'entries.json')
return expense_entries, float('{0:.2f}'.format(total_cost))

def post_expenses(self, upload=False):
expense_url = '{}/expense/detail'.format(BASE)
expense_entries, total = self.prep_expenses()
if upload is not True:
input_map = {'Y': True, 'N': False}
inp = input('Upload ${} {}\n{}'.format(total,
'worth of entries?',
'(y/n)')
).upper()
upload = input_map[inp]
if upload:
for entry in expense_entries:
print(r.post(expense_url, headers=self.header,
data=json.dumps(entry).encode()),
entry['dt'], entry['CostIN'])
else:
print('\t${} expense entries not uploaded!'.format(total))
return len(expense_entries)

def get_active_reports(self):
response = r.get('{0}/expense/reports'.format(BASE),
headers=self.header)
print(response.status_code)
return response.json()


def get_wb(workbook_name='Expenses.xlsx'):
return load_workbook(filename=workbook_name)
return load_workbook(filename=workbook_name, data_only=True)


def build_lookup_dictn_from_excel():
Expand All @@ -35,42 +135,44 @@ def build_lookup_dictn_from_excel():
return project_ids, category_ids


def get_values(sheet_name, start, stop=None):
def get_values(sheet_name, start, stop=None, workbook_name='Expenses.xlsx'):
"""Pulls a column (or section) of values from a Worksheet.
Returns a list."""
values = []
sheet = get_wb()[sheet_name]
sheet = get_wb(workbook_name)[sheet_name]
if not stop:
stop = sheet.max_row
cells = [c[0].value for c in sheet[start:stop]]
values = [c for c in cells if c is not None]
return values


def build_credentials():
"""Pulls Login information from the `Setup` worksheet. Return dictionary
for Auth Header."""
keys = get_values('Setup', 'A1', 'A4')
values = get_values('Setup', 'B1', 'B4')
header = {k: v for (k, v) in zip(keys, values)}
# TODO: Format for BigTime
return header


def get_picklist(picklist_name):
def get_picklist(auth_object, picklist_name):
"""Pulls a BigTime 'Picklist'
Use to build project and expense catagory lookup tables"""
Use to build project and expense catagory lookup tables.
Requires Admin account."""
# TODO: complete `get_picklist()` function
valid_picklists = ['projects', 'ExpenseCodes']
if picklist_name not in valid_picklists:
raise ValueError('Not a valid picklist')
header = build_credentials()
return picklist_name
pick_list_url = '{0}/picklist/{1}'.format(BASE, picklist_name)
print(pick_list_url)
response = r.get(pick_list_url, headers=auth_object.header)
return response.json()
# return response.json()


def json_to_file(json_obj, filename='data.json'):
with open(filename, 'w') as f_out:
json.dump(json_obj, f_out)
return filename


if __name__ == '__main__':
print(__doc__)
print('**DIR:', os.getcwd())
build_lookup_dictn_from_excel()
pp(BT_LOOKUP)
pp(build_credentials())
print('*' * 79)
exp1 = Expensor()
exp_entries = exp1.prep_expenses()
print(len(exp_entries))
pp(exp1.post_expenses())
8 changes: 6 additions & 2 deletions tests/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@
"""
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../bt_expense')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__),
'../bt_expense')))

import bt_expense


def fixpath(path):
path = os.path.normpath(os.path.expanduser(path))
if path.startswith("\\"): return "C:" + path
if path.startswith("\\"):
return "C:" + path
return path


print('USING context.py')

if __name__ == '__main__':
Expand Down
38 changes: 25 additions & 13 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,48 @@
simple tests for bt_expense.
"""
import os
import unittest

# import unittest
import pytest
from sys import version_info
# import pytest

from context import bt_expense as bte
from context import fixpath

PYTHON_VER = version_info[0]

TEST_DIR = fixpath(os.path.abspath(os.path.dirname(__file__)))
ROOT_DIR = fixpath(os.path.dirname(TEST_DIR))
MAIN_DIR = fixpath('{}/bt_expense'.format(ROOT_DIR))
WORKBOOK_NAME = 'bt_expense/Expenses_Template.xlsx'


def test_pulling_column_values():
a1 = bte.get_values('Expenses', 'A1',
workbook_name=WORKBOOK_NAME)[0]
assert a1, 'Project'


class SmokeTest(unittest.TestCase):
"""Test that nothing is on fire."""
def setUp(self):
os.chdir(TEST_DIR)
def test_pulling_auth_info():
expected_keys = ['userid', 'pwd', 'Firm', 'AuthType']
actual_keys = bte.get_values('Setup', 'A1', 'A4',
workbook_name=WORKBOOK_NAME)
for key in expected_keys:
assert key in actual_keys

def tearDown(self):
os.chdir(ROOT_DIR)

def test_pulling_column_values(self):
os.chdir(MAIN_DIR)
a1 = bte.get_values('Expenses', 'A1')[0]
self.assertEqual(a1, 'Project')
def test_authorizer_object_creation():
try:
bte.Authorizer(workbook_filename=WORKBOOK_NAME)
except ConnectionRefusedError as E:
print(E)


if __name__ == "__main__":
print('Python {}'.format(PYTHON_VER))
print(__doc__)
print(__file__)
print('root:', ROOT_DIR)
print('test:', TEST_DIR)
print('main:', MAIN_DIR)
unittest.main()
pytest.main(args=['-v'])