From 7241a6d2b1328614711ced2ae690b3e982fa2b8d Mon Sep 17 00:00:00 2001 From: Shelby Potts Date: Mon, 17 Feb 2025 18:40:35 -0600 Subject: [PATCH 1/2] created a basic user importer class with tests --- .env_example | 3 +- .gitignore | 4 +- app/importers/UserImporter.py | 19 ++++ app/importers/__init__.py | 0 tests/importers/__init__.py | 0 tests/importers/test_UserImporter.py | 85 +++++++++++++++ tests/test_MatchAnalytics.py | 36 ------ tests/test_matches.json | 126 --------------------- tests/test_user.json | 157 --------------------------- 9 files changed, 109 insertions(+), 321 deletions(-) create mode 100644 app/importers/UserImporter.py create mode 100644 app/importers/__init__.py create mode 100644 tests/importers/__init__.py create mode 100644 tests/importers/test_UserImporter.py delete mode 100644 tests/test_MatchAnalytics.py delete mode 100644 tests/test_matches.json delete mode 100644 tests/test_user.json diff --git a/.env_example b/.env_example index 4438f80..6e042bc 100644 --- a/.env_example +++ b/.env_example @@ -1,2 +1,3 @@ HOST=0.0.0.0 -PORT=8050 \ No newline at end of file +PORT=8050 +USER_FILE_PATH='file/path/user.json' \ No newline at end of file diff --git a/.gitignore b/.gitignore index f1d79e3..4e7ff0b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ data/ *__pycache__/ *.pyc ./.pytest_cache -.env \ No newline at end of file +.env +.vscode +.pytest_cache \ No newline at end of file diff --git a/app/importers/UserImporter.py b/app/importers/UserImporter.py new file mode 100644 index 0000000..1747d2e --- /dev/null +++ b/app/importers/UserImporter.py @@ -0,0 +1,19 @@ +import os +import json + +from app.tools.Logger import logger + +class UserImporter: + def __init__(self): + self.user_file_path = os.environ.get("USER_FILE_PATH") + if self.user_file_path is None: + raise Exception("USER_FILE_PATH environment variable is not set.") + + if '.json' not in self.user_file_path: + raise Exception("The user file needs to be a JSON file.") + + with open(self.user_file_path, 'r') as file: + user_data = json.load(file) + logger.info(f"Imported user data from {self.user_file_path}.") + + self.user_data = user_data \ No newline at end of file diff --git a/app/importers/__init__.py b/app/importers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/importers/__init__.py b/tests/importers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/importers/test_UserImporter.py b/tests/importers/test_UserImporter.py new file mode 100644 index 0000000..93e3196 --- /dev/null +++ b/tests/importers/test_UserImporter.py @@ -0,0 +1,85 @@ +import pytest +import os +import json +from unittest.mock import mock_open, patch + +from app.importers.UserImporter import UserImporter + + +######################################################################################### +# test values +######################################################################################### +USER_FILE_PATH = "fake/file/path/users.json" +USER_DATA = ''' +{ + "devices": [ + { + "ip_address": "174.234.168.00", + "device_model": "unknown", + "device_platform": "ios", + "device_os_versions": "16.5.1" + } + ], + "account": { + "signup_time": "2001-06-29 03:27:17.539", + "last_pause_time": "2003-09-04 03:04:32", + "last_unpause_time": "2020-12-10 16:53:40", + "last_seen": "2024-01-17 04:07:39", + "device_platform": "ios", + "device_os": "16.6.1", + "device_model": "unknown", + "app_version": "9.30.0", + "push_notifications_enabled": false + } +} +''' + +######################################################################################### +# pytest fixtures +######################################################################################### +@pytest.fixture +def user_importer(monkeypatch): + monkeypatch.setenv("USER_FILE_PATH", USER_FILE_PATH) + + with patch("builtins.open", mock_open(read_data=USER_DATA)) as mock_file, \ + patch("json.load", return_value=json.loads(USER_DATA)) as mock_json_load: + + user_importer = UserImporter() + return user_importer + + +######################################################################################### +# unit tests +######################################################################################### +def test_exists(user_importer): + assert user_importer is not None + +def test_user_file_path_not_set(): + if "USER_FILE_PATH" in os.environ: + del os.environ["USER_FILE_PATH"] + + with pytest.raises(Exception, match="USER_FILE_PATH environment variable is not set."): + UserImporter() + +def test_user_file_not_json(): + os.environ["USER_FILE_PATH"] = "invalid_file.txt" # invalid value + + with pytest.raises(Exception, match="The user file needs to be a JSON file."): + UserImporter() + +def test_user_importer_loads_data(user_importer): + assert isinstance(user_importer.user_data, dict) + assert "devices" in user_importer.user_data + assert "account" in user_importer.user_data + +def test_device_data(user_importer): + device = user_importer.user_data["devices"][0] + assert device["ip_address"] == "174.234.168.00" + assert device["device_platform"] == "ios" + assert device["device_os_versions"] == "16.5.1" + +def test_account_data(user_importer): + account = user_importer.user_data["account"] + assert account["device_os"] == "16.6.1" + assert account["app_version"] == "9.30.0" + assert account["push_notifications_enabled"] is False diff --git a/tests/test_MatchAnalytics.py b/tests/test_MatchAnalytics.py deleted file mode 100644 index fa706d2..0000000 --- a/tests/test_MatchAnalytics.py +++ /dev/null @@ -1,36 +0,0 @@ -import unittest -import app.analytics.MatchAnalytics as MatchAnalytics - -USER_FILE_PATH = 'tests/test_user.json' -MATCHES_FILE_PATH = 'tests/test_matches.json' - - -class AnalyticsTest(unittest.TestCase): - def test_total_event_count(self): - test_events = MatchAnalytics.prepare_uploaded_match_data(MATCHES_FILE_PATH) - total_events = MatchAnalytics.total_counts(test_events) - self.assertEqual(total_events.size, 8) - - def test_invalid_file_type(self): - with self.assertRaises(ValueError): - MatchAnalytics.prepare_uploaded_match_data('tests/matches.csv') - - def test_invalid_match_file_upload(self): - with self.assertRaises(ValueError): - MatchAnalytics.prepare_uploaded_match_data(USER_FILE_PATH) - - def test_invalid_user_file_upload(self): - with self.assertRaises(ValueError): - MatchAnalytics.import_user_account_data(MATCHES_FILE_PATH) - - def test_account_data_import(self): - results = MatchAnalytics.import_user_account_data(USER_FILE_PATH) - self.assertEqual(len(results), 9) # 9 keys in the dictionary - - def test_device_data_import(self): - results = MatchAnalytics.import_user_device_data(USER_FILE_PATH) - self.assertEqual(len(results), 5) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_matches.json b/tests/test_matches.json deleted file mode 100644 index 158d0e0..0000000 --- a/tests/test_matches.json +++ /dev/null @@ -1,126 +0,0 @@ -[ - { - "like": [ - { - "content": "[{\"photo\":{\"url\":\"test_data/sulley_sunglasses.jpg\",\"boundingBox\":{\"bottomRight\":{\"x\":1.0,\"y\":1.0},\"topLeft\":{\"x\":0.0,\"y\":0.0}},\"caption\":\"\",\"location\":\"\",\"promptId\":\"\",\"cdnId\":\"hfy4sksrpr6anuhyr7l2\",\"contentId\":\"f85e0278-554c-41d3-9f52-6cc217c90ee9\"},\"prompt\":{\"question\":\"\",\"answer\":\"\"},\"comment\":\"Love the sleeve!\",\"video\":{\"url\":\"\",\"thumbnail\":\"\",\"boundingBox\":{\"bottomRight\":{\"x\":0.0,\"y\":0.0},\"topLeft\":{\"x\":0.0,\"y\":0.0}},\"caption\":\"\",\"location\":\"\",\"promptId\":\"\",\"cdnId\":\"\",\"contentId\":\"\"},\"voicePrompt\":{\"question\":\"\",\"url\":\"\",\"waveform\":\"\",\"cdnId\":\"\"},\"promptPoll\":{\"contentId\":\"\",\"questionId\":\"\",\"options\":[],\"selectedOptionIndex\":{\"int\":0},\"selectedOption\":\"\"},\"videoPrompt\":{\"contentId\":\"\",\"cdnId\":\"\",\"videoUrl\":\"\",\"questionId\":\"\",\"thumbnailUrl\":\"\",\"boundingBox\":{\"bottomRight\":{\"x\":0.0,\"y\":0.0},\"topLeft\":{\"x\":0.0,\"y\":0.0}}}}]", - "timestamp": "2023-10-08 22:51:39", - "like": [ - { - "timestamp": "2023-10-08 22:51:39", - "comment": "Hello, World!" - } - ], - "type": "like" - } - ] - }, - { - "like": [ - { - "content": "[{\"photo\":{\"url\":\"test_data/sulley_sunglasses.jpg\",\"boundingBox\":{\"bottomRight\":{\"x\":1.0,\"y\":1.0},\"topLeft\":{\"x\":0.0,\"y\":0.0}},\"caption\":\"\",\"location\":\"\",\"promptId\":\"\",\"cdnId\":\"hfy4sksrpr6anuhyr7l2\",\"contentId\":\"f85e0278-554c-41d3-9f52-6cc217c90ee9\"},\"prompt\":{\"question\":\"\",\"answer\":\"\"},\"comment\":\"Love the sleeve!\",\"video\":{\"url\":\"\",\"thumbnail\":\"\",\"boundingBox\":{\"bottomRight\":{\"x\":0.0,\"y\":0.0},\"topLeft\":{\"x\":0.0,\"y\":0.0}},\"caption\":\"\",\"location\":\"\",\"promptId\":\"\",\"cdnId\":\"\",\"contentId\":\"\"},\"voicePrompt\":{\"question\":\"\",\"url\":\"\",\"waveform\":\"\",\"cdnId\":\"\"},\"promptPoll\":{\"contentId\":\"\",\"questionId\":\"\",\"options\":[],\"selectedOptionIndex\":{\"int\":0},\"selectedOption\":\"\"},\"videoPrompt\":{\"contentId\":\"\",\"cdnId\":\"\",\"videoUrl\":\"\",\"questionId\":\"\",\"thumbnailUrl\":\"\",\"boundingBox\":{\"bottomRight\":{\"x\":0.0,\"y\":0.0},\"topLeft\":{\"x\":0.0,\"y\":0.0}}}}]", - "timestamp": "2023-10-09 22:51:39", - "like": [ - { - "timestamp": "2023-10-08 22:51:39", - "comment": "Hello, World again!" - } - ], - "type": "like" - } - ] - }, - { - "like": [ - { - "content": "[{\"photo\":{\"url\":\"test_data/sulley_sunglasses.jpg\",\"boundingBox\":{\"bottomRight\":{\"x\":1.0,\"y\":1.0},\"topLeft\":{\"x\":0.0,\"y\":0.0}},\"caption\":\"\",\"location\":\"\",\"promptId\":\"\",\"cdnId\":\"hfy4sksrpr6anuhyr7l2\",\"contentId\":\"f85e0278-554c-41d3-9f52-6cc217c90ee9\"},\"prompt\":{\"question\":\"\",\"answer\":\"\"},\"comment\":\"Love the sleeve!\",\"video\":{\"url\":\"\",\"thumbnail\":\"\",\"boundingBox\":{\"bottomRight\":{\"x\":0.0,\"y\":0.0},\"topLeft\":{\"x\":0.0,\"y\":0.0}},\"caption\":\"\",\"location\":\"\",\"promptId\":\"\",\"cdnId\":\"\",\"contentId\":\"\"},\"voicePrompt\":{\"question\":\"\",\"url\":\"\",\"waveform\":\"\",\"cdnId\":\"\"},\"promptPoll\":{\"contentId\":\"\",\"questionId\":\"\",\"options\":[],\"selectedOptionIndex\":{\"int\":0},\"selectedOption\":\"\"},\"videoPrompt\":{\"contentId\":\"\",\"cdnId\":\"\",\"videoUrl\":\"\",\"questionId\":\"\",\"thumbnailUrl\":\"\",\"boundingBox\":{\"bottomRight\":{\"x\":0.0,\"y\":0.0},\"topLeft\":{\"x\":0.0,\"y\":0.0}}}}]", - "timestamp": "2023-11-10 22:51:39", - "like": [ - { - "timestamp": "2023-11-10 22:51:39", - "comment": "Hi there!" - } - ], - "type": "like" - } - ] - }, - { - "like": [ - { - "content": "[{\"photo\":{\"url\":\"test_data/sulley_sunglasses.jpg\",\"boundingBox\":{\"bottomRight\":{\"x\":1.0,\"y\":1.0},\"topLeft\":{\"x\":0.0,\"y\":0.0}},\"caption\":\"\",\"location\":\"\",\"promptId\":\"\",\"cdnId\":\"hfy4sksrpr6anuhyr7l2\",\"contentId\":\"f85e0278-554c-41d3-9f52-6cc217c90ee9\"},\"prompt\":{\"question\":\"\",\"answer\":\"\"},\"comment\":\"Love the sleeve!\",\"video\":{\"url\":\"\",\"thumbnail\":\"\",\"boundingBox\":{\"bottomRight\":{\"x\":0.0,\"y\":0.0},\"topLeft\":{\"x\":0.0,\"y\":0.0}},\"caption\":\"\",\"location\":\"\",\"promptId\":\"\",\"cdnId\":\"\",\"contentId\":\"\"},\"voicePrompt\":{\"question\":\"\",\"url\":\"\",\"waveform\":\"\",\"cdnId\":\"\"},\"promptPoll\":{\"contentId\":\"\",\"questionId\":\"\",\"options\":[],\"selectedOptionIndex\":{\"int\":0},\"selectedOption\":\"\"},\"videoPrompt\":{\"contentId\":\"\",\"cdnId\":\"\",\"videoUrl\":\"\",\"questionId\":\"\",\"thumbnailUrl\":\"\",\"boundingBox\":{\"bottomRight\":{\"x\":0.0,\"y\":0.0},\"topLeft\":{\"x\":0.0,\"y\":0.0}}}}]", - "timestamp": "2023-09-09 22:51:39", - "like": [ - { - "timestamp": "2023-10-08 22:51:39", - "comment": "Hi!" - } - ], - "type": "like" - } - ] - }, - { - "like": [ - { - "timestamp": "2023-07-15T12:54:04", - "type": "like" - } - ] - }, - { - "like": [ - { - "timestamp": "2023-07-20T12:54:04", - "type": "like" - } - ] - }, - { - "like": [ - { - "timestamp": "2023-07-25T12:54:04", - "type": "like" - } - ] - }, - { - "like": [ - { - "timestamp": "2023-07-30T12:54:04", - "type": "like" - } - ] - }, - { - "block": [ - { - "block_type": "remove", - "timestamp": "2023-08-17T13:25:48", - "type": "block" - } - ], - "chats": [ - { - "body": "Hello", - "timestamp": "2023-08-08T23:42:01", - "type": "chats" - }, - { - "body": "How are you?", - "timestamp": "2023-08-10T04:09:26", - "type": "chats" - }, - { - "body": "Let's grab coffee. My number is 555-555-5555", - "timestamp": "2023-08-12T04:04:52", - "type": "chats" - } - ], - "match": [ - { - "timestamp": "2023-08-08T23:41:29", - "type": "match" - } - ] - } -] \ No newline at end of file diff --git a/tests/test_user.json b/tests/test_user.json deleted file mode 100644 index 257a071..0000000 --- a/tests/test_user.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "devices": [ - { - "ip_address": "174.242.128.24", - "device_model": "unknown", - "device_platform": "ios", - "device_os_versions": "16.5.1" - }, - { - "ip_address": "174.231.89.0", - "device_model": "unknown", - "device_platform": "ios", - "device_os_versions": "17.1.1" - }, - { - "ip_address": "174.215.23.182", - "device_model": "unknown", - "device_platform": "ios", - "device_os_versions": "16.6.1" - }, - { - "ip_address": "174.231.92.217", - "device_model": "unknown", - "device_platform": "ios", - "device_os_versions": "17.1.2" - }, - { - "ip_address": "174.231.90.225", - "device_model": "unknown", - "device_platform": "ios", - "device_os_versions": "16.6.1" - } - ], - "account": { - "signup_time": "2023-06-29 03:27:17.539", - "last_pause_time": "2023-12-04 03:04:32", - "last_unpause_time": "2023-12-04 16:53:40", - "last_seen": "2024-01-17 04:07:39", - "device_platform": "ios", - "device_os": "16.6.1", - "device_model": "unknown", - "app_version": "9.30.0", - "push_notifications_enabled": false - }, - "profile": { - "first_name": "Shelby", - "age": 99, - "height_centimeters": 213, - "gender": "female", - "gender_identity": "[\"Prefer Not to Say\"]", - "gender_identity_displayed": false, - "ethnicities": "[\"Prefer Not to Say\"]", - "ethnicities_displayed": false, - "religions": "[\"Prefer Not to Say\"]", - "religions_displayed": true, - "workplaces": "[\"Prefer Not to Say\"]", - "workplaces_displayed": false, - "job_title": "Software Engineer", - "job_title_displayed": true, - "schools": "[\"University of Denver\"]", - "schools_displayed": true, - "hometowns": "[\"Prefer Not to Say\"]", - "hometowns_displayed": false, - "smoking": "[\"Prefer Not to Say\"]", - "smoking_displayed": false, - "drinking": "[\"Prefer Not to Say\"]", - "drinking_displayed": false, - "marijuana": "Prefer Not to Say", - "marijuana_displayed": false, - "drugs": "Prefer Not to Say", - "drugs_displayed": false, - "children": "Prefer Not to Say", - "children_displayed": false, - "family_plans": "Not sure yet", - "family_plans_displayed": false, - "education_attained": "Undergraduate", - "politics": "Prefer Not to Say", - "politics_displayed": false, - "instagram_displayed": false, - "vaccination_status": "Prefer Not to Say", - "vaccination_status_displayed": false, - "dating_intention_displayed": false, - "dating_intention": "Prefer Not to Say", - "dating_intention_text": "", - "languages_spoken_displayed": true, - "languages_spoken": "English", - "pets_displayed": true, - "pets": "Dog", - "relationship_type_displayed": false, - "relationship_types": "Prefer Not to Say", - "selfie_verified": false - }, - "preferences": { - "distance_miles_max": 50, - "age_min": 18, - "age_max": 99, - "age_dealbreaker": true, - "height_min": 92, - "height_max": 214, - "height_dealbreaker": false, - "gender_preference": "Dogs", - "ethnicity_preference": "[\"Open to All\"]", - "ethnicity_dealbreaker": false, - "religion_preference": "[\"Open to All\"]", - "religion_dealbreaker": false, - "smoking_preference": "[\"Open to All\"]", - "smoking_dealbreaker": false, - "drinking_preference": "[\"Open to All\"]", - "drinking_dealbreaker": false, - "marijuana_preference": "[\"Open to All\"]", - "marijuana_dealbreaker": false, - "drugs_preference": "[\"Open to All\"]", - "drugs_dealbreaker": false, - "children_preference": "[\"Open to All\"]", - "children_dealbreaker": false, - "family_plans_preference": "[\"Open to All\"]", - "family_plans_dealbreaker": false, - "education_attained_preference": "[\"Open to All\"]", - "education_attained_dealbreaker": false, - "politics_preference": "[\"Open to All\"]", - "politics_dealbreaker": false - }, - "location": { - "latitude": 40.652378, - "longitude": -73.959015, - "country": "United States", - "country_short": "US", - "admin_area_1": "New York", - "admin_area_1_short": "NY", - "admin_area_2": "Kings County", - "cbsa": "New York-Newark-Jersey City, NY-NJ-PA", - "locality": "New York", - "sublocality": "Brooklyn", - "neighborhood": "Flatbush", - "postal_code": "11226", - "postal_suffix": "3101", - "location_source": "google" - }, - "identity": { - "email": "faker@gmail.com", - "instagram_authorized": false, - "phone_number": "+15553334444", - "phone_country_code": "US", - "phone_country_calling_code": "1", - "phone_carrier": "Verizon Wireless", - "phone_line_type": "Mobile", - "phone_is_prepaid": false - }, - "installs": [ - { - "ip_address": "73.153.4.225", - "idfv": "CF6EDB9A-59AE-420C-A716-60C832907164", - "user_agent": "Hinge/11472 CFNetwork/1408.0.4 Darwin/22.5.0", - "install_time": "2023-06-29 03:28:49.239" - } - ] -} \ No newline at end of file From e18fba7a5367dc621de3ab87276a2e31f8dd2524 Mon Sep 17 00:00:00 2001 From: Shelby Potts Date: Mon, 17 Feb 2025 19:32:44 -0600 Subject: [PATCH 2/2] removed importer and put user upload in analytics class --- app/analytics/MatchAnalytics.py | 30 ----- app/analytics/UserAnalytics.py | 106 +++++++++++------- app/app.py | 2 +- app/importers/UserImporter.py | 19 ---- app/pages/UserPage.py | 32 +++--- .../importers => tests/analytics}/__init__.py | 0 .../test_UserAnalytics.py} | 67 ++++++++--- tests/importers/__init__.py | 0 8 files changed, 131 insertions(+), 125 deletions(-) delete mode 100644 app/importers/UserImporter.py rename {app/importers => tests/analytics}/__init__.py (100%) rename tests/{importers/test_UserImporter.py => analytics/test_UserAnalytics.py} (58%) delete mode 100644 tests/importers/__init__.py diff --git a/app/analytics/MatchAnalytics.py b/app/analytics/MatchAnalytics.py index 5263e69..71fd691 100644 --- a/app/analytics/MatchAnalytics.py +++ b/app/analytics/MatchAnalytics.py @@ -109,17 +109,6 @@ def phone_number_shares(events): columns=["Message Outcomes", "Count"]) return phone_number_share_ratios - -def import_user_account_data(file_path="../data/app_uploaded_files/user.json"): - account_data = __import_user_data_by_key("account", file_path) - return account_data - - -def import_user_device_data(file_path="../data/app_uploaded_files/user.json"): - device_data = __import_user_data_by_key("devices", file_path) - return device_data - - def __build_comments_list(events): likes_w_comments = [] like_events = events["like"].dropna() @@ -133,20 +122,6 @@ def __build_comments_list(events): return likes_w_comments -def __import_user_data_by_key(key, file_path): - __validate_upload_file_type(file_path) - __validate_user_file_upload(file_path) - - with open(file_path, 'r') as file: - raw_user_data = json.load(file) - - user_data = [] - if key in raw_user_data: - user_data = raw_user_data[key] - - return user_data - - def __validate_upload_file_type(file_path): if not file_path.endswith('.json'): raise ValueError("Invalid file type. Please upload a JSON file.") @@ -155,8 +130,3 @@ def __validate_upload_file_type(file_path): def __validate_match_file_upload(file_path): if 'match' not in file_path: raise ValueError("Invalid file name. Please upload a file with 'match' in the file name.") - - -def __validate_user_file_upload(file_path): - if 'user' not in file_path: - raise ValueError("Invalid file name. Please upload a file with 'user' in the file name.") diff --git a/app/analytics/UserAnalytics.py b/app/analytics/UserAnalytics.py index 7593ecd..3f6f9ac 100644 --- a/app/analytics/UserAnalytics.py +++ b/app/analytics/UserAnalytics.py @@ -1,47 +1,69 @@ # from ip2geotools.databases.noncommercial import DbIpCity import json import pandas as pd +import os +class UserAnalytics: + def __init__(self): + self.user_file_path = os.environ.get("USER_FILE_PATH") + if self.user_file_path is None: + raise Exception("USER_FILE_PATH environment variable is not set.") + + if '.json' not in self.user_file_path: + raise Exception("The user file needs to be a JSON file.") -def parse_user_ip_addresses(file_path='data/export/user.json'): - """ - Parses the IP addresses out of the user data and gets latitude and longitude coordinates from the IP addresses. - This is only grabbing a subset of the IP addresses because the full set of data takes too long. - :return: a DataFrame with latitude and longitude coordinates - """ - json_file_path = file_path - - # opening json file - with open(json_file_path, 'r') as file: - # raw data is a list of dictionaries "list of interactions with a person" - raw_data = json.load(file) - - device_value = [] - # parse just the device records - if 'devices' in raw_data: - values = raw_data['devices'] - device_value = values - - # extract the IP addresses - ip_addresses = [entry['ip_address'] for entry in device_value] - - lats = [] - longs = [] - # lookup the latitude and longitude coordinates of each IP address - # TODO: this API call doesn't work super well, replace it - # for ip in ip_addresses[:100]: - # coord = DbIpCity.get(ip, api_key="free") - # lats.append(coord.latitude) - # longs.append(coord.longitude) - - # define column names and create a DataFrame - coordinates = pd.DataFrame({'latitude': lats, 'longitude': longs}) - return coordinates - - -def __validate_user_file_upload(file_path): - if not file_path.endswith('.json'): - raise ValueError("Invalid file type. Please upload a JSON file.") - - if 'user' not in file_path: - raise ValueError("Invalid file. Please upload a user file.") + with open(self.user_file_path, 'r') as file: + user_data = json.load(file) + + self.user_data = user_data + + def get_account_data(self): + return self.user_data["account"] + + def get_devices_data(self): + return self.user_data["devices"] + + def get_profile_data(self): + return self.user_data["profile"] + + def get_preferences_data(self): + return self.user_data["preferences"] + + def get_location_data(self): + return self.user_data["location"] + + +# def parse_user_ip_addresses(file_path='data/export/user.json'): +# """ +# Parses the IP addresses out of the user data and gets latitude and longitude coordinates from the IP addresses. +# This is only grabbing a subset of the IP addresses because the full set of data takes too long. +# :return: a DataFrame with latitude and longitude coordinates +# """ +# json_file_path = file_path + +# # opening json file +# with open(json_file_path, 'r') as file: +# # raw data is a list of dictionaries "list of interactions with a person" +# raw_data = json.load(file) + +# device_value = [] +# # parse just the device records +# if 'devices' in raw_data: +# values = raw_data['devices'] +# device_value = values + +# # extract the IP addresses +# ip_addresses = [entry['ip_address'] for entry in device_value] + +# lats = [] +# longs = [] +# # lookup the latitude and longitude coordinates of each IP address +# # TODO: this API call doesn't work super well, replace it +# # for ip in ip_addresses[:100]: +# # coord = DbIpCity.get(ip, api_key="free") +# # lats.append(coord.latitude) +# # longs.append(coord.longitude) + +# # define column names and create a DataFrame +# coordinates = pd.DataFrame({'latitude': lats, 'longitude': longs}) +# return coordinates diff --git a/app/app.py b/app/app.py index 9e5ad35..97425c4 100644 --- a/app/app.py +++ b/app/app.py @@ -135,7 +135,7 @@ def parse_uploaded_file_contents(list_of_file_contents, list_of_file_names): uploaded_file_data = file_content.encode("utf8").split(b";base64,")[1] with open(os.path.join(USER_FILE_UPLOAD_DIRECTORY, file_name), "wb") as uploaded_file: - logger.info(f"Writing file: {file_name}...") + logger.info(f"Uploading user file: {file_name}...") uploaded_file.write(base64.decodebytes(uploaded_file_data)) # return an html Div of the uploaded file names to display to the user diff --git a/app/importers/UserImporter.py b/app/importers/UserImporter.py deleted file mode 100644 index 1747d2e..0000000 --- a/app/importers/UserImporter.py +++ /dev/null @@ -1,19 +0,0 @@ -import os -import json - -from app.tools.Logger import logger - -class UserImporter: - def __init__(self): - self.user_file_path = os.environ.get("USER_FILE_PATH") - if self.user_file_path is None: - raise Exception("USER_FILE_PATH environment variable is not set.") - - if '.json' not in self.user_file_path: - raise Exception("The user file needs to be a JSON file.") - - with open(self.user_file_path, 'r') as file: - user_data = json.load(file) - logger.info(f"Imported user data from {self.user_file_path}.") - - self.user_data = user_data \ No newline at end of file diff --git a/app/pages/UserPage.py b/app/pages/UserPage.py index 0c01b04..7ac4f61 100644 --- a/app/pages/UserPage.py +++ b/app/pages/UserPage.py @@ -4,8 +4,9 @@ import plotly.express as px from dash.exceptions import PreventUpdate -import analytics.MatchAnalytics as MatchAnalytics -import analytics.UserAnalytics as ua +from analytics.UserAnalytics import UserAnalytics + +user_analytics = UserAnalytics() layout = html.Div([ @@ -45,7 +46,7 @@ def update_comment_table(data): __check_for_live_update_data(data) - account_data = MatchAnalytics.import_user_account_data() + account_data = user_analytics.get_account_data() # passing in the account data as a list for the DataTable return [ dash_table.DataTable(data=[account_data], page_size=5, @@ -53,19 +54,20 @@ def update_comment_table(data): ] -@callback( - Output('live-update-coords-graph', 'figure'), - [Input('refresh-page', 'n_clicks')] -) -def update_coords_graph_live(data): - __check_for_live_update_data(data) +# TODO: commenting this out until there is an alternative +# @callback( +# Output('live-update-coords-graph', 'figure'), +# [Input('refresh-page', 'n_clicks')] +# ) +# def update_coords_graph_live(data): +# __check_for_live_update_data(data) - # initial setup of the global events - user_coordinates = ua.parse_user_ip_addresses() - # create the funnel graph - figure = px.scatter_geo(user_coordinates, locationmode="USA-states", lat="latitude", lon="longitude", - projection="orthographic") - return figure +# # initial setup of the global events +# user_coordinates = ua.parse_user_ip_addresses() +# # create the funnel graph +# figure = px.scatter_geo(user_coordinates, locationmode="USA-states", lat="latitude", lon="longitude", +# projection="orthographic") +# return figure # TODO: I don't like this this is repeated in both files, consolidate at some point diff --git a/app/importers/__init__.py b/tests/analytics/__init__.py similarity index 100% rename from app/importers/__init__.py rename to tests/analytics/__init__.py diff --git a/tests/importers/test_UserImporter.py b/tests/analytics/test_UserAnalytics.py similarity index 58% rename from tests/importers/test_UserImporter.py rename to tests/analytics/test_UserAnalytics.py index 93e3196..7b817bf 100644 --- a/tests/importers/test_UserImporter.py +++ b/tests/analytics/test_UserAnalytics.py @@ -3,8 +3,7 @@ import json from unittest.mock import mock_open, patch -from app.importers.UserImporter import UserImporter - +from app.analytics.UserAnalytics import UserAnalytics ######################################################################################### # test values @@ -30,6 +29,21 @@ "device_model": "unknown", "app_version": "9.30.0", "push_notifications_enabled": false + }, + "profile": { + "first_name": "Fake User", + "age": 99, + "height_centimeters": 213 + }, + "preferences": { + "distance_miles_max": 50, + "age_min": 98, + "age_max": 99 + }, + "location": { + "latitude": 65.00, + "longitude": 18.00, + "country": "Iceland" } } ''' @@ -38,48 +52,65 @@ # pytest fixtures ######################################################################################### @pytest.fixture -def user_importer(monkeypatch): +def user_analytics(monkeypatch): monkeypatch.setenv("USER_FILE_PATH", USER_FILE_PATH) with patch("builtins.open", mock_open(read_data=USER_DATA)) as mock_file, \ patch("json.load", return_value=json.loads(USER_DATA)) as mock_json_load: - user_importer = UserImporter() - return user_importer - + user_analytics = UserAnalytics() + return user_analytics ######################################################################################### # unit tests ######################################################################################### -def test_exists(user_importer): - assert user_importer is not None +def test_exists(user_analytics): + assert user_analytics is not None def test_user_file_path_not_set(): if "USER_FILE_PATH" in os.environ: del os.environ["USER_FILE_PATH"] with pytest.raises(Exception, match="USER_FILE_PATH environment variable is not set."): - UserImporter() + UserAnalytics() def test_user_file_not_json(): os.environ["USER_FILE_PATH"] = "invalid_file.txt" # invalid value with pytest.raises(Exception, match="The user file needs to be a JSON file."): - UserImporter() + UserAnalytics() -def test_user_importer_loads_data(user_importer): - assert isinstance(user_importer.user_data, dict) - assert "devices" in user_importer.user_data - assert "account" in user_importer.user_data +def test_user_analytics_loads_data(user_analytics): + assert isinstance(user_analytics.user_data, dict) + assert "devices" in user_analytics.user_data + assert "account" in user_analytics.user_data -def test_device_data(user_importer): - device = user_importer.user_data["devices"][0] +def test_device_data(user_analytics): + device = user_analytics.get_devices_data()[0] assert device["ip_address"] == "174.234.168.00" assert device["device_platform"] == "ios" assert device["device_os_versions"] == "16.5.1" -def test_account_data(user_importer): - account = user_importer.user_data["account"] +def test_account_data(user_analytics): + account = user_analytics.get_account_data() assert account["device_os"] == "16.6.1" assert account["app_version"] == "9.30.0" assert account["push_notifications_enabled"] is False + +def test_profile_data(user_analytics): + profile = user_analytics.get_profile_data() + assert profile["first_name"] == "Fake User" + assert profile["age"] == 99 + assert profile["height_centimeters"] == 213 + +def test_preferences_data(user_analytics): + preferences = user_analytics.get_preferences_data() + assert preferences["distance_miles_max"] == 50 + assert preferences["age_min"] == 98 + assert preferences["age_max"] == 99 + +def test_location_data(user_analytics): + locations = user_analytics.get_location_data() + assert locations["latitude"] == 65.00 + assert locations["longitude"] == 18.00 + assert locations["country"] == "Iceland" \ No newline at end of file diff --git a/tests/importers/__init__.py b/tests/importers/__init__.py deleted file mode 100644 index e69de29..0000000