Skip to content

Commit d46eb65

Browse files
committed
Fleshed out worker.py and tests
1 parent f7e7d10 commit d46eb65

9 files changed

+374
-21
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,6 @@ Doctests are executed for a single module at a time. The command runs the tests
2626
The `-v` argument requests verbose output - otherwise it only reports test failures.
2727

2828
### Pytest
29-
To run all test cases (in the `tests/` folder):
29+
To run all test cases:
3030

31-
python -m pytest tests
31+
pytest

data/salary_data.csv

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
date,job_title,company_name,salary
2+
11/04/19,Manager,Big Red Jumbo Fries,10000
3+
04/05/19,Python coder,Convex Code,13000
4+
11/06/19,Cook,Tasty Food Shack,14000
File renamed without changes.

run_tests.py

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Pytesting/run_tests.py
4+
5+
Test runner for demo app
6+
7+
@author: Rupert.Thomas
8+
Created 16/11/2019
9+
10+
Adapted from: https://github.com/cambridgespark/pydata-testing-for-data-science
11+
Raoul-Gabriel Urma
12+
13+
Run tests (from the root folder using):
14+
python -m pytest test/
15+
16+
"""
17+
import argparse
18+
import os
19+
# import urllib.request
20+
import sys
21+
22+
import pytest
23+
24+
def run_unittests():
25+
print("Unittesting model...")
26+
pytest.main(['-v', 'tests'])
27+
28+
def run_coverage():
29+
print("Running coverage report...")
30+
pytest.main(['--cov-report', 'term-missing', '--cov=src/', 'tests/'])
31+
32+
def run_generative():
33+
print("Running generative testing...")
34+
pytest.main(['-v', '--hypothesis-show-statistics', 'tests/test_transformers_hypothesis.py'])
35+
36+
37+
def main():
38+
parser = argparse.ArgumentParser(
39+
description="A command line-tool to manage the project.")
40+
parser.add_argument(
41+
'stage',
42+
metavar='stage',
43+
type=str,
44+
choices=['unittest', 'generative', 'coverage'],
45+
help="Stage to run. Either unittest, generative, coverage")
46+
47+
if len(sys.argv[1:]) == 0:
48+
# parser.print_help()
49+
# parser.exit()
50+
print("Running all...")
51+
run_unittests()
52+
run_generative()
53+
run_coverage()
54+
return
55+
56+
stage = parser.parse_args().stage
57+
58+
if stage == "unittest":
59+
run_unittests()
60+
61+
elif stage == "coverage":
62+
run_coverage()
63+
64+
elif stage == "generative":
65+
run_generative()
66+
67+
68+
if __name__ == "__main__":
69+
main()

src/database.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ class DAO(object):
2828
>>> dao = DAO('example.db')
2929
Database connection initialised
3030
>>> dao.create_jobs_table()
31-
>>> dao.insert_job('Cook', 1, 'Tasty Food Shack', 1000)
31+
>>> dao.insert_job('Cook','Tasty Food Shack',11000,'11/06/2019')
3232
1
3333
>>> dao.query_job(1)
34-
('Cook', 1, 'Tasty Food Shack', 1000)
34+
('Cook', 1, 'Tasty Food Shack', 11000, '11/06/2019')
3535
>>> dao.close()
3636
>>> dao.delete_db()
3737
"""
@@ -56,18 +56,19 @@ def create_jobs_table(self):
5656
self.cur.execute('''CREATE TABLE IF NOT EXISTS jobs(job_title text,
5757
job_id integer PRIMARY KEY,
5858
company_name text,
59-
salary integer)''')
60-
def insert_job(self, job_title, job_id, company_name, salary):
59+
salary integer,
60+
date text)''')
61+
def insert_job(self, job_title, company_name, salary, date):
6162
sql = """
62-
INSERT INTO jobs (job_title, job_id, company_name, salary)
63+
INSERT INTO jobs (job_title, company_name, salary, date)
6364
VALUES (?, ?, ?, ?)"""
64-
self.cur.execute(sql, (job_title, job_id, company_name, salary))
65+
self.cur.execute(sql, (job_title, company_name, salary, date))
6566
self.commit()
6667
return self.cur.lastrowid
6768

6869
def query_job(self, job_id):
6970
sql = """
70-
SELECT job_title, job_id, company_name, salary FROM jobs WHERE job_id = ?
71+
SELECT job_title, job_id, company_name, salary, date FROM jobs WHERE job_id = ?
7172
"""
7273
self.cur.execute(sql, str(job_id))
7374
result = self.cur.fetchone()

src/main.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,38 @@
88
Created 15/11/2019
99
"""
1010

11-
from worker import Worker
12-
from database import DAO
11+
from src.worker import Worker
12+
from src.database import DAO
1313

1414
class Application:
1515
def __init__(self):
16+
""" Constructor """
1617
super().__init__()
1718

1819
# Setup database access
1920
dao = DAO()
2021
dao.create_jobs_table()
2122
self.dao = dao
23+
self.last_DB_row = None
2224

25+
# Setup processing worker
26+
self.worker = Worker()
27+
28+
def loadData(self, data_filepath):
29+
30+
parsed_data = self.worker.readData(data_filepath)
31+
32+
for row in parsed_data:
33+
self.last_DB_row = self.dao.insert_job(row['job_title'],
34+
row['company_name'],
35+
row['salary'],
36+
row['date'])
37+
38+
print(f'Last row in database: {self.last_DB_row}')
2339

2440

2541
if __name__ == '__main__':
2642

2743
# if being exectuted, create the app object
2844
app = Application()
45+
app.loadData('data/salary_data.csv')

src/worker.py

+101-3
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,109 @@
88
Created 15/11/2019
99
"""
1010

11+
# Conversion map for three-letter months
12+
short_months = {'Jan': 1, 'Feb': 1, 'Mar': 3, 'Apr': 4, 'May': 5, 'June': 6,
13+
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
14+
15+
# Data file format (columns)
16+
# (date, job_title, company_name, salary)
17+
1118
class Worker:
1219

13-
def extractDates(self, input_string):
20+
def readData(self, data_filepath):
21+
"""
22+
Read comma-separated data from a data file, and parse.
23+
24+
N.B. this function is deliberately lacking in robust validation and checking of the input data
25+
26+
:param data_filepath: (string) path to data file
27+
:returns outputdata: (list) of dictionaries, one per row of imported data
28+
"""
29+
30+
output_data = []
31+
32+
in_file = open(data_filepath, 'r')
33+
in_file.readline() # skip the header line
34+
35+
for temp_line in in_file.readlines():
36+
37+
parsed = self.parseLine(temp_line)
38+
if parsed is not None:
39+
output_data.append(parsed)
40+
41+
else:
42+
# data incomplete
43+
continue
44+
45+
return output_data
46+
47+
48+
def parseLine(self, input_string):
49+
"""
50+
Parse a single row of input data, and run rudimentary checks
51+
52+
N.B. this function is deliberately lacking in robust validation and checking of the input data
53+
54+
:param input_string: (string) of data, hopefully comma-separated
55+
:returns result: (dict) parsed data
56+
"""
57+
58+
result = None
59+
60+
temp_data = input_string.split(',')
61+
if len(temp_data) == 4:
62+
date_string = self.parseDate(temp_data[0])
63+
64+
if date_string is not None:
65+
result = {
66+
'date': date_string,
67+
'job_title': temp_data[1],
68+
'company_name': temp_data[2],
69+
'salary': int(temp_data[3])
70+
}
71+
72+
return result
73+
74+
75+
76+
def parseDate(self, input_string):
1477
"""
15-
Parses an input string, and returns dates
78+
Parses an input string, and extracts integer values for day, month and year
79+
80+
N.B. this function is deliberately lacking in robust validation and checking of the input data
81+
82+
:param input_string: (string) hopefully containing a date in one of the two recognised formats
83+
:returns result: (string) parsed date in dd/mm/YYYY format
84+
"""
85+
result_string = None
86+
# Date format 1: dd/mm/yy e.g. 01/09/19
87+
split_parts = input_string.split('/')
88+
if len(split_parts) == 3:
89+
dd = int(split_parts[0])
90+
mm = int(split_parts[1])
91+
YYYY = 2000 + int(split_parts[2])
92+
93+
# Test and assemble output
94+
if (dd>0) and (dd<=31) and \
95+
(mm>0) and (mm<=12) and \
96+
(YYYY>2000) and (YYYY<2100):
97+
result_string = f'{dd:02}/{mm:02}/{YYYY}'
1698

99+
# Date format 2: ddmmmYYYY 09Jan2019
100+
elif any(month in input_string for month in short_months.keys()):
101+
# One of the three letter month strings is in the input
102+
match = next(month for month in short_months.keys() if month in input_string)
103+
split_parts = input_string.split(match)
104+
if len(split_parts) == 2:
105+
dd = int(split_parts[0])
106+
mm = short_months[match]
107+
YYYY = int(split_parts[1])
108+
109+
# Test and assemble output
110+
if (dd>0) and (dd<=31) and \
111+
(YYYY>2000) and (YYYY<2100):
112+
result_string = f'{dd:02}/{mm:02}/{YYYY}'
113+
114+
# else: # Unknown date format
17115

18-
return True
116+
return result_string

tests/test_database.py

+41-7
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,60 @@
1111
python -m pytest test/
1212
1313
"""
14+
import sqlite3
1415

15-
import os
16-
import mock
16+
from truth.truth import AssertThat
1717

1818
# Module under test
1919
from src.database import DAO
2020

2121

22-
def test_delete_db(mocker):
22+
def test_init(mocker):
2323
"""
24-
Test deleting the database file
24+
Test database initialisation
2525
"""
2626

27-
mocker.patch('os.remove')
27+
# given: setup test framework
28+
mock_SQLite3 = mocker.patch('src.database.sqlite3')
29+
mock_ConnectionObj = mocker.MagicMock(sqlite3.Connection) # mock Connection object
30+
mock_CursorObj = mocker.MagicMock(sqlite3.Cursor) # mock Cursor object
31+
mock_ConnectionObj.cursor.return_value = mock_CursorObj # setup method calls
32+
mock_SQLite3.connect.return_value = mock_ConnectionObj
33+
34+
# when: database is initialised
35+
dao = DAO()
36+
37+
# then: expect connection and request for cursor
38+
AssertThat(mock_SQLite3.connect).WasCalled().Once()
39+
mock_SQLite3.connect.assert_called_once()
40+
mock_ConnectionObj.cursor.assert_called_once()
41+
2842

29-
# /given
43+
def test_delete_db(mocker):
44+
"""
45+
Test deleting the database file
46+
"""
47+
48+
# given: setup test framework
49+
mock_SQLite3 = mocker.patch('src.database.sqlite3') # patch database createion in DAO.__init__
3050
dao = DAO()
51+
mock_os_remove = mocker.patch('src.database.os.remove') # patch database deletion command
3152

3253
# /when
3354
dao.delete_db()
3455

3556
# /then
36-
os.remove.assert_called_once()
57+
mock_os_remove.assert_called_once()
58+
59+
60+
def test_close_db(mocker):
61+
pass
62+
63+
def test_create_jobs_table(mocker):
64+
pass
65+
66+
def test_insert_job(mocker):
67+
pass
68+
69+
def test_query_job(mocker):
70+
pass

0 commit comments

Comments
 (0)