diff --git a/Pipfile b/Pipfile index 5abe100..ea0ea5f 100644 --- a/Pipfile +++ b/Pipfile @@ -41,6 +41,7 @@ passlib = "*" gunicorn = "*" zappa = "*" cognitive-face = "*" +pytest-mock = "*" [requires] python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock index 68535de..a2fe635 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "eb6fab1a6939ddb23f773b1cc41862b7958e2e7d03425c5bf6de2ccb03595d23" + "sha256": "86afa28e09268ef16fe28ec570bdb3d57d7c667e0a260d02b0896dc4ff331a1c" }, "pipfile-spec": 6, "requires": { @@ -49,17 +49,17 @@ }, "boto3": { "hashes": [ - "sha256:0daae195b2b2f7a7ae6b888ff9dd3f9bf6c339522924f5d73f1ddfc44f07a3f5", - "sha256:d97f89a8d1a50377cbb7eca15040e65cd1a97d049e7396757fef00d08db9f285" + "sha256:02e5c1b85a8b22a92f612daf2d1eea305818076b24ce02878b85e92d9ae0082e", + "sha256:0e625e147eafdaf9b6c649391059156517daa579ff231e618bd3372e16dacd41" ], - "version": "==1.9.40" + "version": "==1.9.42" }, "botocore": { "hashes": [ - "sha256:ad7f7e11aa5927a41e00b398b2c77f49d9f04da3fc509c0378f54973bb8a3421", - "sha256:cd01d12bd983c132d53b839097f6d7a9bda0d31d9dd0cb9b86566680efdad24a" + "sha256:0e495bcf2e474b82da7938b35ad2f71e28384c246b47ca131779f736621da504", + "sha256:a890509fb7625fc5c86475db58ac7db489539000cecab349d8eabf5e5777e363" ], - "version": "==1.12.40" + "version": "==1.12.42" }, "certifi": { "hashes": [ @@ -70,9 +70,10 @@ }, "cfn-flip": { "hashes": [ - "sha256:9c61039c71995ab204c005ec46d47d0f7a109e9f1b6d63569397f8bc648a8151" + "sha256:66a0bc5706ca7bd1d79b8ad8f5a23425cd1b504fc673606d5e404d1cb183401a", + "sha256:6e56eb23bf7f8d241633a3be60ce793459263445895fac4ea8ded81ad01e5016" ], - "version": "==1.0.3" + "version": "==1.0.4" }, "chardet": { "hashes": [ @@ -381,39 +382,39 @@ }, "psycopg2": { "hashes": [ - "sha256:0b9e48a1c1505699a64ac58815ca99104aacace8321e455072cee4f7fe7b2698", - "sha256:0f4c784e1b5a320efb434c66a50b8dd7e30a7dc047e8f45c0a8d2694bfe72781", - "sha256:0fdbaa32c9eb09ef09d425dc154628fca6fa69d2f7c1a33f889abb7e0efb3909", - "sha256:11fbf688d5c953c0a5ba625cc42dea9aeb2321942c7c5ed9341a68f865dc8cb1", - "sha256:19eaac4eb25ab078bd0f28304a0cb08702d120caadfe76bb1e6846ed1f68635e", - "sha256:3232ec1a3bf4dba97fbf9b03ce12e4b6c1d01ea3c85773903a67ced725728232", - "sha256:36f8f9c216fcca048006f6dd60e4d3e6f406afde26cfb99e063f137070139eaf", - "sha256:59c1a0e4f9abe970062ed35d0720935197800a7ef7a62b3a9e3a70588d9ca40b", - "sha256:6506c5ff88750948c28d41852c09c5d2a49f51f28c6d90cbf1b6808e18c64e88", - "sha256:6bc3e68ee16f571681b8c0b6d5c0a77bef3c589012352b3f0cf5520e674e9d01", - "sha256:6dbbd7aabbc861eec6b910522534894d9dbb507d5819bc982032c3ea2e974f51", - "sha256:6e737915de826650d1a5f7ff4ac6cf888a26f021a647390ca7bafdba0e85462b", - "sha256:6ed9b2cfe85abc720e8943c1808eeffd41daa73e18b7c1e1a228b0b91f768ccc", - "sha256:711ec617ba453fdfc66616db2520db3a6d9a891e3bf62ef9aba4c95bb4e61230", - "sha256:844dacdf7530c5c612718cf12bc001f59b2d9329d35b495f1ff25045161aa6af", - "sha256:86b52e146da13c896e50c5a3341a9448151f1092b1a4153e425d1e8b62fec508", - "sha256:985c06c2a0f227131733ae58d6a541a5bc8b665e7305494782bebdb74202b793", - "sha256:a86dfe45f4f9c55b1a2312ff20a59b30da8d39c0e8821d00018372a2a177098f", - "sha256:aa3cd07f7f7e3183b63d48300666f920828a9dbd7d7ec53d450df2c4953687a9", - "sha256:b1964ed645ef8317806d615d9ff006c0dadc09dfc54b99ae67f9ba7a1ec9d5d2", - "sha256:b2abbff9e4141484bb89b96eb8eae186d77bc6d5ffbec6b01783ee5c3c467351", - "sha256:cc33c3a90492e21713260095f02b12bee02b8d1f2c03a221d763ce04fa90e2e9", - "sha256:d7de3bf0986d777807611c36e809b77a13bf1888f5c8db0ebf24b47a52d10726", - "sha256:db5e3c52576cc5b93a959a03ccc3b02cb8f0af1fbbdc80645f7a215f0b864f3a", - "sha256:e168aa795ffbb11379c942cf95bf813c7db9aa55538eb61de8c6815e092416f5", - "sha256:e9ca911f8e2d3117e5241d5fa9aaa991cb22fb0792627eeada47425d706b5ec8", - "sha256:eccf962d41ca46e6326b97c8fe0a6687b58dfc1a5f6540ed071ff1474cea749e", - "sha256:efa19deae6b9e504a74347fe5e25c2cb9343766c489c2ae921b05f37338b18d1", - "sha256:f4b0460a21f784abe17b496f66e74157a6c36116fa86da8bf6aa028b9e8ad5fe", - "sha256:f93d508ca64d924d478fb11e272e09524698f0c581d9032e68958cfbdd41faef" - ], - "index": "pypi", - "version": "==2.7.5" + "sha256:03e22041bee0611408e6aad49f1bcf4406faeacffdbdc63a0f82cb7bdc5c8326", + "sha256:064404626947717dd342d5ed7a10affe64154a9a637cb22d502d84fcb39b40a2", + "sha256:07749db03fa21de99329750675dfd81b9eeed99139a6753485092d84f8371aee", + "sha256:165da83922b6fc3b027db2aba8bb444bee49af32f95b062e1523c76f605294e9", + "sha256:255a77a914b7d080359238a99371f23dd77489901cfec718448fa605f5b8dbe8", + "sha256:27c88e8fcbea1dc3777c676a21b3e6878da2b00624cb62b7f16e85fc27ce4ce5", + "sha256:28d6f1f400f8d1a4a191116943030e7558c6f51d34b9a4d06581e5d84d0eb75b", + "sha256:3138f11fccd6e1dc5b357af0d4f5016ed47d5f0ec81a4fb0f82ad8ddd460e903", + "sha256:49b96b8ef11e0f2016189ba208b5c63495c9b6ac89697eb3db97f40673d8ec65", + "sha256:4a0d29c44316e57cea3368ca1fd9dbfd82b3973aec77dcb610e487c5632418bc", + "sha256:4a658550b0bcb259e97f77f2dc93ed6b108fe2eda963a9e6fc8b48040d542ec2", + "sha256:520cf9576c6b8b45117b6367a155c27668f22860bcc34cbc77e0e52f51ded7c2", + "sha256:5bc0dc64fb26892b42653dea934f7c0b70361325647e7775528e40813b2c5f4e", + "sha256:65c123366e35e9068b9c39b6aac27e198d84c9a41bb17fa1ae8d042e0b50f04d", + "sha256:6c7e4cbc75aac67c32a6e778e215a30b8318371cdc59d7d10550296f11190fcb", + "sha256:784475327650967505cf92977f51c73b5e908c3e0f79e08a892fdbc10718baf8", + "sha256:7995bd16a61b31e922bfe3b18be24dfb63c81ec197a46c2504c45e3c5187c6e6", + "sha256:83890c9ea4e9ae032e298e2ba269660544fc73561431985d38268b3664ad4ef7", + "sha256:9501dc8412eb8b2881149d8af74a347f4d6ce85d03513be27fd56c3c066bd9dc", + "sha256:9eea6c03534aebb157a8ced5df70ba0f277d3b936d708c1654d7236e5e20edda", + "sha256:a32acdd8675e105676f5cf7b0706e2a404ddb6a2f2b8aada7646ed2df981fef6", + "sha256:bf4090882bb0bfad11033b2c262c47a9ba1cdb145981d94b873a46df69b53cb4", + "sha256:ca0cf5a443e4d0bb381d5a523fd4f155cb7414ac4c450e7b901cb04e5f59d5ed", + "sha256:ce503de4a77870dce37365d3fc224c41409f2bf1ee576159430835af4fde1d86", + "sha256:db48d28badfdd7ea62d71e8420749e66461732fa574063b4f1a1162c3d015049", + "sha256:db97bc920901745d665ccdcb4c672bf209ad13db5d19d5a1f6110b3785f1bebf", + "sha256:ed15e97b8fb6ffc791fe12c8050a0c8d84bdf48d436b8c462db3aa1766b236de", + "sha256:fc73255af56a4bf6151cd516ebd6ac1eabb922c5e44d5b8859c0ca696ba88da7", + "sha256:fe3471a0bd29595e59a4422ee332bf09ca7e15b1e6800f1b640d1c09c03ef19c", + "sha256:ff158aed5220b54461019b636a56a85b2af32670804a27f7298dae4bd715902f" + ], + "index": "pypi", + "version": "==2.7.6" }, "py": { "hashes": [ @@ -463,6 +464,14 @@ "index": "pypi", "version": "==2.6.0" }, + "pytest-mock": { + "hashes": [ + "sha256:53801e621223d34724926a5c98bd90e8e417ce35264365d39d6c896388dcc928", + "sha256:d89a8209d722b8307b5e351496830d5cc5e192336003a485443ae9adeb7dd4c0" + ], + "index": "pypi", + "version": "==1.10.0" + }, "python-dateutil": { "hashes": [ "sha256:891c38b2a02f5bb1be3e4793866c8df49c7d19baabf9c1bad62547e0b4866aca", @@ -496,10 +505,10 @@ }, "requests": { "hashes": [ - "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c", - "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279" + "sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54", + "sha256:ea881206e59f41dbd0bd445437d792e43906703fff75ca8ff43ccdb11f33f263" ], - "version": "==2.20.0" + "version": "==2.20.1" }, "s3transfer": { "hashes": [ diff --git a/biz/css/emotion_recognition.py b/biz/css/emotion_recognition.py index 7d2adc9..1865c04 100644 --- a/biz/css/emotion_recognition.py +++ b/biz/css/emotion_recognition.py @@ -1,41 +1,20 @@ -import cognitive_face as CF -import locale - - -class SatisfactionScore: - def __init__(self): - KEY = 'fcef05be3b9f440f9e38dfb675b07de6' - BASE = 'https://westcentralus.api.cognitive.microsoft.com/face/v1.0' - self._cognitive_face = CF - self._cognitive_face.Key.set(KEY) - self._cognitive_face.BaseUrl.set(BASE) - - def detect_from_url(self, url): - return self.detect(url) - - def detect_from_local_file(self, filepath): - # with open(filepath, "rb") as image: - # image_as_bytes = image.read() - locale.getdefaultlocale() - - f = open(filepath, "rb") - image_as_bytes = f.read() - f.close() +""" +Adapter for the microsoft face recignition - return self.detect(image_as_bytes) +Author: David Niwczyk and Robin Wohlers-Reichel +Date: 11/11/2018 +""" - def detect(self, image, face_id=True, - landmarks=False, attributes='emotion,glasses'): - return self._cognitive_face.face.detect( - image, face_id, landmarks, attributes - ) - - -if __name__ == "__main__": - css = SatisfactionScore() - - print(css.detect_from_url(""" - https://raw.githubusercontent.com/Microsoft/ - Cognitive-Face-Windows/master/Data/detection1.jpg""")) - - # print(css.detect_from_local_file("face.jpg")) +import cognitive_face as CF +import config + + +def detect_from_url(url): # pragma: no cover + """ + Hit the CF api for the image at 'url'. + """ + KEY = config.cf_api_key() + BASE = 'https://westcentralus.api.cognitive.microsoft.com/face/v1.0' + CF.Key.set(KEY) + CF.BaseUrl.set(BASE) + return CF.face.detect(url, True, False, 'emotion,glasses') diff --git a/config/__init__.py b/config/__init__.py index 73aae16..37f2589 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -12,3 +12,7 @@ def connection_string(): # pragma: no cover def is_running_on_lambda(): root = os.environ.get("LAMBDA_TASK_ROOT", '') return len(root) > 0 + + +def cf_api_key(): # pragma: no cover + return os.environ.get("CF_API_KEY", 'fcef05be3b9f440f9e38dfb675b07de6') diff --git a/events/satisfaction_lambda.py b/events/satisfaction_lambda.py index c53d222..86c1254 100644 --- a/events/satisfaction_lambda.py +++ b/events/satisfaction_lambda.py @@ -1,40 +1,144 @@ +""" +Lambda which converts images into css +Triggers on s3 bucket event +Upload the image to Azure +Put the CSS into the db + +Author: Robin Wohlers-Reichel +Date: 11/11/2018 +""" + import urllib.parse import boto3 -from biz.css.emotion_recognition import SatisfactionScore -from biz.css.reduction import apply_reduction +import biz.css.emotion_recognition as er +import biz.css.reduction as r +import biz.css.manage_satisfaction as ms +import biz.css.file_storage as fs +from psycopg2 import pool +import config print('Loading function') s3 = boto3.client('s3') +__pool = None -# second parameter is the context, but currently unused -def customer_satisfaction(event, _): +def get_pool_lazy(): # pragma: no cover + """ + Get a database pool, but in a way we can override for tests + """ + # pylint: disable=global-statement + global __pool + if __pool is None: + __pool = pool.ThreadedConnectionPool(1, 1, config.connection_string()) + return __pool + + +def get_details(event): + """ + Get details of the event being passed into the lambda + + :param event: Event object passed into the lambda + :return: Name of the bucket and key of file (filename) + """ bucket = event['Records'][0]['s3']['bucket']['name'] key = urllib.parse.unquote_plus( event['Records'][0]['s3']['object']['key'], encoding='utf-8' ) + return (bucket, key) + + +def generate_url(s3client, bucket, key): # pragma: no cover + """ + Generate a short-lived public url for the specified key + + :param s3client: AWS S3 client to use to generate url + :param bucket: The bucket the object resides in + :param key: The key of the object + :return: Public url to the object + """ + return s3client.generate_presigned_url( + ClientMethod='get_object', + ExpiresIn='60', + Params={ + 'Bucket': bucket, + 'Key': key + } + ) + + +def css_for_image_at_url(url): + """ + Calculate the css for an image at `url` + + :param url: The url to use + :return: CSS score + """ + css = er.detect_from_url(url) + print("Cognitive Face Result: {}".format(css)) + return r.apply_reduction(css) + + +def save_css(p, css, eid, rid): + """ + Save the CSS to the database + + :param p: Database pool + :param css: CSS Score + :param eid: Event ID + :param rid: Reservation ID + """ + conn = p.getconn() try: + ms.create_satisfaction(conn, css, eid, rid) + conn.commit() + except Exception as e: + conn.rollback() + raise e + finally: + p.putconn(conn) + - url = s3.generate_presigned_url( - ClientMethod='get_object', - ExpiresIn='600', - Params={ - 'Bucket': bucket, - 'Key': key - } +# second parameter is the context, but currently unused +def calculate_css_from_image(event, _): + """ + The lambda event handler entry point + + :param event: Information about the trigger + """ + bucket, key = get_details(event) + eid = None + rid = None + try: + eid, rid = fs.deconstruct_filename(key) + except ValueError as e: + print(e) + print('Error deconstructing filename {} into \ + event id and reservation id.'.format(key)) + return dict( + event_id=eid, + reservation_id=rid, + css=None, + result='invalid_filename' ) - print('detecting from url:{}.'.format(url)) - css = SatisfactionScore().detect_from_url(url) - print(css) - reduced = apply_reduction(css) - print(reduced) + print('Calculate CSS for Event={}, Reservation={}'.format(eid, rid)) - return css + url = None + try: + url = generate_url(s3, bucket, key) except Exception as e: print(e) print('Error getting object {} from bucket {}.\ Make sure they exist and your bucket is in the \ same region as this function.'.format(key, bucket)) raise e + + reduced = css_for_image_at_url(url) + save_css(get_pool_lazy(), reduced, eid, rid) + return dict( + event_id=eid, + reservation_id=rid, + css=reduced, + result='success' + ) diff --git a/test/biz/test_manage_satisfaction.py b/test/biz/test_manage_satisfaction.py index 16e9d7e..cd13da5 100644 --- a/test/biz/test_manage_satisfaction.py +++ b/test/biz/test_manage_satisfaction.py @@ -7,36 +7,8 @@ import datetime import biz.css.manage_satisfaction as ms import biz.manage_restaurant as mr -import biz.manage_staff as mgs +import test.helper as h import biz.manage_menu as mm -from biz.staff import Permission -from biz.restaurant_table import (Coordinate, Shape) - - -def __spoof_tables(db_conn, n, username, first, last): - """Load a series of restaurant tables and a staff member. - - :param db_conn: A psycopg2 connection to the database. - :param n: The number of restaurant_tables to create. - :param username: The username for the staff member. - :param first: First name of staff member. - :param last: Last name of staff member. - :return: ([t1_id, t2_id ... tn_id], staff_id) - """ - staff_id = mgs.create_staff_member( - db_conn, username, 'prettygood', (first, last), - Permission.wait_staff - ) - - tables = [] - for _ in range(0, n): - tables.append( - mr.create_restaurant_table( - db_conn, 2, Coordinate(x=0, y=3), 1, - 5, Shape.rectangle, staff_id - )[0] - ) - return (tables, staff_id) def __spoof_menu_items(db_conn, n): @@ -94,7 +66,7 @@ def __update_reservation_dt(db_conn, rid, eid, dt): def test_create_satisfaction(database_snapshot): """Attempt to create a satisfaction order.""" with database_snapshot.getconn() as conn: - t, staff = __spoof_tables(conn, 1, 'ldavid', 'Lt', 'David') + t, staff = h.spoof_tables(conn, 1) conn.commit() (e1, r1) = mr.create_reservation(conn, t[0], staff, 5) ms.create_satisfaction(conn, 100, e1, r1) @@ -104,7 +76,7 @@ def test_create_satisfaction(database_snapshot): def test_lookup_missing_satisfaction(database_snapshot): """Attempt to lookup a missing satisfaction.""" with database_snapshot.getconn() as conn: - t, staff = __spoof_tables(conn, 1, 'ldavid', 'Lt', 'David') + t, staff = h.spoof_tables(conn, 1) conn.commit() (e1, r1) = mr.create_reservation(conn, t[0], staff, 5) assert ms.lookup_satisfaction(conn, e1, r1) is None @@ -113,7 +85,7 @@ def test_lookup_missing_satisfaction(database_snapshot): def test_create_multiple_satisfaction(database_snapshot): """Create a satisfaciton record for multiple customer events.""" with database_snapshot.getconn() as conn: - t, staff = __spoof_tables(conn, 1, 'ldavid', 'Lt', 'David') + t, staff = h.spoof_tables(conn, 1) conn.commit() ce1 = mr.create_reservation(conn, t[0], staff, 5) @@ -133,7 +105,7 @@ def test_create_multiple_satisfaction(database_snapshot): def test_avg_css_per_period(database_snapshot): """Retrieve average css on and between 2018-01-01 and 2018-12-31""" with database_snapshot.getconn() as conn: - t, staff = __spoof_tables(conn, 1, 'ldavid', 'Lt', 'David') + t, staff = h.spoof_tables(conn, 1) conn.commit() dt1 = datetime.datetime(2018, 1, 1) @@ -158,7 +130,7 @@ def test_avg_css_per_period(database_snapshot): def test_missing_avg_css_per_period(database_snapshot): """Retrieve missing average css on 2018-12-31""" with database_snapshot.getconn() as conn: - t, staff = __spoof_tables(conn, 1, 'ldavid', 'Lt', 'David') + t, staff = h.spoof_tables(conn, 1) conn.commit() dt1 = datetime.datetime(2018, 1, 1) @@ -174,8 +146,8 @@ def test_missing_avg_css_per_period(database_snapshot): def test_avg_css_per_staff(database_snapshot): """Retrieve average css for specified staff""" with database_snapshot.getconn() as conn: - t, staff = __spoof_tables(conn, 1, 'ldavid', 'Lt', 'David') - t2, staff2 = __spoof_tables(conn, 1, 'lsarge', 'Lt', 'Sarge') + t, staff = h.spoof_tables(conn, 1) + t2, staff2 = h.spoof_tables(conn, 1, 'lsarge', 'Lt', 'Sarge') conn.commit() @@ -199,7 +171,7 @@ def test_avg_css_per_staff(database_snapshot): def test_missing_avg_css_per_staff(database_snapshot): """Retrieve missing average css for specified staff""" with database_snapshot.getconn() as conn: - t, staff = __spoof_tables(conn, 1, 'ldavid', 'Lt', 'David') + t, staff = h.spoof_tables(conn, 1) conn.commit() @@ -211,8 +183,8 @@ def test_missing_avg_css_per_staff(database_snapshot): def test_avg_css_all_staff(database_snapshot): """Retrieve average css for all staff""" with database_snapshot.getconn() as conn: - t, staff = __spoof_tables(conn, 1, 'ldavid', 'Lt', 'David') - t2, staff2 = __spoof_tables(conn, 1, 'lsarge', 'Lt', 'Sarge') + t, staff = h.spoof_tables(conn, 1) + t2, staff2 = h.spoof_tables(conn, 1, 'lsarge', 'Lt', 'Sarge') conn.commit() @@ -237,7 +209,7 @@ def test_avg_css_all_staff(database_snapshot): def test_missing_avg_css_all_staff(database_snapshot): """Retrieve missing average css for all staff""" with database_snapshot.getconn() as conn: - t, staff = __spoof_tables(conn, 1, 'ldavid', 'Lt', 'David') + t, staff = h.spoof_tables(conn, 1) conn.commit() @@ -249,7 +221,7 @@ def test_missing_avg_css_all_staff(database_snapshot): def test_avg_css_per_menu_item(database_snapshot): """Retrieve average css for specified menu item""" with database_snapshot.getconn() as conn: - t, staff = __spoof_tables(conn, 1, 'ldavid', 'Lt', 'David') + t, staff = h.spoof_tables(conn, 1) conn.commit() assert ms.avg_css_per_menu_item(conn, 1) is None @@ -270,7 +242,7 @@ def test_avg_css_per_menu_item(database_snapshot): def test_missing_avg_css_per_menu_item(database_snapshot): """Retrieve missing average css for specified menu item""" with database_snapshot.getconn() as conn: - t, staff = __spoof_tables(conn, 1, 'ldavid', 'Lt', 'David') + t, staff = h.spoof_tables(conn, 1) conn.commit() mr.create_reservation(conn, t[0], staff, 5) diff --git a/test/css/test_lambda.py b/test/css/test_lambda.py new file mode 100644 index 0000000..228f1b6 --- /dev/null +++ b/test/css/test_lambda.py @@ -0,0 +1,215 @@ +""" +Test the CSS lambda + +Author: Robin Wohlers-Reichel +Date: 11/11/2018 +""" + +import events.satisfaction_lambda as sl +import biz.css.manage_satisfaction as ms +import biz.manage_restaurant as mr +import biz.css.reduction as r +import biz.css.emotion_recognition as er +import pytest +import test.helper as h + +# pylint: disable=no-member + + +def test_get_details(): + """ + Check the details are read from the filename + """ + event = {'Records': [ + { + 's3': { + 'bucket': { + 'name': 'css-bucket' + }, + 'object': { + 'key': '2-3.img' + } + } + } + ]} + bucket, key = sl.get_details(event) + assert bucket == "css-bucket" + assert key == "2-3.img" + + +def test_get_details_throw(): + """ + Correctly handle malformed filenames + """ + event = {'Records': [ + { + 's3': { + 'no_bucket': { + 'name': 'css-bucket' + }, + 'object': { + 'key': '2-3.img' + } + } + } + ]} + with pytest.raises(KeyError): + sl.get_details(event) + + +def test_save_css_exception(mocker): + """ + Do not eat database exceptions + """ + mocker.patch('biz.css.manage_satisfaction.create_satisfaction') + ms.create_satisfaction.side_effect = Exception('oh no') + dodgy_pool = h.mock_db_pool(mocker) + with pytest.raises(Exception) as execinfo: + sl.save_css(dodgy_pool, 50, 11, 22) + ms.create_satisfaction.assert_called_once_with(dodgy_pool.conn, + 50, + 11, + 22) + assert str(execinfo.value) == 'oh no' + dodgy_pool.getconn.assert_called_once() + dodgy_pool.putconn.assert_called_once() + dodgy_pool.conn.commit.assert_not_called() + dodgy_pool.conn.rollback.assert_called_once() + + +def test_save_css(mocker): + """ + Saving css using correct biz methods + """ + mocker.patch('biz.css.manage_satisfaction.create_satisfaction') + dodgy_pool = h.mock_db_pool(mocker) + sl.save_css(dodgy_pool, 50, 11, 22) + ms.create_satisfaction.assert_called_once_with(dodgy_pool.conn, + 50, + 11, + 22) + dodgy_pool.getconn.assert_called_once() + dodgy_pool.putconn.assert_called_once() + dodgy_pool.conn.commit.assert_called_once() + dodgy_pool.conn.rollback.assert_not_called() + + +def test_save_css_with_database(database_snapshot): + """ + Saving css using db snapshot. check transaction etc + """ + # setup db + setup_conn = database_snapshot.getconn() + t, staff = h.spoof_tables(setup_conn, 1) + setup_conn.commit() + (e1, r1) = mr.create_reservation(setup_conn, t[0], staff, 5) + setup_conn.commit() + database_snapshot.putconn(setup_conn) + setup_conn = None + + # run test + sl.save_css(database_snapshot, 50, e1, r1) + + # assert result + conn = database_snapshot.getconn() + try: + score = ms.lookup_satisfaction(conn, e1, r1) + assert score == 50 + finally: + database_snapshot.putconn(conn) + + +def test_event_css(mocker): + """ + Test whole thing through + """ + mock_image_url = 'https://www.example.com/1-2.img' + + mocker.patch('events.satisfaction_lambda.generate_url') + sl.generate_url.return_value = mock_image_url + + mocker.patch('events.satisfaction_lambda.css_for_image_at_url') + sl.css_for_image_at_url.return_value = 50 + + mocker.patch('events.satisfaction_lambda.save_css') + mocker.patch('events.satisfaction_lambda.get_pool_lazy') + sl.get_pool_lazy.return_value = 'super-legit-pool' + + event = {'Records': [ + { + 's3': { + 'bucket': { + 'name': 'css-bucket' + }, + 'object': { + 'key': '2-3.img' + } + } + } + ]} + sl.calculate_css_from_image(event, None) + + sl.css_for_image_at_url.assert_called_once_with(mock_image_url) + sl.save_css.assert_called_once_with('super-legit-pool', 50, 2, 3) + + +def test_event_css_bad_filename(mocker): + """ + Bad filename means no call to Azure + """ + mocker.patch('events.satisfaction_lambda.generate_url') + event = {'Records': [ + { + 's3': { + 'bucket': { + 'name': 'css-bucket' + }, + 'object': { + 'key': 'wew.img' + } + } + } + ]} + sl.calculate_css_from_image(event, None) + sl.generate_url.assert_not_called() + + +def test_event_css_generate_failed(mocker): + """ + Throws exception while generating URL + """ + mocker.patch('events.satisfaction_lambda.generate_url') + sl.generate_url.side_effect = Exception('oh no') + mocker.patch('events.satisfaction_lambda.save_css') + event = {'Records': [ + { + 's3': { + 'bucket': { + 'name': 'css-bucket' + }, + 'object': { + 'key': '1-2.img' + } + } + } + ]} + with pytest.raises(Exception) as execinfo: + sl.calculate_css_from_image(event, None) + assert str(execinfo.value) == 'oh no' + sl.save_css.assert_not_called() + + +def test_css_for_image_at_url(mocker): + """ + Make sure we are getting features, then reducing + """ + mocker.patch('biz.css.emotion_recognition.detect_from_url') + + mocker.patch('biz.css.reduction.apply_reduction') + r.apply_reduction.return_value = 50 + + result = sl.css_for_image_at_url('bingo') + assert result == 50 + + er.detect_from_url.assert_called_once() + r.apply_reduction.assert_called_once() diff --git a/test/helper.py b/test/helper.py new file mode 100644 index 0000000..75ae4bf --- /dev/null +++ b/test/helper.py @@ -0,0 +1,66 @@ +import biz.manage_staff as ms +import biz.manage_restaurant as mr +from biz.staff import Permission +from biz.restaurant_table import (Coordinate, Shape) + + +def spoof_user(client): + """A user must be present and 'logged in'. + + :param client: The client running the flask app. + :return: staff_id + """ + pool = client.testing_db_pool + conn = pool.getconn() + s_id = ms.create_staff_member( + conn, 'rrobot', 'password', ('Roberto', 'Robot'), + Permission.robot + ) + conn.commit() + pool.putconn(conn) + with client.session_transaction() as sess: + sess['username'] = 'rrobot' + return s_id + + +def spoof_tables(db_conn, n, username='ldavid', first='Larry', last='David'): + """Load a series of restaurant tables and a staff member. + + :param db_conn: A psycopg2 connection to the database. + :param n: The number of restaurant_tables to create. + :return: ([t1_id, t2_id ... tn_id], staff_id) + """ + staff_id = ms.create_staff_member( + db_conn, username, 'prettygood', (first, last), + Permission.wait_staff + ) + + tables = [] + for _ in range(0, n): + tables.append( + mr.create_restaurant_table( + db_conn, 2, Coordinate(x=0, y=3), 1, + 5, Shape.rectangle, staff_id + )[0] + ) + return (tables, staff_id) + + +def mock_db_pool(mocker): + """ + Make a mock db pool to test commit/rollback being called + """ + # pylint: disable=too-few-public-methods + class MockConn(): + def __init__(self): + self.rollback = mocker.stub(name='conn_rollback') + self.commit = mocker.stub(name='conn_commit') + + class MockPool(): + def __init__(self): + self.conn = MockConn() + self.getconn = mocker.stub(name='getconn_stub') + self.getconn.return_value = self.conn + self.putconn = mocker.stub(name='putconn_stub') + + return MockPool() diff --git a/test/web/helper.py b/test/web/helper.py deleted file mode 100644 index e236981..0000000 --- a/test/web/helper.py +++ /dev/null @@ -1,21 +0,0 @@ -import biz.manage_staff as ms -from biz.staff import Permission - - -def spoof_user(client): - """A user must be present and 'logged in'. - - :param client: The client running the flask app. - :return: staff_id - """ - pool = client.testing_db_pool - conn = pool.getconn() - s_id = ms.create_staff_member( - conn, 'rrobot', 'password', ('Roberto', 'Robot'), - Permission.robot - ) - conn.commit() - pool.putconn(conn) - with client.session_transaction() as sess: - sess['username'] = 'rrobot' - return s_id diff --git a/test/web/test_error.py b/test/web/test_error.py index c59737f..25ed470 100644 --- a/test/web/test_error.py +++ b/test/web/test_error.py @@ -4,7 +4,7 @@ Author: Andrew Pope Date: 06/11/2018 """ -from test.web.helper import spoof_user +from test.helper import spoof_user def test_404(client): diff --git a/test/web/test_robot.py b/test/web/test_robot.py index 30a7369..04b5000 100644 --- a/test/web/test_robot.py +++ b/test/web/test_robot.py @@ -5,7 +5,7 @@ Shape, Coordinate ) import pytest -from test.web.helper import spoof_user +from test.helper import spoof_user def test_index(client): diff --git a/zappa_settings.json b/zappa_settings.json index b97f3e7..fa1eedb 100644 --- a/zappa_settings.json +++ b/zappa_settings.json @@ -26,7 +26,7 @@ "s3_bucket": "irs-zappa", "keep_warm": false, "events": [{ - "function": "events.satisfaction_lambda.customer_satisfaction", + "function": "events.satisfaction_lambda.calculate_css_from_image", "event_source": { "arn": "arn:aws:s3:::irs-images", "events": [