aboutsummaryrefslogtreecommitdiff
path: root/tests/models/thl
diff options
context:
space:
mode:
Diffstat (limited to 'tests/models/thl')
-rw-r--r--tests/models/thl/question/test_question_info.py139
-rw-r--r--tests/models/thl/question/test_user_info.py29
-rw-r--r--tests/models/thl/test_adjustments.py128
-rw-r--r--tests/models/thl/test_bucket.py8
-rw-r--r--tests/models/thl/test_buyer.py4
-rw-r--r--tests/models/thl/test_contest/test_contest.py10
-rw-r--r--tests/models/thl/test_contest/test_leaderboard_contest.py44
-rw-r--r--tests/models/thl/test_contest/test_raffle_contest.py44
-rw-r--r--tests/models/thl/test_ledger.py6
-rw-r--r--tests/models/thl/test_marketplace_condition.py42
-rw-r--r--tests/models/thl/test_payout.py120
-rw-r--r--tests/models/thl/test_payout_format.py2
-rw-r--r--tests/models/thl/test_product.py305
-rw-r--r--tests/models/thl/test_product_userwalletconfig.py10
-rw-r--r--tests/models/thl/test_soft_pair.py12
-rw-r--r--tests/models/thl/test_upkquestion.py86
-rw-r--r--tests/models/thl/test_user.py141
-rw-r--r--tests/models/thl/test_user_iphistory.py6
-rw-r--r--tests/models/thl/test_user_metadata.py4
-rw-r--r--tests/models/thl/test_user_streak.py4
-rw-r--r--tests/models/thl/test_wall.py42
-rw-r--r--tests/models/thl/test_wall_session.py24
22 files changed, 612 insertions, 598 deletions
diff --git a/tests/models/thl/question/test_question_info.py b/tests/models/thl/question/test_question_info.py
index 945ee7a..af8d2b9 100644
--- a/tests/models/thl/question/test_question_info.py
+++ b/tests/models/thl/question/test_question_info.py
@@ -1,145 +1,16 @@
+from __future__ import annotations
+
from generalresearch.models.thl.profiling.upk_property import (
- UpkProperty,
ProfilingInfo,
+ UpkProperty,
)
class TestQuestionInfo:
- def test_init(self):
+ def test_init(self, profiling_info_json: str):
- s = (
- '[{"property_label": "hispanic", "cardinality": "*", "prop_type": "i", "country_iso": "us", '
- '"property_id": "05170ae296ab49178a075cab2a2073a6", "item_id": "7911ec1468b146ee870951f8ae9cbac1", '
- '"item_label": "panamanian", "gold_standard": 1, "options": [{"id": "c358c11e72c74fa2880358f1d4be85ab", '
- '"label": "not_hispanic"}, {"id": "b1d6c475770849bc8e0200054975dc9c", "label": "yes_hispanic"}, '
- '{"id": "bd1eb44495d84b029e107c188003c2bd", "label": "other_hispanic"}, '
- '{"id": "f290ad5e75bf4f4ea94dc847f57c1bd3", "label": "mexican"}, '
- '{"id": "49f50f2801bd415ea353063bfc02d252", "label": "puerto_rican"}, '
- '{"id": "dcbe005e522f4b10928773926601f8bf", "label": "cuban"}, '
- '{"id": "467ef8ddb7ac4edb88ba9ef817cbb7e9", "label": "salvadoran"}, '
- '{"id": "3c98e7250707403cba2f4dc7b877c963", "label": "dominican"}, '
- '{"id": "981ee77f6d6742609825ef54fea824a8", "label": "guatemalan"}, '
- '{"id": "81c8057b809245a7ae1b8a867ea6c91e", "label": "colombian"}, '
- '{"id": "513656d5f9e249fa955c3b527d483b93", "label": "honduran"}, '
- '{"id": "afc8cddd0c7b4581bea24ccd64db3446", "label": "ecuadorian"}, '
- '{"id": "61f34b36e80747a89d85e1eb17536f84", "label": "argentinian"}, '
- '{"id": "5330cfa681d44aa8ade3a6d0ea198e44", "label": "peruvian"}, '
- '{"id": "e7bceaffd76e486596205d8545019448", "label": "nicaraguan"}, '
- '{"id": "b7bbb2ebf8424714962e6c4f43275985", "label": "spanish"}, '
- '{"id": "8bf539785e7a487892a2f97e52b1932d", "label": "venezuelan"}, '
- '{"id": "7911ec1468b146ee870951f8ae9cbac1", "label": "panamanian"}], "category": [{"id": '
- '"4fd8381d5a1c4409ab007ca254ced084", "label": "Demographic", "path": "/Demographic", '
- '"adwords_vertical_id": null}]}, {"property_label": "ethnic_group", "cardinality": "*", "prop_type": '
- '"i", "country_iso": "us", "property_id": "15070958225d4132b7f6674fcfc979f6", "item_id": '
- '"64b7114cf08143949e3bcc3d00a5d8a0", "item_label": "other_ethnicity", "gold_standard": 1, "options": [{'
- '"id": "a72e97f4055e4014a22bee4632cbf573", "label": "caucasians"}, '
- '{"id": "4760353bc0654e46a928ba697b102735", "label": "black_or_african_american"}, '
- '{"id": "20ff0a2969fa4656bbda5c3e0874e63b", "label": "asian"}, '
- '{"id": "107e0a79e6b94b74926c44e70faf3793", "label": "native_hawaiian_or_other_pacific_islander"}, '
- '{"id": "900fa12691d5458c8665bf468f1c98c1", "label": "native_americans"}, '
- '{"id": "64b7114cf08143949e3bcc3d00a5d8a0", "label": "other_ethnicity"}], "category": [{"id": '
- '"4fd8381d5a1c4409ab007ca254ced084", "label": "Demographic", "path": "/Demographic", '
- '"adwords_vertical_id": null}]}, {"property_label": "educational_attainment", "cardinality": "?", '
- '"prop_type": "i", "country_iso": "us", "property_id": "2637783d4b2b4075b93e2a156e16e1d8", "item_id": '
- '"934e7b81d6744a1baa31bbc51f0965d5", "item_label": "other_education", "gold_standard": 1, "options": [{'
- '"id": "df35ef9e474b4bf9af520aa86630202d", "label": "3rd_grade_completion"}, '
- '{"id": "83763370a1064bd5ba76d1b68c4b8a23", "label": "8th_grade_completion"}, '
- '{"id": "f0c25a0670c340bc9250099dcce50957", "label": "not_high_school_graduate"}, '
- '{"id": "02ff74c872bd458983a83847e1a9f8fd", "label": "high_school_completion"}, '
- '{"id": "ba8beb807d56441f8fea9b490ed7561c", "label": "vocational_program_completion"}, '
- '{"id": "65373a5f348a410c923e079ddbb58e9b", "label": "some_college_completion"}, '
- '{"id": "2d15d96df85d4cc7b6f58911fdc8d5e2", "label": "associate_academic_degree_completion"}, '
- '{"id": "497b1fedec464151b063cd5367643ffa", "label": "bachelors_degree_completion"}, '
- '{"id": "295133068ac84424ae75e973dc9f2a78", "label": "some_graduate_completion"}, '
- '{"id": "e64f874faeff4062a5aa72ac483b4b9f", "label": "masters_degree_completion"}, '
- '{"id": "cbaec19a636d476385fb8e7842b044f5", "label": "doctorate_degree_completion"}, '
- '{"id": "934e7b81d6744a1baa31bbc51f0965d5", "label": "other_education"}], "category": [{"id": '
- '"4fd8381d5a1c4409ab007ca254ced084", "label": "Demographic", "path": "/Demographic", '
- '"adwords_vertical_id": null}]}, {"property_label": "household_spoken_language", "cardinality": "*", '
- '"prop_type": "i", "country_iso": "us", "property_id": "5a844571073d482a96853a0594859a51", "item_id": '
- '"62b39c1de141422896ad4ab3c4318209", "item_label": "dut", "gold_standard": 1, "options": [{"id": '
- '"f65cd57b79d14f0f8460761ce41ec173", "label": "ara"}, {"id": "6d49de1f8f394216821310abd29392d9", '
- '"label": "zho"}, {"id": "be6dc23c2bf34c3f81e96ddace22800d", "label": "eng"}, '
- '{"id": "ddc81f28752d47a3b1c1f3b8b01a9b07", "label": "fre"}, {"id": "2dbb67b29bd34e0eb630b1b8385542ca", '
- '"label": "ger"}, {"id": "a747f96952fc4b9d97edeeee5120091b", "label": "hat"}, '
- '{"id": "7144b04a3219433baac86273677551fa", "label": "hin"}, {"id": "e07ff3e82c7149eaab7ea2b39ee6a6dc", '
- '"label": "ita"}, {"id": "b681eff81975432ebfb9f5cc22dedaa3", "label": "jpn"}, '
- '{"id": "5cb20440a8f64c9ca62fb49c1e80cdef", "label": "kor"}, {"id": "171c4b77d4204bc6ac0c2b81e38a10ff", '
- '"label": "pan"}, {"id": "8c3ec18e6b6c4a55a00dd6052e8e84fb", "label": "pol"}, '
- '{"id": "3ce074d81d384dd5b96f1fb48f87bf01", "label": "por"}, {"id": "6138dc951990458fa88a666f6ddd907b", '
- '"label": "rus"}, {"id": "e66e5ecc07df4ebaa546e0b436f034bd", "label": "spa"}, '
- '{"id": "5a981b3d2f0d402a96dd2d0392ec2fcb", "label": "tgl"}, {"id": "b446251bd211403487806c4d0a904981", '
- '"label": "vie"}, {"id": "92fb3ee337374e2db875fb23f52eed46", "label": "xxx"}, '
- '{"id": "8b1f590f12f24cc1924d7bdcbe82081e", "label": "ind"}, {"id": "bf3f4be556a34ff4b836420149fd2037", '
- '"label": "tur"}, {"id": "87ca815c43ba4e7f98cbca98821aa508", "label": "zul"}, '
- '{"id": "0adbf915a7a64d67a87bb3ce5d39ca54", "label": "may"}, {"id": "62b39c1de141422896ad4ab3c4318209", '
- '"label": "dut"}], "category": [{"id": "4fd8381d5a1c4409ab007ca254ced084", "label": "Demographic", '
- '"path": "/Demographic", "adwords_vertical_id": null}]}, {"property_label": "gender", "cardinality": '
- '"?", "prop_type": "i", "country_iso": "us", "property_id": "73175402104741549f21de2071556cd7", '
- '"item_id": "093593e316344cd3a0ac73669fca8048", "item_label": "other_gender", "gold_standard": 1, '
- '"options": [{"id": "b9fc5ea07f3a4252a792fd4a49e7b52b", "label": "male"}, '
- '{"id": "9fdb8e5e18474a0b84a0262c21e17b56", "label": "female"}, '
- '{"id": "093593e316344cd3a0ac73669fca8048", "label": "other_gender"}], "category": [{"id": '
- '"4fd8381d5a1c4409ab007ca254ced084", "label": "Demographic", "path": "/Demographic", '
- '"adwords_vertical_id": null}]}, {"property_label": "age_in_years", "cardinality": "?", "prop_type": '
- '"n", "country_iso": "us", "property_id": "94f7379437874076b345d76642d4ce6d", "item_id": null, '
- '"item_label": null, "gold_standard": 1, "category": [{"id": "4fd8381d5a1c4409ab007ca254ced084", '
- '"label": "Demographic", "path": "/Demographic", "adwords_vertical_id": null}]}, {"property_label": '
- '"children_age_gender", "cardinality": "*", "prop_type": "i", "country_iso": "us", "property_id": '
- '"e926142fcea94b9cbbe13dc7891e1e7f", "item_id": "b7b8074e95334b008e8958ccb0a204f1", "item_label": '
- '"female_18", "gold_standard": 1, "options": [{"id": "16a6448ec24c48d4993d78ebee33f9b4", '
- '"label": "male_under_1"}, {"id": "809c04cb2e3b4a3bbd8077ab62cdc220", "label": "female_under_1"}, '
- '{"id": "295e05bb6a0843bc998890b24c99841e", "label": "no_children"}, '
- '{"id": "142cb948d98c4ae8b0ef2ef10978e023", "label": "male_0"}, '
- '{"id": "5a5c1b0e9abc48a98b3bc5f817d6e9d0", "label": "male_1"}, '
- '{"id": "286b1a9afb884bdfb676dbb855479d1e", "label": "male_2"}, '
- '{"id": "942ca3cda699453093df8cbabb890607", "label": "male_3"}, '
- '{"id": "995818d432f643ec8dd17e0809b24b56", "label": "male_4"}, '
- '{"id": "f38f8b57f25f4cdea0f270297a1e7a5c", "label": "male_5"}, '
- '{"id": "975df709e6d140d1a470db35023c432d", "label": "male_6"}, '
- '{"id": "f60bd89bbe0f4e92b90bccbc500467c2", "label": "male_7"}, '
- '{"id": "6714ceb3ed5042c0b605f00b06814207", "label": "male_8"}, '
- '{"id": "c03c2f8271d443cf9df380e84b4dea4c", "label": "male_9"}, '
- '{"id": "11690ee0f5a54cb794f7ddd010d74fa2", "label": "male_10"}, '
- '{"id": "17bef9a9d14b4197b2c5609fa94b0642", "label": "male_11"}, '
- '{"id": "e79c8338fe28454f89ccc78daf6f409a", "label": "male_12"}, '
- '{"id": "3a4f87acb3fa41f4ae08dfe2858238c1", "label": "male_13"}, '
- '{"id": "36ffb79d8b7840a7a8cb8d63bbc8df59", "label": "male_14"}, '
- '{"id": "1401a508f9664347aee927f6ec5b0a40", "label": "male_15"}, '
- '{"id": "6e0943c5ec4a4f75869eb195e3eafa50", "label": "male_16"}, '
- '{"id": "47d4b27b7b5242758a9fff13d3d324cf", "label": "male_17"}, '
- '{"id": "9ce886459dd44c9395eb77e1386ab181", "label": "female_0"}, '
- '{"id": "6499ccbf990d4be5b686aec1c7353fd8", "label": "female_1"}, '
- '{"id": "d85ceaa39f6d492abfc8da49acfd14f2", "label": "female_2"}, '
- '{"id": "18edb45c138e451d8cb428aefbb80f9c", "label": "female_3"}, '
- '{"id": "bac6f006ed9f4ccf85f48e91e99fdfd1", "label": "female_4"}, '
- '{"id": "5a6a1a8ad00c4ce8be52dcb267b034ff", "label": "female_5"}, '
- '{"id": "6bff0acbf6364c94ad89507bcd5f4f45", "label": "female_6"}, '
- '{"id": "d0d56a0a6b6f4516a366a2ce139b4411", "label": "female_7"}, '
- '{"id": "bda6028468044b659843e2bef4db2175", "label": "female_8"}, '
- '{"id": "dbb6d50325464032b456357b1a6e5e9c", "label": "female_9"}, '
- '{"id": "b87a93d7dc1348edac5e771684d63fb8", "label": "female_10"}, '
- '{"id": "11449d0d98f14e27ba47de40b18921d7", "label": "female_11"}, '
- '{"id": "16156501e97b4263962cbbb743840292", "label": "female_12"}, '
- '{"id": "04ee971c89a345cc8141a45bce96050c", "label": "female_13"}, '
- '{"id": "e818d310bfbc4faba4355e5d2ed49d4f", "label": "female_14"}, '
- '{"id": "440d25e078924ba0973163153c417ed6", "label": "female_15"}, '
- '{"id": "78ff804cc9b441c5a524bd91e3d1f8bf", "label": "female_16"}, '
- '{"id": "4b04d804d7d84786b2b1c22e4ed440f5", "label": "female_17"}, '
- '{"id": "28bc848cd3ff44c3893c76bfc9bc0c4e", "label": "male_18"}, '
- '{"id": "b7b8074e95334b008e8958ccb0a204f1", "label": "female_18"}], "category": [{"id": '
- '"e18ba6e9d51e482cbb19acf2e6f505ce", "label": "Parenting", "path": "/People & Society/Family & '
- 'Relationships/Family/Parenting", "adwords_vertical_id": "58"}]}, {"property_label": "home_postal_code", '
- '"cardinality": "?", "prop_type": "x", "country_iso": "us", "property_id": '
- '"f3b32ebe78014fbeb1ed6ff77d6338bf", "item_id": null, "item_label": null, "gold_standard": 1, '
- '"category": [{"id": "4fd8381d5a1c4409ab007ca254ced084", "label": "Demographic", "path": "/Demographic", '
- '"adwords_vertical_id": null}]}, {"property_label": "household_income", "cardinality": "?", "prop_type": '
- '"n", "country_iso": "us", "property_id": "ff5b1d4501d5478f98de8c90ef996ac1", "item_id": null, '
- '"item_label": null, "gold_standard": 1, "category": [{"id": "4fd8381d5a1c4409ab007ca254ced084", '
- '"label": "Demographic", "path": "/Demographic", "adwords_vertical_id": null}]}]'
- )
- instance_list = ProfilingInfo.validate_json(s)
+ instance_list = ProfilingInfo.validate_json(profiling_info_json)
assert isinstance(instance_list, list)
for i in instance_list:
diff --git a/tests/models/thl/question/test_user_info.py b/tests/models/thl/question/test_user_info.py
index 0bbbc78..5410d35 100644
--- a/tests/models/thl/question/test_user_info.py
+++ b/tests/models/thl/question/test_user_info.py
@@ -1,32 +1,11 @@
+from __future__ import annotations
+
from generalresearch.models.thl.profiling.user_info import UserInfo
class TestUserInfo:
- def test_init(self):
+ def test_init(self, profiling_user_info_json: str):
- s = (
- '{"user_profile_knowledge": [], "marketplace_profile_knowledge": [{"source": "d", "question_id": '
- '"1", "answer": ["1"], "created": "2023-11-07T16:41:05.234096Z"}, {"source": "pr", '
- '"question_id": "3", "answer": ["1"], "created": "2023-11-07T16:41:05.234096Z"}, {"source": '
- '"h", "question_id": "60", "answer": ["58"], "created": "2023-11-07T16:41:05.234096Z"}, '
- '{"source": "c", "question_id": "43", "answer": ["1"], "created": "2023-11-07T16:41:05.234096Z"}, '
- '{"source": "s", "question_id": "211", "answer": ["111"], "created": '
- '"2023-11-07T16:41:05.234096Z"}, {"source": "s", "question_id": "1843", "answer": ["111"], '
- '"created": "2023-11-07T16:41:05.234096Z"}, {"source": "h", "question_id": "13959", "answer": ['
- '"244155"], "created": "2023-11-07T16:41:05.234096Z"}, {"source": "c", "question_id": "33092", '
- '"answer": ["1"], "created": "2023-11-07T16:41:05.234096Z"}, {"source": "c", "question_id": "gender", '
- '"answer": ["10682"], "created": "2023-11-07T16:41:05.234096Z"}, {"source": "e", "question_id": '
- '"gender", "answer": ["male"], "created": "2023-11-07T16:41:05.234096Z"}, {"source": "f", '
- '"question_id": "gender", "answer": ["male"], "created": "2023-11-07T16:41:05.234096Z"}, {"source": '
- '"i", "question_id": "gender", "answer": ["1"], "created": "2023-11-07T16:41:05.234096Z"}, '
- '{"source": "c", "question_id": "137510", "answer": ["1"], "created": "2023-11-07T16:41:05.234096Z"}, '
- '{"source": "m", "question_id": "gender", "answer": ["1"], "created": '
- '"2023-11-07T16:41:05.234096Z"}, {"source": "o", "question_id": "gender", "answer": ["male"], '
- '"created": "2023-11-07T16:41:05.234096Z"}, {"source": "c", "question_id": "gender_plus", "answer": ['
- '"7657644"], "created": "2023-11-07T16:41:05.234096Z"}, {"source": "i", "question_id": '
- '"gender_plus", "answer": ["1"], "created": "2023-11-07T16:41:05.234096Z"}, {"source": "c", '
- '"question_id": "income_level", "answer": ["9071"], "created": "2023-11-07T16:41:05.234096Z"}]}'
- )
- instance = UserInfo.model_validate_json(s)
+ instance = UserInfo.model_validate_json(profiling_user_info_json)
assert isinstance(instance, UserInfo)
diff --git a/tests/models/thl/test_adjustments.py b/tests/models/thl/test_adjustments.py
index 27091bb..5d8605a 100644
--- a/tests/models/thl/test_adjustments.py
+++ b/tests/models/thl/test_adjustments.py
@@ -1,36 +1,42 @@
-from datetime import datetime, timedelta, timezone
+from __future__ import annotations
+
+from collections.abc import Callable
+from datetime import UTC, datetime, timedelta
from decimal import Decimal
-from typing import Callable
+from typing import TYPE_CHECKING
import pytest
-from generalresearch.models import Source
+from generalresearch.models.definitions import Source
from generalresearch.models.thl.product import Product
from generalresearch.models.thl.session import (
- Session,
SessionAdjustedStatus,
Status,
StatusCode1,
- Wall,
WallAdjustedStatus,
+ Session,
+ Wall,
)
-from generalresearch.models.thl.user import User
-started1 = datetime(2023, 1, 1, tzinfo=timezone.utc)
-started2 = datetime(2023, 1, 1, 0, 10, 0, tzinfo=timezone.utc)
+if TYPE_CHECKING:
+ from generalresearch.managers.thl.session import SessionManager
+ from generalresearch.managers.thl.wall import WallManager
+ from generalresearch.models.thl.user import User
+
+started1 = datetime(2023, 1, 1, tzinfo=UTC)
+started2 = datetime(2023, 1, 1, 0, 10, 0, tzinfo=UTC)
finished1 = started1 + timedelta(minutes=10)
finished2 = started2 + timedelta(minutes=10)
-adj_ts = datetime(2023, 2, 2, tzinfo=timezone.utc)
-adj_ts2 = datetime(2023, 2, 3, tzinfo=timezone.utc)
-adj_ts3 = datetime(2023, 2, 4, tzinfo=timezone.utc)
+adj_ts = datetime(2023, 2, 2, tzinfo=UTC)
+adj_ts2 = datetime(2023, 2, 3, tzinfo=UTC)
+adj_ts3 = datetime(2023, 2, 4, tzinfo=UTC)
class TestProductAdjustments:
-
@pytest.mark.parametrize("payout", [".6", "1", "1.8", "2", "500.0000"])
def test_determine_bp_payment_no_rounding(
- self, product_factory: Callable[..., Product], payout
+ self, product_factory: Callable[..., Product], payout: str
):
p1 = product_factory(commission_pct=Decimal("0.05"))
res = p1.determine_bp_payment(thl_net=Decimal(payout))
@@ -39,7 +45,7 @@ class TestProductAdjustments:
@pytest.mark.parametrize("payout", [".01", ".05", ".5"])
def test_determine_bp_payment_rounding(
- self, product_factory: Callable[..., Product], payout
+ self, product_factory: Callable[..., Product], payout: str
):
p1 = product_factory(commission_pct=Decimal("0.05"))
res = p1.determine_bp_payment(thl_net=Decimal(payout))
@@ -48,7 +54,6 @@ class TestProductAdjustments:
class TestSessionAdjustments:
-
def test_status_complete(self, session_factory: Callable[..., Session], user: User):
# Completed Session with 2 wall events
s1 = session_factory(
@@ -60,7 +65,7 @@ class TestSessionAdjustments:
)
# Confirm only the last Wall Event is a complete
- assert not s1.wall_events[0].status == Status.COMPLETE
+ assert s1.wall_events[0].status != Status.COMPLETE
assert s1.wall_events[1].status == Status.COMPLETE
# Confirm the Session is marked as finished and the simple brokerage
@@ -71,9 +76,11 @@ class TestSessionAdjustments:
class TestAdjustments:
-
def test_finish_with_status(
- self, session_factory: Callable[..., Session], user: User, session_manager
+ self,
+ session_factory: Callable[..., Session],
+ user: User,
+ session_manager: SessionManager,
):
# Completed Session with 2 wall events
s1 = session_factory(
@@ -85,6 +92,7 @@ class TestAdjustments:
)
status, status_code_1 = s1.determine_session_status()
+ assert isinstance(user.product, Product)
payout = user.product.determine_bp_payment(Decimal(1))
session_manager.finish_with_status(
session=s1,
@@ -97,7 +105,10 @@ class TestAdjustments:
assert Decimal("0.95") == payout
def test_never_adjusted(
- self, session_factory: Callable[..., Session], user: User, session_manager
+ self,
+ session_factory: Callable[..., Session],
+ user: User,
+ session_manager: SessionManager,
):
s1 = session_factory(
user=user,
@@ -130,8 +141,8 @@ class TestAdjustments:
self,
session_factory: Callable[..., Session],
user: User,
- session_manager,
- wall_manager,
+ session_manager: SessionManager,
+ wall_manager: WallManager,
):
# Completed Session with 2 wall events
s1 = session_factory(
@@ -174,13 +185,14 @@ class TestAdjustments:
# Because the Product doesn't have the Wallet mode enabled, the
# user_payout fields should always be None
+ assert isinstance(user.product, Product)
assert not user.product.user_wallet_config.enabled
assert s1.adjusted_user_payout is None
def test_adjustment_session_values(
self,
- wall_manager,
- session_manager,
+ wall_manager: WallManager,
+ session_manager: SessionManager,
session_factory: Callable[..., Session],
user: User,
):
@@ -218,13 +230,14 @@ class TestAdjustments:
# Because the Product doesn't have the Wallet mode enabled, the
# user_payout fields should always be None
+ assert isinstance(user.product, Product)
assert not user.product.user_wallet_config.enabled
assert s1.adjusted_user_payout is None
def test_double_adjustment_session_values(
self,
- wall_manager,
- session_manager,
+ wall_manager: WallManager,
+ session_manager: SessionManager,
session_factory: Callable[..., Session],
user: User,
):
@@ -276,8 +289,8 @@ class TestAdjustments:
def test_double_adjustment_sm_vs_db_values(
self,
- wall_manager,
- session_manager,
+ wall_manager: WallManager,
+ session_manager: SessionManager,
session_factory: Callable[..., Session],
user: User,
):
@@ -343,8 +356,8 @@ class TestAdjustments:
def test_double_adjustment_double_completes(
self,
- wall_manager,
- session_manager,
+ wall_manager: WallManager,
+ session_manager: SessionManager,
session_factory: Callable[..., Session],
user: User,
):
@@ -419,14 +432,14 @@ class TestAdjustments:
self,
session_factory: Callable[..., Session],
user: User,
- session_manager,
- wall_manager,
+ session_manager: SessionManager,
+ wall_manager: WallManager,
utc_hour_ago: datetime,
):
s1 = session_factory(
user=user,
wall_count=1,
- wall_req_cpi=Decimal("1"),
+ wall_req_cpi=Decimal(1),
final_status=Status.COMPLETE,
started=utc_hour_ago,
)
@@ -435,6 +448,7 @@ class TestAdjustments:
assert status == Status.COMPLETE
thl_net = Decimal(sum(w.cpi for w in s1.wall_events if w.is_visible_complete()))
+ assert isinstance(user.product, Product)
payout = user.product.determine_bp_payment(thl_net=thl_net)
session_manager.finish_with_status(
@@ -525,22 +539,20 @@ class TestAdjustments:
s1 = session_factory(
user=user,
wall_count=1,
- wall_req_cpi=Decimal("1"),
+ wall_req_cpi=Decimal(1),
final_status=Status.COMPLETE,
started=utc_hour_ago,
)
w1 = s1.wall_events[0]
status, status_code_1 = s1.determine_session_status()
- thl_net, commission_amount, bp_pay, user_pay = s1.determine_payments()
+ _, _, bp_pay, user_pay = s1.determine_payments()
s1.update(
- **{
- "status": status,
- "status_code_1": status_code_1,
- "finished": utc_hour_ago + timedelta(minutes=10),
- "payout": bp_pay,
- "user_payout": user_pay,
- }
+ status=status,
+ status_code_1=status_code_1,
+ finished=utc_hour_ago + timedelta(minutes=10),
+ payout=bp_pay,
+ user_payout=user_pay,
)
w1.update(
adjusted_status=WallAdjustedStatus.ADJUSTED_TO_FAIL,
@@ -562,6 +574,7 @@ class TestAdjustments:
new_status, new_payout, new_user_payout = s1.determine_new_status_and_payouts()
assert Status.COMPLETE == new_status
assert Decimal("0.95") == new_payout
+ assert isinstance(user.product, Product)
assert not user.product.user_wallet_config.enabled
# assert Decimal("0.48") == new_user_payout
assert new_user_payout is None
@@ -590,15 +603,14 @@ class TestAdjustments:
status, status_code_1 = s1.determine_session_status()
thl_net = Decimal(sum(w.cpi for w in s1.wall_events if w.is_visible_complete()))
+ assert isinstance(user.product, Product)
payout = user.product.determine_bp_payment(thl_net=thl_net)
s1.update(
- **{
- "status": status,
- "status_code_1": status_code_1,
- "finished": utc_hour_ago + timedelta(minutes=25),
- "payout": payout,
- "user_payout": None,
- }
+ status=status,
+ status_code_1=status_code_1,
+ finished=utc_hour_ago + timedelta(minutes=25),
+ payout=payout,
+ user_payout=None,
)
# Test. Adjust first fail to complete. Now we have 2 completes.
@@ -628,7 +640,10 @@ class TestAdjustments:
assert s1.adjusted_user_payout is None
def test_complete_to_fail_to_complete_adj1(
- self, user, session_factory, utc_hour_ago
+ self,
+ user: User,
+ session_factory: Callable[..., Session],
+ utc_hour_ago: datetime,
):
# Same as test_complete_to_fail_to_complete_adj but in opposite order
s1 = session_factory(
@@ -644,15 +659,14 @@ class TestAdjustments:
status, status_code_1 = s1.determine_session_status()
thl_net = Decimal(sum(w.cpi for w in s1.wall_events if w.is_visible_complete()))
+ assert isinstance(user.product, Product)
payout = user.product.determine_bp_payment(thl_net)
s1.update(
- **{
- "status": status,
- "status_code_1": status_code_1,
- "finished": utc_hour_ago + timedelta(minutes=25),
- "payout": payout,
- "user_payout": None,
- }
+ status=status,
+ status_code_1=status_code_1,
+ finished=utc_hour_ago + timedelta(minutes=25),
+ payout=payout,
+ user_payout=None,
)
# Test. Adjust complete to fail. Now we have 2 fails.
@@ -664,6 +678,7 @@ class TestAdjustments:
s1.adjust_status()
assert SessionAdjustedStatus.ADJUSTED_TO_FAIL == s1.adjusted_status
assert Decimal(0) == s1.adjusted_payout
+ assert isinstance(user.product, Product)
assert not user.product.user_wallet_config.enabled
# assert Decimal(0) == s.adjusted_user_payout
assert s1.adjusted_user_payout is None
@@ -708,6 +723,7 @@ class TestAdjustments:
s1.adjust_status()
assert SessionAdjustedStatus.ADJUSTED_TO_COMPLETE == s1.adjusted_status
assert Decimal("1.90") == s1.adjusted_payout
+ assert isinstance(user.product, Product)
assert not user.product.user_wallet_config.enabled
# assert Decimal("0.95") == s1.adjusted_user_payout
assert s1.adjusted_user_payout is None
diff --git a/tests/models/thl/test_bucket.py b/tests/models/thl/test_bucket.py
index 0aa5843..8d2f728 100644
--- a/tests/models/thl/test_bucket.py
+++ b/tests/models/thl/test_bucket.py
@@ -1,14 +1,17 @@
+from __future__ import annotations
+
from datetime import timedelta
from decimal import Decimal
import pytest
from pydantic import ValidationError
+from generalresearch.models.legacy.bucket import Bucket
+
class TestBucket:
def test_raises_payout(self):
- from generalresearch.models.legacy.bucket import Bucket
with pytest.raises(expected_exception=ValidationError) as e:
Bucket(user_payout_min=123)
@@ -27,7 +30,6 @@ class TestBucket:
assert "user_payout_min should be <= user_payout_max" in str(e.value)
def test_raises_loi(self):
- from generalresearch.models.legacy.bucket import Bucket
with pytest.raises(expected_exception=ValidationError) as e:
Bucket(loi_min=123)
@@ -63,7 +65,6 @@ class TestBucket:
assert "loi_q1 should be <= loi_q2" in str(e.value)
def test_parse_1(self):
- from generalresearch.models.legacy.bucket import Bucket
b1 = Bucket.parse_from_offerwall({"payout": {"min": 123}})
b_exp = Bucket(
@@ -180,7 +181,6 @@ class TestBucket:
assert b_exp == b4
def test_parse_3(self):
- from generalresearch.models.legacy.bucket import Bucket
b1 = Bucket.parse_from_offerwall({"payout": 123})
b_exp = Bucket(
diff --git a/tests/models/thl/test_buyer.py b/tests/models/thl/test_buyer.py
index eebb828..ef97166 100644
--- a/tests/models/thl/test_buyer.py
+++ b/tests/models/thl/test_buyer.py
@@ -1,4 +1,6 @@
-from generalresearch.models import Source
+from __future__ import annotations
+
+from generalresearch.models.definitions import Source
from generalresearch.models.thl.survey.buyer import BuyerCountryStat
diff --git a/tests/models/thl/test_contest/test_contest.py b/tests/models/thl/test_contest/test_contest.py
index 0fbd4cc..ed8477b 100644
--- a/tests/models/thl/test_contest/test_contest.py
+++ b/tests/models/thl/test_contest/test_contest.py
@@ -1,9 +1,13 @@
-from typing import Callable
+from __future__ import annotations
+
+from collections.abc import Callable
+from typing import TYPE_CHECKING
import pytest
-from generalresearch.models.thl.product import Product
-from generalresearch.models.thl.user import User
+if TYPE_CHECKING:
+ from generalresearch.models.thl.product import Product
+ from generalresearch.models.thl.user import User
class TestContest:
diff --git a/tests/models/thl/test_contest/test_leaderboard_contest.py b/tests/models/thl/test_contest/test_leaderboard_contest.py
index 8b714ee..a639261 100644
--- a/tests/models/thl/test_contest/test_leaderboard_contest.py
+++ b/tests/models/thl/test_contest/test_leaderboard_contest.py
@@ -1,7 +1,11 @@
-from datetime import timezone
+from __future__ import annotations
+
+from datetime import UTC
+from typing import TYPE_CHECKING
from uuid import uuid4
import pytest
+from redis import Redis
from generalresearch.currency import USDCent
from generalresearch.managers.leaderboard.manager import LeaderboardManager
@@ -17,16 +21,20 @@ from generalresearch.models.thl.contest.utils import (
distribute_leaderboard_prizes,
)
from generalresearch.models.thl.leaderboard import LeaderboardRow
-from generalresearch.models.thl.product import Product
+from generalresearch.models.thl.user import User
from tests.models.thl.test_contest.test_contest import TestContest
+if TYPE_CHECKING:
+ from generalresearch.managers.thl.user_manager.user_manager import UserManager
+ from generalresearch.models.thl.product import Product
+
class TestLeaderboardContest(TestContest):
@pytest.fixture
def leaderboard_contest(
- self, product: Product, thl_redis, user_manager
- ) -> "LeaderboardContest":
+ self, product: Product, thl_redis_client: Redis, user_manager: UserManager
+ ) -> LeaderboardContest:
board_key = f"leaderboard:{product.uuid}:us:weekly:2025-05-26:complete_count"
c = LeaderboardContest(
@@ -59,16 +67,22 @@ class TestLeaderboardContest(TestContest):
),
],
)
- c._redis_client = thl_redis
+ c._redis_client = thl_redis_client
c._user_manager = user_manager
return c
- def test_init(self, leaderboard_contest, thl_redis, user_1, user_2):
+ def test_init(
+ self,
+ leaderboard_contest: LeaderboardContest,
+ thl_redis_client: Redis,
+ user_1: User,
+ user_2: User,
+ ):
model = leaderboard_contest.leaderboard_model
assert leaderboard_contest.end_condition.ends_at is not None
lbm = LeaderboardManager(
- redis_client=thl_redis,
+ redis_client=thl_redis_client,
board_code=model.board_code,
country_iso=model.country_iso,
freq=model.freq,
@@ -83,15 +97,22 @@ class TestLeaderboardContest(TestContest):
lb = leaderboard_contest.get_leaderboard()
print(lb)
- def test_win(self, leaderboard_contest, thl_redis, user_1, user_2, user_3):
+ def test_win(
+ self,
+ leaderboard_contest: LeaderboardContest,
+ thl_redis_client: Redis,
+ user_1: User,
+ user_2: User,
+ user_3: User,
+ ):
model = leaderboard_contest.leaderboard_model
lbm = LeaderboardManager(
- redis_client=thl_redis,
+ redis_client=thl_redis_client,
board_code=model.board_code,
country_iso=model.country_iso,
freq=model.freq,
product_id=leaderboard_contest.product_id,
- within_time=model.period_start_local.astimezone(tz=timezone.utc),
+ within_time=model.period_start_local.astimezone(tz=UTC),
)
lbm.hit_complete_count(product_user_id=user_1.product_user_id)
@@ -102,10 +123,13 @@ class TestLeaderboardContest(TestContest):
lbm.hit_complete_count(product_user_id=user_3.product_user_id)
leaderboard_contest.end_contest()
+ assert isinstance(leaderboard_contest.all_winners, list)
assert len(leaderboard_contest.all_winners) == 3
# Prizes are $15, $10, $5. user 2 and 3 ties for 2nd place, so they split (10 + 5)
assert leaderboard_contest.all_winners[0].awarded_cash_amount == USDCent(15_00)
+
+ assert isinstance(leaderboard_contest.all_winners[0].user, User)
assert (
leaderboard_contest.all_winners[0].user.product_user_id
== user_1.product_user_id
diff --git a/tests/models/thl/test_contest/test_raffle_contest.py b/tests/models/thl/test_contest/test_raffle_contest.py
index d7920f0..e71851e 100644
--- a/tests/models/thl/test_contest/test_raffle_contest.py
+++ b/tests/models/thl/test_contest/test_raffle_contest.py
@@ -1,4 +1,8 @@
+from __future__ import annotations
+
from collections import Counter
+from datetime import datetime
+from typing import TYPE_CHECKING
from uuid import uuid4
import pytest
@@ -18,9 +22,12 @@ from generalresearch.models.thl.contest.definitions import (
ContestType,
)
from generalresearch.models.thl.contest.raffle import RaffleContest
-from generalresearch.models.thl.product import Product
from tests.models.thl.test_contest.test_contest import TestContest
+if TYPE_CHECKING:
+ from generalresearch.models.thl.product import Product
+ from generalresearch.models.thl.user import User
+
class TestRaffleContest(TestContest):
@@ -42,7 +49,9 @@ class TestRaffleContest(TestContest):
)
@pytest.fixture(scope="function")
- def ended_raffle_contest(self, raffle_contest, utc_now) -> RaffleContest:
+ def ended_raffle_contest(
+ self, raffle_contest: RaffleContest, utc_now: datetime
+ ) -> RaffleContest:
# Fake ending the contest
raffle_contest = raffle_contest.model_copy()
raffle_contest.update(
@@ -55,7 +64,7 @@ class TestRaffleContest(TestContest):
class TestRaffleContestUserView(TestRaffleContest):
- def test_user_view(self, raffle_contest, user):
+ def test_user_view(self, raffle_contest: RaffleContest, user: User):
from generalresearch.models.thl.contest.raffle import RaffleUserView
data = {
@@ -78,7 +87,7 @@ class TestRaffleContestUserView(TestRaffleContest):
assert res["current_win_probability"] == approx(0.0099, rel=0.001)
assert res["projected_win_probability"] == approx(0.0099, rel=0.001)
- def test_win_pct(self, raffle_contest, user):
+ def test_win_pct(self, raffle_contest: RaffleContest, user: User):
from generalresearch.models.thl.contest.raffle import RaffleUserView
data = {
@@ -124,7 +133,9 @@ class TestRaffleContestUserView(TestRaffleContest):
class TestRaffleContestWinners(TestRaffleContest):
- def test_winners_1_prize(self, ended_raffle_contest, user_1, user_2, user_3):
+ def test_winners_1_prize(
+ self, ended_raffle_contest, user_1: User, user_2: User, user_3: User
+ ):
ended_raffle_contest.entries = [
ContestEntry(
user=user_1,
@@ -160,7 +171,13 @@ class TestRaffleContestWinners(TestRaffleContest):
assert c[user_2.user_id] == approx(10000 * 2 / 6, rel=0.1)
assert c[user_3.user_id] == approx(10000 * 3 / 6, rel=0.1)
- def test_winners_2_prizes(self, ended_raffle_contest, user_1, user_2, user_3):
+ def test_winners_2_prizes(
+ self,
+ ended_raffle_contest: RaffleContest,
+ user_1: User,
+ user_2: User,
+ user_3: User,
+ ):
ended_raffle_contest.prizes.append(
ContestPrize(
name="iPod 64GB Black",
@@ -193,7 +210,9 @@ class TestRaffleContestWinners(TestRaffleContest):
# Same user
assert all(w.user.user_id == user_1.user_id for w in winners)
- def test_winners_2_prizes_1_entry(self, ended_raffle_contest, user_3):
+ def test_winners_2_prizes_1_entry(
+ self, ended_raffle_contest: RaffleContest, user_3: User
+ ):
ended_raffle_contest.prizes = [
ContestPrize(
name="iPod 64GB White",
@@ -218,7 +237,9 @@ class TestRaffleContestWinners(TestRaffleContest):
winners = ended_raffle_contest.select_winners()
assert len(winners) == 1
- def test_winners_2_prizes_1_entry_2_pennies(self, ended_raffle_contest, user_3):
+ def test_winners_2_prizes_1_entry_2_pennies(
+ self, ended_raffle_contest: RaffleContest, user_3: User
+ ):
ended_raffle_contest.prizes = [
ContestPrize(
name="iPod 64GB White",
@@ -243,7 +264,12 @@ class TestRaffleContestWinners(TestRaffleContest):
assert len(winners) == 2
def test_winners_3_prizes_3_entries(
- self, ended_raffle_contest, product, user_1, user_2, user_3
+ self,
+ ended_raffle_contest: RaffleContest,
+ product: Product,
+ user_1: User,
+ user_2: User,
+ user_3: User,
):
ended_raffle_contest.prizes = [
ContestPrize(
diff --git a/tests/models/thl/test_ledger.py b/tests/models/thl/test_ledger.py
index 257de3c..7c48dbd 100644
--- a/tests/models/thl/test_ledger.py
+++ b/tests/models/thl/test_ledger.py
@@ -1,4 +1,6 @@
-from datetime import datetime, timezone
+from __future__ import annotations
+
+from datetime import UTC, datetime
from uuid import uuid4
import pytest
@@ -21,7 +23,7 @@ class TestLedgerTransaction:
assert [] == t.entries
assert {} == t.metadata
t = LedgerTransaction(
- created=datetime.now(tz=timezone.utc),
+ created=datetime.now(tz=UTC),
metadata={"a": "b", "user": "1234"},
ext_description="foo",
)
diff --git a/tests/models/thl/test_marketplace_condition.py b/tests/models/thl/test_marketplace_condition.py
index 8a4b25c..6936a7c 100644
--- a/tests/models/thl/test_marketplace_condition.py
+++ b/tests/models/thl/test_marketplace_condition.py
@@ -1,15 +1,18 @@
+from __future__ import annotations
+
import pytest
from pydantic import ValidationError
+from generalresearch.models.definitions import LogicalOperator
+from generalresearch.models.thl.survey.condition import (
+ ConditionValueType,
+ MarketplaceCondition,
+)
+
class TestMarketplaceCondition:
def test_list_or(self):
- from generalresearch.models import LogicalOperator
- from generalresearch.models.thl.survey.condition import (
- ConditionValueType,
- MarketplaceCondition,
- )
user_qas = {"q1": {"a2"}}
c = MarketplaceCondition(
@@ -46,11 +49,6 @@ class TestMarketplaceCondition:
assert c.evaluate_criterion(user_qas) is None
def test_list_or_negate(self):
- from generalresearch.models import LogicalOperator
- from generalresearch.models.thl.survey.condition import (
- ConditionValueType,
- MarketplaceCondition,
- )
user_qas = {"q1": {"a2"}}
c = MarketplaceCondition(
@@ -87,11 +85,6 @@ class TestMarketplaceCondition:
assert c.evaluate_criterion(user_qas) is None
def test_list_and(self):
- from generalresearch.models import LogicalOperator
- from generalresearch.models.thl.survey.condition import (
- ConditionValueType,
- MarketplaceCondition,
- )
user_qas = {"q1": {"a1", "a2"}}
c = MarketplaceCondition(
@@ -137,7 +130,7 @@ class TestMarketplaceCondition:
assert c.evaluate_criterion(user_qas) is None
def test_list_and_negate(self):
- from generalresearch.models import LogicalOperator
+ from generalresearch.models.definitions import LogicalOperator
from generalresearch.models.thl.survey.condition import (
ConditionValueType,
MarketplaceCondition,
@@ -178,11 +171,6 @@ class TestMarketplaceCondition:
assert c.evaluate_criterion(user_qas) is None
def test_ranges(self):
- from generalresearch.models import LogicalOperator
- from generalresearch.models.thl.survey.condition import (
- ConditionValueType,
- MarketplaceCondition,
- )
user_qas = {"q1": {"2", "50"}}
c = MarketplaceCondition(
@@ -245,12 +233,6 @@ class TestMarketplaceCondition:
)
def test_ranges_to_list(self):
- from generalresearch.models import LogicalOperator
- from generalresearch.models.thl.survey.condition import (
- ConditionValueType,
- MarketplaceCondition,
- )
-
user_qas = {"q1": {"2", "50"}}
MarketplaceCondition._CONVERT_LIST_TO_RANGE = ["q1"]
c = MarketplaceCondition(
@@ -265,7 +247,7 @@ class TestMarketplaceCondition:
assert ["1", "10", "11", "12", "2", "3", "4", "5"] == c.values
def test_ranges_infinity(self):
- from generalresearch.models import LogicalOperator
+ from generalresearch.models.definitions import LogicalOperator
from generalresearch.models.thl.survey.condition import (
ConditionValueType,
MarketplaceCondition,
@@ -309,10 +291,6 @@ class TestMarketplaceCondition:
assert not c.evaluate_criterion({"q1": {"50"}})
def test_answered(self):
- from generalresearch.models.thl.survey.condition import (
- ConditionValueType,
- MarketplaceCondition,
- )
user_qas = {"q1": {"a2"}}
c = MarketplaceCondition(
diff --git a/tests/models/thl/test_payout.py b/tests/models/thl/test_payout.py
index 3a51328..cc00f33 100644
--- a/tests/models/thl/test_payout.py
+++ b/tests/models/thl/test_payout.py
@@ -1,10 +1,122 @@
+from __future__ import annotations
+
+from uuid import uuid4
+
+import pytest
+from pydantic import ValidationError
+
+from generalresearch.currency import USDCent
+from generalresearch.models.gr import Team
+from generalresearch.models.gr.business import (
+ Business,
+ BusinessAddress,
+)
+from generalresearch.models.gr.definitions import BusinessType
+from generalresearch.models.thl.payout import (
+ BrokerageProductPayoutEvent,
+ BusinessPayoutEvent,
+)
+from generalresearch.models.thl.wallet.definitions import PayoutType
+
+
class TestBusinessPayoutEvent:
def test_validate(self):
- from generalresearch.models.gr.business import Business
- instance = Business.model_validate_json(
- json_data='{"id":123,"uuid":"947f6ba5250d442b9a66cde9ee33605a","name":"Example » Demo","kind":"c","tax_number":null,"contact":null,"addresses":[],"teams":[{"id":53,"uuid":"8e4197dcaefe4f1f831a02b212e6b44a","name":"Example » Demo","memberships":null,"gr_users":null,"businesses":null,"products":null}],"products":[{"id":"fc23e741b5004581b30e6478363525df","id_int":1234,"name":"Example","enabled":true,"payments_enabled":true,"created":"2025-04-14T13:25:37.279403Z","team_id":"9e4197dcaefe4f1f831a02b212e6b44a","business_id":"857f6ba6160d442b9a66cde9ee33605a","tags":[],"commission_pct":"0.050000","redirect_url":"https://pam-api-us.reppublika.com/v2/public/4970ef00-0ef7-11f0-9962-05cb6323c84c/grl/status","harmonizer_domain":"https://talk.generalresearch.com/","sources_config":{"user_defined":[{"name":"w","active":false,"banned_countries":[],"allow_mobile_ip":true,"supplier_id":null,"allow_pii_only_buyers":false,"allow_unhashed_buyers":false,"withhold_profiling":false,"pass_unconditional_eligible_unknowns":true,"address":null,"allow_vpn":null,"distribute_harmonizer_active":null}]},"session_config":{"max_session_len":600,"max_session_hard_retry":5,"min_payout":"0.14"},"payout_config":{"payout_format":null,"payout_transformation":null},"user_wallet_config":{"enabled":false,"amt":false,"supported_payout_types":["CASH_IN_MAIL","PAYPAL","TANGO"],"min_cashout":null},"user_create_config":{"min_hourly_create_limit":0,"max_hourly_create_limit":null},"offerwall_config":{},"profiling_config":{"enabled":true,"grs_enabled":true,"n_questions":null,"max_questions":10,"avg_question_count":5.0,"task_injection_freq_mult":1.0,"non_us_mult":2.0,"hidden_questions_expiration_hours":168},"user_health_config":{"banned_countries":[],"allow_ban_iphist":true},"yield_man_config":{},"balance":null,"payouts_total_str":null,"payouts_total":null,"payouts":null,"user_wallet":{"enabled":false,"amt":false,"supported_payout_types":["CASH_IN_MAIL","PAYPAL","TANGO"],"min_cashout":null}}],"bank_accounts":[],"balance":{"product_balances":[{"product_id":"fc14e741b5004581b30e6478363414df","last_event":null,"bp_payment_credit":780251,"adjustment_credit":4678,"adjustment_debit":26446,"supplier_credit":0,"supplier_debit":451513,"user_bonus_credit":0,"user_bonus_debit":0,"issued_payment":0,"payout":780251,"payout_usd_str":"$7,802.51","adjustment":-21768,"expense":0,"net":758483,"payment":451513,"payment_usd_str":"$4,515.13","balance":306970,"retainer":76742,"retainer_usd_str":"$767.42","available_balance":230228,"available_balance_usd_str":"$2,302.28","recoup":0,"recoup_usd_str":"$0.00","adjustment_percent":0.027898714644390074}],"payout":780251,"payout_usd_str":"$7,802.51","adjustment":-21768,"expense":0,"net":758483,"net_usd_str":"$7,584.83","payment":451513,"payment_usd_str":"$4,515.13","balance":306970,"balance_usd_str":"$3,069.70","retainer":76742,"retainer_usd_str":"$767.42","available_balance":230228,"available_balance_usd_str":"$2,302.28","adjustment_percent":0.027898714644390074,"recoup":0,"recoup_usd_str":"$0.00"},"payouts_total_str":"$4,515.13","payouts_total":451513,"payouts":[{"bp_payouts":[{"uuid":"40cf2c3c341e4f9d985be4bca43e6116","debit_account_uuid":"3a058056da85493f9b7cdfe375aad0e0","cashout_method_uuid":"602113e330cf43ae85c07d94b5100291","created":"2025-08-02T09:18:20.433329Z","amount":345735,"status":"COMPLETE","ext_ref_id":null,"payout_type":"ACH","request_data":{},"order_data":null,"product_id":"fc14e741b5004581b30e6478363414df","method":"ACH","amount_usd":345735,"amount_usd_str":"$3,457.35"}],"amount":345735,"amount_usd_str":"$3,457.35","created":"2025-08-02T09:18:20.433329Z","line_items":1,"ext_ref_id":null},{"bp_payouts":[{"uuid":"63ce1787087248978919015c8fcd5ab9","debit_account_uuid":"3a058056da85493f9b7cdfe375aad0e0","cashout_method_uuid":"602113e330cf43ae85c07d94b5100291","created":"2025-06-10T22:16:18.765668Z","amount":105778,"status":"COMPLETE","ext_ref_id":"11175997868","payout_type":"ACH","request_data":{},"order_data":null,"product_id":"fc14e741b5004581b30e6478363414df","method":"ACH","amount_usd":105778,"amount_usd_str":"$1,057.78"}],"amount":105778,"amount_usd_str":"$1,057.78","created":"2025-06-10T22:16:18.765668Z","line_items":1,"ext_ref_id":"11175997868"}]}'
+ # Doesn't validate anymore
+ # instance = Business.model_validate_json(
+ # json_data='{"id":123,"uuid":"947f6ba5250d442b9a66cde9ee33605a","name":"Example » Demo","kind":"c","tax_number":null,"contact":null,"addresses":[],"teams":[{"id":53,"uuid":"8e4197dcaefe4f1f831a02b212e6b44a","name":"Example » Demo","memberships":null,"gr_users":null,"businesses":null,"products":null}],"products":[{"id":"fc23e741b5004581b30e6478363525df","id_int":1234,"name":"Example","enabled":true,"payments_enabled":true,"created":"2025-04-14T13:25:37.279403Z","team_id":"9e4197dcaefe4f1f831a02b212e6b44a","business_id":"857f6ba6160d442b9a66cde9ee33605a","tags":[],"commission_pct":"0.050000","redirect_url":"https://pam-api-us.reppublika.com/v2/public/4970ef00-0ef7-11f0-9962-05cb6323c84c/grl/status","harmonizer_domain":"https://talk.generalresearch.com/","sources_config":{"user_defined":[{"name":"w","active":false,"banned_countries":[],"allow_mobile_ip":true,"supplier_id":null,"allow_pii_only_buyers":false,"allow_unhashed_buyers":false,"withhold_profiling":false,"pass_unconditional_eligible_unknowns":true,"address":null,"allow_vpn":null,"distribute_harmonizer_active":null}]},"session_config":{"max_session_len":600,"max_session_hard_retry":5,"min_payout":"0.14"},"payout_config":{"payout_format":null,"payout_transformation":null},"user_wallet_config":{"enabled":false,"amt":false,"supported_payout_types":["CASH_IN_MAIL","PAYPAL","TANGO"],"min_cashout":null},"user_create_config":{"min_hourly_create_limit":0,"max_hourly_create_limit":null},"offerwall_config":{},"profiling_config":{"enabled":true,"grs_enabled":true,"n_questions":null,"max_questions":10,"avg_question_count":5.0,"task_injection_freq_mult":1.0,"non_us_mult":2.0,"hidden_questions_expiration_hours":168},"user_health_config":{"banned_countries":[],"allow_ban_iphist":true},"yield_man_config":{},"balance":null,"payouts_total_str":null,"payouts_total":null,"payouts":null,"user_wallet":{"enabled":false,"amt":false,"supported_payout_types":["CASH_IN_MAIL","PAYPAL","TANGO"],"min_cashout":null}}],"bank_accounts":[],"balance":{"product_balances":[{"product_id":"fc14e741b5004581b30e6478363414df","last_event":null,"bp_payment_credit":780251,"adjustment_credit":4678,"adjustment_debit":26446,"supplier_credit":0,"supplier_debit":451513,"user_bonus_credit":0,"user_bonus_debit":0,"issued_payment":0,"payout":780251,"payout_usd_str":"$7,802.51","adjustment":-21768,"expense":0,"net":758483,"payment":451513,"payment_usd_str":"$4,515.13","balance":306970,"retainer":76742,"retainer_usd_str":"$767.42","available_balance":230228,"available_balance_usd_str":"$2,302.28","recoup":0,"recoup_usd_str":"$0.00","adjustment_percent":0.027898714644390074}],"payout":780251,"payout_usd_str":"$7,802.51","adjustment":-21768,"expense":0,"net":758483,"net_usd_str":"$7,584.83","payment":451513,"payment_usd_str":"$4,515.13","balance":306970,"balance_usd_str":"$3,069.70","retainer":76742,"retainer_usd_str":"$767.42","available_balance":230228,"available_balance_usd_str":"$2,302.28","adjustment_percent":0.027898714644390074,"recoup":0,"recoup_usd_str":"$0.00"},"payouts_total_str":"$4,515.13","payouts_total":451513,"payouts":[{"bp_payouts":[{"uuid":"40cf2c3c341e4f9d985be4bca43e6116","debit_account_uuid":"3a058056da85493f9b7cdfe375aad0e0","cashout_method_uuid":"602113e330cf43ae85c07d94b5100291","created":"2025-08-02T09:18:20.433329Z","amount":345735,"status":"COMPLETE","ext_ref_id":null,"payout_type":"ACH","request_data":{},"order_data":null,"product_id":"fc14e741b5004581b30e6478363414df","method":"ACH","amount_usd":345735,"amount_usd_str":"$3,457.35"}],"amount":345735,"amount_usd_str":"$3,457.35","created":"2025-08-02T09:18:20.433329Z","line_items":1,"ext_ref_id":null},{"bp_payouts":[{"uuid":"63ce1787087248978919015c8fcd5ab9","debit_account_uuid":"3a058056da85493f9b7cdfe375aad0e0","cashout_method_uuid":"602113e330cf43ae85c07d94b5100291","created":"2025-06-10T22:16:18.765668Z","amount":105778,"status":"COMPLETE","ext_ref_id":"11175997868","payout_type":"ACH","request_data":{},"order_data":null,"product_id":"fc14e741b5004581b30e6478363414df","method":"ACH","amount_usd":105778,"amount_usd_str":"$1,057.78"}],"amount":105778,"amount_usd_str":"$1,057.78","created":"2025-06-10T22:16:18.765668Z","line_items":1,"ext_ref_id":"11175997868"}]}'
+ # )
+ # assert isinstance(instance, Business)
+
+ # Make manually
+ b = Business(
+ id=123,
+ uuid=uuid4().hex,
+ name="Example",
+ addresses=[
+ BusinessAddress(
+ uuid=uuid4().hex,
+ city="xxx",
+ line_1="xxx",
+ state="fl",
+ business_id=123,
+ )
+ ],
+ kind=BusinessType.COMPANY,
+ teams=[Team(uuid=uuid4().hex, name="Example » Demo")],
+ products=[],
+ bank_accounts=[],
+ )
+ assert isinstance(b, Business)
+
+ ext_ref_id = uuid4().hex
+ bpe = BusinessPayoutEvent(
+ business_id=uuid4().hex,
+ amount=USDCent(100_00),
+ payout_type=PayoutType.ACH,
+ ext_ref_id=ext_ref_id,
)
+ bpe.bp_payouts = [
+ BrokerageProductPayoutEvent(
+ product_id=uuid4().hex,
+ payout_type=PayoutType.ACH,
+ amount=USDCent(47_00),
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ ext_ref_id=ext_ref_id,
+ ),
+ BrokerageProductPayoutEvent(
+ product_id=uuid4().hex,
+ payout_type=PayoutType.ACH,
+ amount=USDCent(53_00),
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ ext_ref_id=ext_ref_id,
+ ),
+ ]
+
+ # Test validations (amount sum)
+ with pytest.raises(
+ ValidationError,
+ match="BusinessPayoutEvent.amount must equal the sum of bp_payouts amounts",
+ ):
+ bpe.bp_payouts = [
+ BrokerageProductPayoutEvent(
+ product_id=uuid4().hex,
+ payout_type=PayoutType.ACH,
+ amount=USDCent(47_00),
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ ext_ref_id=ext_ref_id,
+ )
+ ]
+
+ with pytest.raises(
+ ValidationError,
+ match="All BrokerageProductPayoutEvent.ext_ref_id values must equal",
+ ):
+ bpe.bp_payouts = [
+ BrokerageProductPayoutEvent(
+ product_id=uuid4().hex,
+ payout_type=PayoutType.ACH,
+ amount=USDCent(100_00),
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ ext_ref_id="a different value",
+ )
+ ]
- assert isinstance(instance, Business)
+ with pytest.raises(
+ ValidationError, match="All BrokerageProductPayoutEvent.payout_type values"
+ ):
+ bpe.bp_payouts = [
+ BrokerageProductPayoutEvent(
+ product_id=uuid4().hex,
+ payout_type=PayoutType.PAYPAL,
+ amount=USDCent(100_00),
+ cashout_method_uuid=uuid4().hex,
+ debit_account_uuid=uuid4().hex,
+ ext_ref_id=ext_ref_id,
+ )
+ ]
diff --git a/tests/models/thl/test_payout_format.py b/tests/models/thl/test_payout_format.py
index 83fde25..fe7aea5 100644
--- a/tests/models/thl/test_payout_format.py
+++ b/tests/models/thl/test_payout_format.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from pydantic import BaseModel
diff --git a/tests/models/thl/test_product.py b/tests/models/thl/test_product.py
index 39469dc..97abf0c 100644
--- a/tests/models/thl/test_product.py
+++ b/tests/models/thl/test_product.py
@@ -2,9 +2,10 @@ from __future__ import annotations
import os
import shutil
-from datetime import datetime, timedelta, timezone
+from collections.abc import Callable
+from datetime import UTC, datetime, timedelta
from decimal import Decimal
-from typing import Callable
+from typing import TYPE_CHECKING
from uuid import uuid4
import pytest
@@ -12,18 +13,9 @@ from dask.distributed import Client as DaskClient
from pydantic import ValidationError
from generalresearch.currency import USDCent
-from generalresearch.incite.base import GRLDatasets
-from generalresearch.incite.mergers.pop_ledger import PopLedgerMerge
-from generalresearch.managers.thl.ledger_manager.thl_ledger import (
- ThlLedgerManager,
-)
-from generalresearch.managers.thl.product import ProductManager
-from generalresearch.models import Source
-from generalresearch.models.gr.business import Business
+from generalresearch.models.definitions import Source
from generalresearch.models.thl.finance import ProductBalances
from generalresearch.models.thl.product import (
- BrokerageProductPayoutEvent,
- BrokerageProductPayoutEventManager,
IntegrationMode,
PayoutConfig,
PayoutTransformation,
@@ -35,12 +27,27 @@ from generalresearch.models.thl.product import (
SupplyConfig,
SupplyPolicy,
)
-from generalresearch.models.thl.session import Session
-from generalresearch.models.thl.user import User
+if TYPE_CHECKING:
+ from generalresearch.incite.base import GRLDatasets
+ from generalresearch.incite.collections.thl_web import LedgerDFCollection
+ from generalresearch.incite.mergers.pop_ledger import PopLedgerMerge
+ from generalresearch.managers.thl.ledger_manager.thl_ledger import (
+ ThlLedgerManager,
+ )
+ from generalresearch.managers.thl.payout import PayoutEventManager
+ from generalresearch.managers.thl.product import ProductManager
+ from generalresearch.models.gr.business import Business
+ from generalresearch.models.thl.payout import (
+ BrokerageProductPayoutEvent,
+ )
+ from generalresearch.models.thl.product import BrokerageProductPayoutEventManager
+ from generalresearch.models.thl.session import Session
+ from generalresearch.models.thl.user import User
+ from generalresearch.redis_helper import RedisConfig
-class TestProduct:
+class TestProduct:
def test_init(self):
# By default, just a Pydantic instance doesn't have an id_int
instance = Product.model_validate(
@@ -56,17 +63,19 @@ class TestProduct:
# We're not excluding anything here, only in the "*Out" variants
assert "id_int" in res
- def test_init_db(self, product_manager: ProductManager):
+ def test_init_db(
+ self, product_factory: Callable[..., Product], product_manager: ProductManager
+ ):
# By default, just a Pydantic instance doesn't have an id_int
- instance = product_manager.create_dummy()
+ instance = product_factory()
assert isinstance(instance.id_int, int)
+ assert isinstance(instance, Product)
res = instance.model_dump_json()
- assert isinstance(res, Product)
# we json skip & exclude
- res = instance.model_dump()
- assert isinstance(res, Product)
+ p = Product.model_validate_json(res)
+ assert isinstance(p, Product)
def test_redirect_url(self):
p = Product.model_validate(
@@ -140,12 +149,6 @@ class TestProduct:
redirect_url="https://www.google.com/hey",
)
- assert isinstance(p.payout_config.payout_transformation, PayoutTransformation)
- assert isinstance(
- p.payout_config.payout_transformation.kwargs,
- PayoutTransformationPercentArgs,
- )
-
p.payout_config.payout_transformation = PayoutTransformation.model_validate(
{
"f": "payout_transformation_percent",
@@ -156,6 +159,11 @@ class TestProduct:
assert (
"payout_transformation_percent" == p.payout_config.payout_transformation.f
)
+
+ assert isinstance(
+ p.payout_config.payout_transformation.kwargs,
+ PayoutTransformationPercentArgs,
+ )
assert 0.5 == p.payout_config.payout_transformation.kwargs.pct
assert (
Decimal("0.10") == p.payout_config.payout_transformation.kwargs.min_payout
@@ -287,10 +295,10 @@ class TestProduct:
p.profiling_config = ProfilingConfig(max_questions=1)
assert p.profiling_config.max_questions == 1
- def test_bp_account(self, product, thl_lm):
+ def test_bp_account(self, product: Product, thl_ledger_manager: ThlLedgerManager):
assert product.bp_account is None
- product.prefetch_bp_account(thl_lm=thl_lm)
+ product.prefetch_bp_account(thl_lm=thl_ledger_manager)
from generalresearch.models.thl.ledger import LedgerAccount
@@ -583,14 +591,13 @@ class TestGlobalProductConfigFor:
class TestProductFinancials:
-
@pytest.fixture
def start(self) -> datetime:
- return datetime(year=2018, month=3, day=14, hour=0, tzinfo=timezone.utc)
+ return datetime(year=2018, month=3, day=14, hour=0, tzinfo=UTC)
@pytest.fixture
def offset(self) -> str:
- return "30d"
+ return "30D"
@pytest.fixture
def duration(self) -> timedelta | None:
@@ -598,55 +605,75 @@ class TestProductFinancials:
def test_balance(
self,
- business: Business,
+ gr_business: Business,
product_factory: Callable[..., Product],
user_factory: Callable[..., User],
mnt_filepath: GRLDatasets,
- bp_payout_factory: Callable[..., BrokerageProductPayoutEvent],
- thl_lm: ThlLedgerManager,
+ brokerage_product_payout_event_factory: Callable[
+ ..., BrokerageProductPayoutEvent
+ ],
+ thl_ledger_manager: ThlLedgerManager,
start: datetime,
brokerage_product_payout_event_manager: BrokerageProductPayoutEventManager,
session_with_tx_factory: Callable[..., Session],
- delete_ledger_db,
- create_main_accounts,
+ delete_ledger_db: Callable[..., None],
+ create_main_accounts: Callable[..., None],
client_no_amm: DaskClient,
- ledger_collection,
+ ledger_collection: LedgerDFCollection,
pop_ledger_merge: PopLedgerMerge,
- delete_df_collection,
+ delete_df_collection: Callable[..., None],
):
delete_ledger_db()
create_main_accounts()
delete_df_collection(coll=ledger_collection)
- from generalresearch.currency import USDCent
-
- p1: Product = product_factory(business=business)
+ p1: Product = product_factory(business=gr_business)
u1: User = user_factory(product=p1)
- bp_wallet = thl_lm.get_account_or_create_bp_wallet(product=p1)
- thl_lm.get_account_or_create_user_wallet(user=u1)
- brokerage_product_payout_event_manager.set_account_lookup_table(thl_lm=thl_lm)
+ bp_wallet = thl_ledger_manager.get_account_or_create_bp_wallet(product=p1)
+ thl_ledger_manager.get_account_or_create_user_wallet(user=u1)
- assert len(thl_lm.get_tx_filtered_by_account(account_uuid=bp_wallet.uuid)) == 0
+ assert (
+ len(
+ thl_ledger_manager.get_tx_filtered_by_account(
+ account_uuid=bp_wallet.uuid
+ )
+ )
+ == 0
+ )
session_with_tx_factory(
user=u1,
wall_req_cpi=Decimal(".50"),
started=start + timedelta(days=1),
)
- assert thl_lm.get_account_balance(account=bp_wallet) == 48
- assert len(thl_lm.get_tx_filtered_by_account(account_uuid=bp_wallet.uuid)) == 1
+ assert thl_ledger_manager.get_account_balance(account=bp_wallet) == 48
+ assert (
+ len(
+ thl_ledger_manager.get_tx_filtered_by_account(
+ account_uuid=bp_wallet.uuid
+ )
+ )
+ == 1
+ )
session_with_tx_factory(
user=u1,
wall_req_cpi=Decimal("1.00"),
started=start + timedelta(days=2),
)
- assert thl_lm.get_account_balance(account=bp_wallet) == 143
- assert len(thl_lm.get_tx_filtered_by_account(account_uuid=bp_wallet.uuid)) == 2
+ assert thl_ledger_manager.get_account_balance(account=bp_wallet) == 143
+ assert (
+ len(
+ thl_ledger_manager.get_tx_filtered_by_account(
+ account_uuid=bp_wallet.uuid
+ )
+ )
+ == 2
+ )
with pytest.raises(expected_exception=AssertionError) as cm:
p1.prebuild_balance(
- thl_lm=thl_lm,
+ thl_lm=thl_ledger_manager,
ds=mnt_filepath,
client=client_no_amm,
)
@@ -656,7 +683,7 @@ class TestProductFinancials:
pop_ledger_merge.build(client=client_no_amm, ledger_coll=ledger_collection)
p1.prebuild_balance(
- thl_lm=thl_lm,
+ thl_lm=thl_ledger_manager,
ds=mnt_filepath,
client=client_no_amm,
)
@@ -670,7 +697,7 @@ class TestProductFinancials:
assert p1.balance.available_balance == 108
p1.prebuild_payouts(
- thl_lm=thl_lm,
+ thl_lm=thl_ledger_manager,
bp_pem=brokerage_product_payout_event_manager,
)
assert p1.payouts is not None
@@ -680,14 +707,21 @@ class TestProductFinancials:
# -- Now pay them out...
- bp_payout_factory(
+ from generalresearch.currency import USDCent
+
+ brokerage_product_payout_event_factory(
product=p1,
amount=USDCent(50),
created=start + timedelta(days=3),
- skip_wallet_balance_check=True,
- skip_one_per_day_check=True,
)
- assert len(thl_lm.get_tx_filtered_by_account(account_uuid=bp_wallet.uuid)) == 3
+ assert (
+ len(
+ thl_ledger_manager.get_tx_filtered_by_account(
+ account_uuid=bp_wallet.uuid
+ )
+ )
+ == 3
+ )
# RM the entire directories
shutil.rmtree(ledger_collection.archive_path)
@@ -699,7 +733,7 @@ class TestProductFinancials:
pop_ledger_merge.build(client=client_no_amm, ledger_coll=ledger_collection)
p1.prebuild_balance(
- thl_lm=thl_lm,
+ thl_lm=thl_ledger_manager,
ds=mnt_filepath,
client=client_no_amm,
)
@@ -713,24 +747,29 @@ class TestProductFinancials:
assert p1.balance.available_balance == 70
p1.prebuild_payouts(
- thl_lm=thl_lm,
+ thl_lm=thl_ledger_manager,
bp_pem=brokerage_product_payout_event_manager,
)
assert p1.payouts is not None
assert len(p1.payouts) == 1
- assert p1.payouts_total == 50
+ assert p1.payouts_total == USDCent(50)
assert p1.payouts_total_str == "$0.50"
# -- Now pay ou another!.
- bp_payout_factory(
+ brokerage_product_payout_event_factory(
product=p1,
amount=USDCent(5),
created=start + timedelta(days=4),
- skip_wallet_balance_check=True,
- skip_one_per_day_check=True,
)
- assert len(thl_lm.get_tx_filtered_by_account(account_uuid=bp_wallet.uuid)) == 4
+ assert (
+ len(
+ thl_ledger_manager.get_tx_filtered_by_account(
+ account_uuid=bp_wallet.uuid
+ )
+ )
+ == 4
+ )
# RM the entire directories
shutil.rmtree(ledger_collection.archive_path)
@@ -742,7 +781,7 @@ class TestProductFinancials:
pop_ledger_merge.build(client=client_no_amm, ledger_coll=ledger_collection)
p1.prebuild_balance(
- thl_lm=thl_lm,
+ thl_lm=thl_ledger_manager,
ds=mnt_filepath,
client=client_no_amm,
)
@@ -756,7 +795,7 @@ class TestProductFinancials:
assert p1.balance.available_balance == 66
p1.prebuild_payouts(
- thl_lm=thl_lm,
+ thl_lm=thl_ledger_manager,
bp_pem=brokerage_product_payout_event_manager,
)
assert p1.payouts is not None
@@ -766,14 +805,13 @@ class TestProductFinancials:
class TestProductBalance:
-
@pytest.fixture
def start(self) -> datetime:
- return datetime(year=2018, month=3, day=14, hour=0, tzinfo=timezone.utc)
+ return datetime(year=2018, month=3, day=14, hour=0, tzinfo=UTC)
@pytest.fixture
def offset(self) -> str:
- return "30d"
+ return "30D"
@pytest.fixture
def duration(self) -> timedelta | None:
@@ -783,18 +821,20 @@ class TestProductBalance:
self,
product: Product,
mnt_filepath: GRLDatasets,
- thl_lm: ThlLedgerManager,
+ thl_ledger_manager: ThlLedgerManager,
client_no_amm: DaskClient,
- delete_ledger_db,
- create_main_accounts,
- delete_df_collection,
- ledger_collection,
+ delete_ledger_db: Callable[..., None],
+ create_main_accounts: Callable[..., None],
+ delete_df_collection: Callable[..., None],
+ ledger_collection: LedgerDFCollection,
user_factory: Callable[..., User],
session_with_tx_factory: Callable[..., Session],
- pop_ledger_merge,
+ pop_ledger_merge: PopLedgerMerge,
start: datetime,
- bp_payout_factory,
- payout_event_manager,
+ brokerage_product_payout_event_factory: Callable[
+ ..., BrokerageProductPayoutEvent
+ ],
+ payout_event_manager: PayoutEventManager,
):
# Now let's load it up and actually test some things
delete_ledger_db()
@@ -813,21 +853,18 @@ class TestProductBalance:
pop_ledger_merge.build(client=client_no_amm, ledger_coll=ledger_collection)
# 2. Payout and build Parquets 2nd time
- payout_event_manager.set_account_lookup_table(thl_lm=thl_lm)
- bp_payout_factory(
+ brokerage_product_payout_event_factory(
product=product,
amount=USDCent(71),
ext_ref_id=uuid4().hex,
created=start + timedelta(days=1, minutes=1),
- skip_wallet_balance_check=True,
- skip_one_per_day_check=True,
)
ledger_collection.initial_load(client=None, sync=True)
pop_ledger_merge.build(client=client_no_amm, ledger_coll=ledger_collection)
with pytest.raises(expected_exception=AssertionError) as cm:
product.prebuild_balance(
- thl_lm=thl_lm, ds=mnt_filepath, client=client_no_amm
+ thl_lm=thl_ledger_manager, ds=mnt_filepath, client=client_no_amm
)
assert "Sql and Parquet Balance inconsistent" in str(cm)
@@ -835,18 +872,20 @@ class TestProductBalance:
self,
product: Product,
mnt_filepath: GRLDatasets,
- thl_lm: ThlLedgerManager,
+ thl_ledger_manager: ThlLedgerManager,
client_no_amm: DaskClient,
- delete_ledger_db,
- create_main_accounts,
- delete_df_collection,
- ledger_collection,
+ delete_ledger_db: Callable[..., None],
+ create_main_accounts: Callable[..., None],
+ delete_df_collection: Callable[..., None],
+ ledger_collection: LedgerDFCollection,
user_factory: Callable[..., User],
- session_with_tx_factory,
+ session_with_tx_factory: Callable[..., None],
pop_ledger_merge: PopLedgerMerge,
start: datetime,
- bp_payout_factory,
- payout_event_manager,
+ brokerage_product_payout_event_factory: Callable[
+ ..., BrokerageProductPayoutEvent
+ ],
+ payout_event_manager: PayoutEventManager,
):
# This is very similar to the test_complete_payout_pq_inconsistent
# test, however this time we're only going to assign the payout
@@ -872,31 +911,29 @@ class TestProductBalance:
# 2. Payout and build Parquets 2nd time but this payout is "now"
# so it hasn't already been archived
- payout_event_manager.set_account_lookup_table(thl_lm=thl_lm)
- bp_payout_factory(
+ brokerage_product_payout_event_factory(
product=product,
amount=USDCent(71),
ext_ref_id=uuid4().hex,
- created=datetime.now(tz=timezone.utc),
- skip_wallet_balance_check=True,
- skip_one_per_day_check=True,
+ created=datetime.now(tz=UTC),
)
ledger_collection.initial_load(client=None, sync=True)
pop_ledger_merge.build(client=client_no_amm, ledger_coll=ledger_collection)
# We just want to call this to confirm it doesn't raise.
- product.prebuild_balance(thl_lm=thl_lm, ds=mnt_filepath, client=client_no_amm)
+ product.prebuild_balance(
+ thl_lm=thl_ledger_manager, ds=mnt_filepath, client=client_no_amm
+ )
class TestProductPOPFinancial:
-
@pytest.fixture
def start(self) -> datetime:
- return datetime(year=2018, month=3, day=14, hour=0, tzinfo=timezone.utc)
+ return datetime(year=2018, month=3, day=14, hour=0, tzinfo=UTC)
@pytest.fixture
def offset(self) -> str:
- return "30d"
+ return "30D"
@pytest.fixture
def duration(self) -> timedelta | None:
@@ -906,14 +943,14 @@ class TestProductPOPFinancial:
self,
product: Product,
mnt_filepath: GRLDatasets,
- thl_lm: ThlLedgerManager,
+ thl_ledger_manager: ThlLedgerManager,
client_no_amm: DaskClient,
- delete_ledger_db,
- create_main_accounts,
- delete_df_collection,
- ledger_collection,
+ delete_ledger_db: Callable[..., None],
+ create_main_accounts: Callable[..., None],
+ delete_df_collection: Callable[..., None],
+ ledger_collection: LedgerDFCollection,
user_factory: Callable[..., User],
- session_with_tx_factory,
+ session_with_tx_factory: Callable[..., None],
pop_ledger_merge: PopLedgerMerge,
start: datetime,
):
@@ -942,7 +979,7 @@ class TestProductPOPFinancial:
# --- test ---
assert product.pop_financial is None
product.prebuild_pop_financial(
- thl_lm=thl_lm,
+ thl_lm=thl_ledger_manager,
ds=mnt_filepath,
client=client_no_amm,
pop_ledger=pop_ledger_merge,
@@ -962,14 +999,13 @@ class TestProductPOPFinancial:
class TestProductCache:
-
@pytest.fixture
def start(self) -> datetime:
- return datetime(year=2018, month=3, day=14, hour=0, tzinfo=timezone.utc)
+ return datetime(year=2018, month=3, day=14, hour=0, tzinfo=UTC)
@pytest.fixture
def offset(self) -> str:
- return "30d"
+ return "30D"
@pytest.fixture
def duration(self) -> timedelta | None:
@@ -978,17 +1014,17 @@ class TestProductCache:
def test_basic(
self,
product: Product,
- mnt_filepath,
- thl_lm,
+ mnt_filepath: GRLDatasets,
+ thl_ledger_manager: ThlLedgerManager,
client_no_amm: DaskClient,
- thl_redis_config,
- brokerage_product_payout_event_manager,
- delete_ledger_db,
- create_main_accounts,
- delete_df_collection,
- ledger_collection,
+ thl_redis_config: RedisConfig,
+ brokerage_product_payout_event_manager: BrokerageProductPayoutEventManager,
+ delete_ledger_db: Callable[..., None],
+ create_main_accounts: Callable[..., None],
+ delete_df_collection: Callable[..., None],
+ ledger_collection: LedgerDFCollection,
user_factory: Callable[..., User],
- session_with_tx_factory,
+ session_with_tx_factory: Callable[..., None],
pop_ledger_merge: PopLedgerMerge,
start: datetime,
):
@@ -1003,7 +1039,7 @@ class TestProductCache:
assert res is None
with pytest.raises(expected_exception=AssertionError):
product.set_cache(
- thl_lm=thl_lm,
+ thl_lm=thl_ledger_manager,
ds=mnt_filepath,
client=client_no_amm,
bp_pem=brokerage_product_payout_event_manager,
@@ -1025,7 +1061,7 @@ class TestProductCache:
# Now try again with everything in place
product.set_cache(
- thl_lm=thl_lm,
+ thl_lm=thl_ledger_manager,
ds=mnt_filepath,
client=client_no_amm,
bp_pem=brokerage_product_payout_event_manager,
@@ -1050,21 +1086,23 @@ class TestProductCache:
self,
product: Product,
mnt_filepath: GRLDatasets,
- thl_lm,
+ thl_ledger_manager: ThlLedgerManager,
client_no_amm: DaskClient,
- thl_redis_config,
- brokerage_product_payout_event_manager,
- delete_ledger_db,
- create_main_accounts,
- delete_df_collection,
- ledger_collection,
+ thl_redis_config: RedisConfig,
+ brokerage_product_payout_event_manager: BrokerageProductPayoutEventManager,
+ delete_ledger_db: Callable[..., None],
+ create_main_accounts: Callable[..., None],
+ delete_df_collection: Callable[..., None],
+ ledger_collection: LedgerDFCollection,
user_factory: Callable[..., User],
- session_with_tx_factory,
+ session_with_tx_factory: Callable[..., None],
pop_ledger_merge: PopLedgerMerge,
start: datetime,
- bp_payout_factory,
- payout_event_manager,
- adj_to_fail_with_tx_factory,
+ brokerage_product_payout_event_factory: Callable[
+ ..., BrokerageProductPayoutEvent
+ ],
+ payout_event_manager: PayoutEventManager,
+ adj_to_fail_with_tx_factory: Callable[..., None],
):
# Now let's load it up and actually test some things
delete_ledger_db()
@@ -1083,14 +1121,11 @@ class TestProductCache:
)
# 2. Payout
- payout_event_manager.set_account_lookup_table(thl_lm=thl_lm)
- bp_payout_factory(
+ brokerage_product_payout_event_factory(
product=product,
amount=USDCent(71),
ext_ref_id=uuid4().hex,
created=start + timedelta(days=1, minutes=1),
- skip_wallet_balance_check=True,
- skip_one_per_day_check=True,
)
# 3. Recon
@@ -1104,7 +1139,7 @@ class TestProductCache:
pop_ledger_merge.build(client=client_no_amm, ledger_coll=ledger_collection)
product.set_cache(
- thl_lm=thl_lm,
+ thl_lm=thl_ledger_manager,
ds=mnt_filepath,
client=client_no_amm,
bp_pem=brokerage_product_payout_event_manager,
diff --git a/tests/models/thl/test_product_userwalletconfig.py b/tests/models/thl/test_product_userwalletconfig.py
index 614fc0a..9fb9c73 100644
--- a/tests/models/thl/test_product_userwalletconfig.py
+++ b/tests/models/thl/test_product_userwalletconfig.py
@@ -1,13 +1,15 @@
+from __future__ import annotations
+
from itertools import groupby
from random import shuffle as rshuffle
from generalresearch.models.thl.product import (
UserWalletConfig,
)
-from generalresearch.models.thl.wallet import PayoutType
+from generalresearch.models.thl.wallet.definitions import PayoutType
-def all_equal(iterable):
+def all_equal(iterable: list[str]) -> bool:
g = groupby(iterable)
return next(g, True) and not next(g, False)
@@ -41,13 +43,13 @@ class TestProductUserWalletConfig:
# in the same order because they're the same
assert isinstance(instance.model_dump_json(), str)
res = []
- for idx in range(100):
+ for _ in range(100):
res.append(instance.model_dump_json())
assert all_equal(res)
def test_model_dump_payout_types(self):
res = []
- for idx in range(100):
+ for _ in range(100):
# Generate a random order of PayoutTypes each time
payout_types = [e for e in PayoutType]
diff --git a/tests/models/thl/test_soft_pair.py b/tests/models/thl/test_soft_pair.py
index 588847e..34902e2 100644
--- a/tests/models/thl/test_soft_pair.py
+++ b/tests/models/thl/test_soft_pair.py
@@ -1,12 +1,14 @@
-from generalresearch.models import Source
+from __future__ import annotations
+
+from generalresearch.models.definitions import Source
+from generalresearch.models.dynata.survey import (
+ ConditionValueType,
+ DynataCondition,
+)
from generalresearch.models.thl.soft_pair import SoftPairResult, SoftPairResultType
def test_model():
- from generalresearch.models.dynata.survey import (
- ConditionValueType,
- DynataCondition,
- )
c1 = DynataCondition(
question_id="1", value_type=ConditionValueType.LIST, values=["a", "b"]
diff --git a/tests/models/thl/test_upkquestion.py b/tests/models/thl/test_upkquestion.py
index d32875c..719fcff 100644
--- a/tests/models/thl/test_upkquestion.py
+++ b/tests/models/thl/test_upkquestion.py
@@ -1,13 +1,30 @@
+from __future__ import annotations
+
import pytest
from pydantic import ValidationError
+from generalresearch.models.morning.question import (
+ MorningQuestion,
+ MorningQuestionType,
+)
+from generalresearch.models.thl.profiling.upk_question import (
+ PatternValidation,
+ UPKImportance,
+ UpkQuestion,
+ UpkQuestionChoice,
+ UpkQuestionConfigurationMC,
+ UpkQuestionConfigurationTE,
+ UpkQuestionSelectorMC,
+ UpkQuestionSelectorTE,
+ UpkQuestionType,
+ UpkQuestionValidation,
+ order_exclusive_options,
+)
+
class TestUpkQuestion:
def test_importance(self):
- from generalresearch.models.thl.profiling.upk_question import (
- UPKImportance,
- )
res = UPKImportance(task_score=1, task_count=None)
assert isinstance(res, UPKImportance)
@@ -20,9 +37,6 @@ class TestUpkQuestion:
assert "Input should be greater than or equal to 0" in str(e.value)
def test_pattern(self):
- from generalresearch.models.thl.profiling.upk_question import (
- PatternValidation,
- )
s = PatternValidation(message="hi", pattern="x")
with pytest.raises(ValidationError) as e:
@@ -30,13 +44,6 @@ class TestUpkQuestion:
assert "Instance is frozen" in str(e.value)
def test_mc(self):
- from generalresearch.models.thl.profiling.upk_question import (
- UpkQuestion,
- UpkQuestionChoice,
- UpkQuestionConfigurationMC,
- UpkQuestionSelectorMC,
- UpkQuestionType,
- )
q = UpkQuestion(
id="601377a0d4c74529afc6293a8e5c3b5e",
@@ -126,14 +133,6 @@ class TestUpkQuestion:
assert "Extra inputs are not permitted" in str(e.value)
def test_te(self):
- from generalresearch.models.thl.profiling.upk_question import (
- PatternValidation,
- UpkQuestion,
- UpkQuestionConfigurationTE,
- UpkQuestionSelectorTE,
- UpkQuestionType,
- UpkQuestionValidation,
- )
q = UpkQuestion(
id="601377a0d4c74529afc6293a8e5c3b5e",
@@ -152,9 +151,6 @@ class TestUpkQuestion:
assert q.choices is None
def test_deserialization(self):
- from generalresearch.models.thl.profiling.upk_question import (
- UpkQuestion,
- )
q = UpkQuestion.model_validate(
{
@@ -195,24 +191,18 @@ class TestUpkQuestion:
assert q == UpkQuestion.model_validate(q.model_dump(mode="json"))
def test_from_morning(self):
- from generalresearch.models.morning.question import (
- MorningQuestion,
- MorningQuestionType,
- )
q = MorningQuestion(
- **{
- "id": "gender",
- "country_iso": "us",
- "language_iso": "eng",
- "name": "Gender",
- "text": "What is your gender?",
- "type": "s",
- "options": [
- {"id": "1", "text": "yes", "order": 1},
- {"id": "2", "text": "no", "order": 2},
- ],
- }
+ id="gender",
+ country_iso="us",
+ language_iso="eng",
+ name="Gender",
+ text="What is your gender?",
+ type="s",
+ options=[
+ {"id": "1", "text": "yes", "order": 1},
+ {"id": "2", "text": "no", "order": 2},
+ ],
)
q.to_upk_question()
q = MorningQuestion(
@@ -226,13 +216,6 @@ class TestUpkQuestion:
q.to_upk_question()
def test_order(self):
- from generalresearch.models.thl.profiling.upk_question import (
- UpkQuestion,
- UpkQuestionChoice,
- UpkQuestionSelectorMC,
- UpkQuestionType,
- order_exclusive_options,
- )
q = UpkQuestion(
country_iso="us",
@@ -266,9 +249,6 @@ class TestUpkQuestion:
class TestUpkQuestionValidateAnswer:
def test_validate_answer_SA(self):
- from generalresearch.models.thl.profiling.upk_question import (
- UpkQuestion,
- )
question = UpkQuestion.model_validate(
{
@@ -304,9 +284,6 @@ class TestUpkQuestionValidateAnswer:
)
def test_validate_answer_MA(self):
- from generalresearch.models.thl.profiling.upk_question import (
- UpkQuestion,
- )
question = UpkQuestion.model_validate(
{
@@ -376,9 +353,6 @@ class TestUpkQuestionValidateAnswer:
)
def test_validate_answer_TE(self):
- from generalresearch.models.thl.profiling.upk_question import (
- UpkQuestion,
- )
question = UpkQuestion.model_validate(
{
diff --git a/tests/models/thl/test_user.py b/tests/models/thl/test_user.py
index 943ae8e..68b413c 100644
--- a/tests/models/thl/test_user.py
+++ b/tests/models/thl/test_user.py
@@ -1,25 +1,35 @@
+from __future__ import annotations
+
import json
-from datetime import datetime, timedelta, timezone
+from collections.abc import Callable
+from datetime import UTC, datetime, timedelta, timezone
from decimal import Decimal
from random import choice as rand_choice
from random import randint
+from typing import TYPE_CHECKING
from uuid import uuid4
import pytest
from pydantic import ValidationError
+from generalresearch.models.thl.user import User
+
+if TYPE_CHECKING:
+ from generalresearch.managers.thl.ledger_manager.thl_ledger import ThlLedgerManager
+ from generalresearch.managers.thl.userhealth import AuditLogManager
+ from generalresearch.models.thl.product import Product
+ from generalresearch.models.thl.userhealth import AuditLog
+
class TestUserUserID:
def test_valid(self):
- from generalresearch.models.thl.user import User
val = randint(1, 2**30)
user = User(user_id=val)
assert user.user_id == val
def test_type(self):
- from generalresearch.models.thl.user import User
# It will cast str to int
assert User(user_id="1").user_id == 1
@@ -44,7 +54,6 @@ class TestUserUserID:
assert "Input should be a valid integer," in str(cm.value)
def test_zero(self):
- from generalresearch.models.thl.user import User
with pytest.raises(expected_exception=ValidationError) as cm:
User(user_id=0)
@@ -52,7 +61,6 @@ class TestUserUserID:
assert "Input should be greater than 0" in str(cm.value)
def test_negative(self):
- from generalresearch.models.thl.user import User
with pytest.raises(expected_exception=ValidationError) as cm:
User(user_id=-1)
@@ -60,7 +68,6 @@ class TestUserUserID:
assert "Input should be greater than 0" in str(cm.value)
def test_too_big(self):
- from generalresearch.models.thl.user import User
val = 2**31
with pytest.raises(expected_exception=ValidationError) as cm:
@@ -69,7 +76,6 @@ class TestUserUserID:
assert "Input should be less than 2147483648" in str(cm.value)
def test_identifiable(self):
- from generalresearch.models.thl.user import User
val = randint(1, 2**30)
user = User(user_id=val)
@@ -80,7 +86,6 @@ class TestUserProductID:
user_id = randint(1, 2**30)
def test_valid(self):
- from generalresearch.models.thl.user import User
product_id = uuid4().hex
@@ -89,7 +94,6 @@ class TestUserProductID:
assert user.product_id == product_id
def test_type(self):
- from generalresearch.models.thl.user import User
with pytest.raises(expected_exception=ValueError) as cm:
User(user_id=self.user_id, product_id=0)
@@ -102,7 +106,6 @@ class TestUserProductID:
assert "Input should be a valid string" in str(cm.value)
def test_empty(self):
- from generalresearch.models.thl.user import User
with pytest.raises(expected_exception=ValueError) as cm:
User(user_id=self.user_id, product_id="")
@@ -110,7 +113,6 @@ class TestUserProductID:
assert "String should have at least 32 characters" in str(cm.value)
def test_invalid_len(self):
- from generalresearch.models.thl.user import User
# Valid uuid4s are 32 char long
product_id = uuid4().hex[:31]
@@ -133,7 +135,6 @@ class TestUserProductID:
assert "String should have at most 32 characters" in str(cm.value)
def test_invalid_uuid(self):
- from generalresearch.models.thl.user import User
# Modify the UUID to break it
product_id = uuid4().hex[:31] + "x"
@@ -144,7 +145,6 @@ class TestUserProductID:
assert "Invalid UUID" in str(cm.value)
def test_invalid_hex_form(self):
- from generalresearch.models.thl.user import User
# Sure not in hex form, but it'll get caught for being the
# wrong length before anything else
@@ -157,7 +157,6 @@ class TestUserProductID:
def test_identifiable(self):
"""Can't create a User with only a product_id because it also
needs to the product_user_id"""
- from generalresearch.models.thl.user import User
product_id = uuid4().hex
with pytest.raises(expected_exception=ValueError) as cm:
@@ -172,10 +171,9 @@ class TestUserProductUserID:
def randomword(self, length: int = 50):
# Raw so nothing is escaped to add additional backslashes
_bpuid_allowed = r"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&()*+,-.:;<=>?@[]^_{|}~"
- return "".join(rand_choice(_bpuid_allowed) for i in range(length))
+ return "".join(rand_choice(_bpuid_allowed) for _ in range(length))
def test_valid(self):
- from generalresearch.models.thl.user import User
product_user_id = uuid4().hex[:12]
user = User(user_id=self.user_id, product_user_id=product_user_id)
@@ -184,7 +182,6 @@ class TestUserProductUserID:
assert user.product_user_id == product_user_id
def test_type(self):
- from generalresearch.models.thl.user import User
with pytest.raises(expected_exception=ValueError) as cm:
User(user_id=self.user_id, product_user_id=0)
@@ -197,12 +194,11 @@ class TestUserProductUserID:
assert "Input should be a valid string" in str(cm.value)
with pytest.raises(ValueError) as cm:
- User(user_id=self.user_id, product_user_id=Decimal("0"))
+ User(user_id=self.user_id, product_user_id=Decimal(0))
assert "1 validation error for User" in str(cm.value)
assert "Input should be a valid string" in str(cm.value)
def test_empty(self):
- from generalresearch.models.thl.user import User
with pytest.raises(expected_exception=ValueError) as cm:
User(user_id=self.user_id, product_user_id="")
@@ -210,7 +206,6 @@ class TestUserProductUserID:
assert "String should have at least 3 characters" in str(cm.value)
def test_invalid_len(self):
- from generalresearch.models.thl.user import User
product_user_id = self.randomword(251)
with pytest.raises(expected_exception=ValueError) as cm:
@@ -225,7 +220,6 @@ class TestUserProductUserID:
assert "String should have at least 3 characters" in str(cm.value)
def test_invalid_chars_space(self):
- from generalresearch.models.thl.user import User
product_user_id = f"{self.randomword(50)} {self.randomword(50)}"
with pytest.raises(expected_exception=ValueError) as cm:
@@ -234,9 +228,8 @@ class TestUserProductUserID:
assert "String cannot contain spaces" in str(cm.value)
def test_invalid_chars_slash(self):
- from generalresearch.models.thl.user import User
- product_user_id = f"{self.randomword(50)}\{self.randomword(50)}"
+ product_user_id = rf"{self.randomword(50)}\{self.randomword(50)}"
with pytest.raises(expected_exception=ValueError) as cm:
User(user_id=self.user_id, product_user_id=product_user_id)
assert "1 validation error for User" in str(cm.value)
@@ -253,7 +246,6 @@ class TestUserProductUserID:
I wanted a test that made sure the regex was hit. I do not know
how we want to provide with the level of specific String checks
we do in here for specific error messages."""
- from generalresearch.models.thl.user import User
product_user_id = f"{self.randomword(50)}`{self.randomword(50)}"
with pytest.raises(expected_exception=ValueError) as cm:
@@ -275,7 +267,6 @@ class TestUserProductUserID:
def test_identifiable(self):
"""Can't create a User with only a product_user_id because it also
needs to the product_id"""
- from generalresearch.models.thl.user import User
product_user_id = uuid4().hex
with pytest.raises(ValueError) as cm:
@@ -288,7 +279,6 @@ class TestUserUUID:
user_id = randint(1, 2**30)
def test_valid(self):
- from generalresearch.models.thl.user import User
uuid_pk = uuid4().hex
@@ -297,7 +287,6 @@ class TestUserUUID:
assert user.uuid == uuid_pk
def test_type(self):
- from generalresearch.models.thl.user import User
with pytest.raises(ValueError) as cm:
User(user_id=self.user_id, uuid=0)
@@ -310,12 +299,11 @@ class TestUserUUID:
assert "Input should be a valid string" in str(cm.value)
with pytest.raises(ValueError) as cm:
- User(user_id=self.user_id, uuid=Decimal("0"))
+ User(user_id=self.user_id, uuid=Decimal(0))
assert "1 validation error for User" in str(cm.value)
assert "Input should be a valid string" in str(cm.value)
def test_empty(self):
- from generalresearch.models.thl.user import User
with pytest.raises(ValueError) as cm:
User(user_id=self.user_id, uuid="")
@@ -323,7 +311,6 @@ class TestUserUUID:
assert "String should have at least 32 characters" in str(cm.value)
def test_invalid_len(self):
- from generalresearch.models.thl.user import User
# Valid uuid4s are 32 char long
uuid_pk = uuid4().hex[:31]
@@ -341,7 +328,6 @@ class TestUserUUID:
assert "String should have at most 32 characters" in str(cm.value)
def test_invalid_uuid(self):
- from generalresearch.models.thl.user import User
# Modify the UUID to break it
uuid_pk = uuid4().hex[:31] + "x"
@@ -352,7 +338,6 @@ class TestUserUUID:
assert "Invalid UUID" in str(cm.value)
def test_invalid_hex_form(self):
- from generalresearch.models.thl.user import User
# Sure not in hex form, but it'll get caught for being the
# wrong length before anything else
@@ -369,7 +354,6 @@ class TestUserUUID:
assert "Invalid UUID" in str(cm.value)
def test_identifiable(self):
- from generalresearch.models.thl.user import User
user_uuid = uuid4().hex
user = User(uuid=user_uuid)
@@ -380,33 +364,29 @@ class TestUserCreated:
user_id = randint(1, 2**30)
def test_valid(self):
- from generalresearch.models.thl.user import User
user = User(user_id=self.user_id)
- dt = datetime.now(tz=timezone.utc)
+ dt = datetime.now(tz=UTC)
user.created = dt
assert user.created == dt
def test_tz_naive_throws_init(self):
- from generalresearch.models.thl.user import User
with pytest.raises(ValueError) as cm:
- User(user_id=self.user_id, created=datetime.now(tz=None))
+ User(user_id=self.user_id, created=datetime.now(tz=None)) # noqa
assert "1 validation error for User" in str(cm.value)
assert "Input should have timezone info" in str(cm.value)
def test_tz_naive_throws_setter(self):
- from generalresearch.models.thl.user import User
user = User(user_id=self.user_id)
with pytest.raises(ValueError) as cm:
- user.created = datetime.now(tz=None)
+ user.created = datetime.now(tz=None) # noqa
assert "1 validation error for User" in str(cm.value)
assert "Input should have timezone info" in str(cm.value)
def test_tz_utc(self):
- from generalresearch.models.thl.user import User
with pytest.raises(ValueError) as cm:
User(
@@ -417,20 +397,18 @@ class TestUserCreated:
assert "Timezone is not UTC" in str(cm.value)
def test_not_in_future(self):
- from generalresearch.models.thl.user import User
- the_future = datetime.now(tz=timezone.utc) + timedelta(minutes=1)
+ the_future = datetime.now(tz=UTC) + timedelta(minutes=1)
with pytest.raises(ValueError) as cm:
User(user_id=self.user_id, created=the_future)
assert "1 validation error for User" in str(cm.value)
assert "Input is in the future" in str(cm.value)
def test_after_anno_domini(self):
- from generalresearch.models.thl.user import User
- before_ad = datetime(
- year=2015, month=1, day=1, tzinfo=timezone.utc
- ) + timedelta(minutes=1)
+ before_ad = datetime(year=2015, month=1, day=1, tzinfo=UTC) + timedelta(
+ minutes=1
+ )
with pytest.raises(ValueError) as cm:
User(user_id=self.user_id, created=before_ad)
assert "1 validation error for User" in str(cm.value)
@@ -441,33 +419,29 @@ class TestUserLastSeen:
user_id = randint(1, 2**30)
def test_valid(self):
- from generalresearch.models.thl.user import User
user = User(user_id=self.user_id)
- dt = datetime.now(tz=timezone.utc)
+ dt = datetime.now(tz=UTC)
user.last_seen = dt
assert user.last_seen == dt
def test_tz_naive_throws_init(self):
- from generalresearch.models.thl.user import User
with pytest.raises(ValueError) as cm:
- User(user_id=self.user_id, last_seen=datetime.now(tz=None))
+ User(user_id=self.user_id, last_seen=datetime.now(tz=None)) # noqa
assert "1 validation error for User" in str(cm.value)
assert "Input should have timezone info" in str(cm.value)
def test_tz_naive_throws_setter(self):
- from generalresearch.models.thl.user import User
user = User(user_id=self.user_id)
with pytest.raises(ValueError) as cm:
- user.last_seen = datetime.now(tz=None)
+ user.last_seen = datetime.now(tz=None) # noqa
assert "1 validation error for User" in str(cm.value)
assert "Input should have timezone info" in str(cm.value)
def test_tz_utc(self):
- from generalresearch.models.thl.user import User
with pytest.raises(ValueError) as cm:
User(
@@ -478,20 +452,18 @@ class TestUserLastSeen:
assert "Timezone is not UTC" in str(cm.value)
def test_not_in_future(self):
- from generalresearch.models.thl.user import User
- the_future = datetime.now(tz=timezone.utc) + timedelta(minutes=1)
+ the_future = datetime.now(tz=UTC) + timedelta(minutes=1)
with pytest.raises(ValueError) as cm:
User(user_id=self.user_id, last_seen=the_future)
assert "1 validation error for User" in str(cm.value)
assert "Input is in the future" in str(cm.value)
def test_after_anno_domini(self):
- from generalresearch.models.thl.user import User
- before_ad = datetime(
- year=2015, month=1, day=1, tzinfo=timezone.utc
- ) + timedelta(minutes=1)
+ before_ad = datetime(year=2015, month=1, day=1, tzinfo=UTC) + timedelta(
+ minutes=1
+ )
with pytest.raises(ValueError) as cm:
User(user_id=self.user_id, last_seen=before_ad)
assert "1 validation error for User" in str(cm.value)
@@ -502,7 +474,6 @@ class TestUserBlocked:
user_id = randint(1, 2**30)
def test_valid(self):
- from generalresearch.models.thl.user import User
user = User(user_id=self.user_id, blocked=True)
assert user.blocked
@@ -510,7 +481,6 @@ class TestUserBlocked:
def test_str_casting(self):
"""We don't want any of these to work, and that's why
we set strict=True on the column"""
- from generalresearch.models.thl.user import User
with pytest.raises(ValueError) as cm:
User(user_id=self.user_id, blocked="true")
@@ -547,20 +517,18 @@ class TestUserTiming:
user_id = randint(1, 2**30)
def test_valid(self):
- from generalresearch.models.thl.user import User
- created = datetime.now(tz=timezone.utc) - timedelta(minutes=60)
- last_seen = datetime.now(tz=timezone.utc) - timedelta(minutes=59)
+ created = datetime.now(tz=UTC) - timedelta(minutes=60)
+ last_seen = datetime.now(tz=UTC) - timedelta(minutes=59)
user = User(user_id=self.user_id, created=created, last_seen=last_seen)
assert user.created == created
assert user.last_seen == last_seen
def test_created_first(self):
- from generalresearch.models.thl.user import User
- created = datetime.now(tz=timezone.utc) - timedelta(minutes=60)
- last_seen = datetime.now(tz=timezone.utc) - timedelta(minutes=59)
+ created = datetime.now(tz=UTC) - timedelta(minutes=60)
+ last_seen = datetime.now(tz=UTC) - timedelta(minutes=59)
with pytest.raises(ValueError) as cm:
User(user_id=self.user_id, created=last_seen, last_seen=created)
@@ -572,7 +540,6 @@ class TestUserModelVerification:
"""Tests that may be dependent on more than 1 attribute"""
def test_identifiable(self):
- from generalresearch.models.thl.user import User
product_id = uuid4().hex
product_user_id = uuid4().hex
@@ -580,7 +547,6 @@ class TestUserModelVerification:
assert user.is_identifiable
def test_valid_helper(self):
- from generalresearch.models.thl.user import User
user_bool = User.is_valid_ubp(
product_id=uuid4().hex, product_user_id=uuid4().hex
@@ -594,7 +560,6 @@ class TestUserModelVerification:
class TestUserSerialization:
def test_basic_json(self):
- from generalresearch.models.thl.user import User
product_id = uuid4().hex
product_user_id = uuid4().hex
@@ -602,7 +567,7 @@ class TestUserSerialization:
user = User(
product_id=product_id,
product_user_id=product_user_id,
- created=datetime.now(tz=timezone.utc),
+ created=datetime.now(tz=UTC),
blocked=False,
)
@@ -615,7 +580,6 @@ class TestUserSerialization:
assert d.get("created").endswith("Z")
def test_basic_dict(self):
- from generalresearch.models.thl.user import User
product_id = uuid4().hex
product_user_id = uuid4().hex
@@ -623,7 +587,7 @@ class TestUserSerialization:
user = User(
product_id=product_id,
product_user_id=product_user_id,
- created=datetime.now(tz=timezone.utc),
+ created=datetime.now(tz=UTC),
blocked=False,
)
@@ -633,10 +597,11 @@ class TestUserSerialization:
assert not d.get("blocked")
assert d.get("product") is None
- assert d.get("created").tzinfo == timezone.utc
+ created = d.get("created")
+ assert isinstance(created, datetime)
+ assert created.tzinfo == UTC
def test_from_json(self):
- from generalresearch.models.thl.user import User
product_id = uuid4().hex
product_user_id = uuid4().hex
@@ -644,41 +609,51 @@ class TestUserSerialization:
user = User(
product_id=product_id,
product_user_id=product_user_id,
- created=datetime.now(tz=timezone.utc),
+ created=datetime.now(tz=UTC),
blocked=False,
)
u = User.model_validate_json(user.to_json())
assert u.product_id == product_id
assert u.product is None
- assert u.created.tzinfo == timezone.utc
+ assert isinstance(u.created, datetime)
+ assert u.created.tzinfo == UTC
class TestUserMethods:
- def test_audit_log(self, user, audit_log_manager):
+ def test_audit_log(
+ self,
+ audit_log_factory: Callable[..., AuditLog],
+ user: User,
+ audit_log_manager: AuditLogManager,
+ ):
assert user.audit_log is None
user.prefetch_audit_log(audit_log_manager=audit_log_manager)
assert user.audit_log == []
- audit_log_manager.create_dummy(user_id=user.user_id)
+ audit_log_factory(user_id=user.user_id)
user.prefetch_audit_log(audit_log_manager=audit_log_manager)
assert len(user.audit_log) == 1
def test_transactions(
- self, user_factory, thl_lm, session_with_tx_factory, product_user_wallet_yes
+ self,
+ user_factory: Callable[..., User],
+ thl_ledger_manager: ThlLedgerManager,
+ session_with_tx_factory: Callable[..., None],
+ product_user_wallet_yes: Product,
):
u1 = user_factory(product=product_user_wallet_yes)
assert u1.transactions is None
- u1.prefetch_transactions(thl_lm=thl_lm)
+ u1.prefetch_transactions(thl_lm=thl_ledger_manager)
assert u1.transactions == []
session_with_tx_factory(user=u1)
- u1.prefetch_transactions(thl_lm=thl_lm)
+ u1.prefetch_transactions(thl_lm=thl_ledger_manager)
assert len(u1.transactions) == 1
@pytest.mark.skip(reason="TODO")
- def test_location_history(self, user):
+ def test_location_history(self, user: User):
assert user.location_history is None
diff --git a/tests/models/thl/test_user_iphistory.py b/tests/models/thl/test_user_iphistory.py
index 596849c..b8a0be3 100644
--- a/tests/models/thl/test_user_iphistory.py
+++ b/tests/models/thl/test_user_iphistory.py
@@ -1,4 +1,6 @@
-from datetime import datetime, timedelta, timezone
+from __future__ import annotations
+
+from datetime import UTC, datetime, timedelta
from generalresearch.models.thl.user_iphistory import (
UserIPHistory,
@@ -8,7 +10,7 @@ from generalresearch.models.thl.user_iphistory import (
def test_collapse_ip_records():
# This does not exist in a db, so we do not need fixtures/ real user ids, whatever
- now = datetime.now(tz=timezone.utc) - timedelta(days=1)
+ now = datetime.now(tz=UTC) - timedelta(days=1)
# Gets stored most recent first. This is reversed, but the validator will order it
records = [
UserIPRecord(ip="1.2.3.5", created=now + timedelta(minutes=1)),
diff --git a/tests/models/thl/test_user_metadata.py b/tests/models/thl/test_user_metadata.py
index 3d851dc..7e84f3e 100644
--- a/tests/models/thl/test_user_metadata.py
+++ b/tests/models/thl/test_user_metadata.py
@@ -1,6 +1,8 @@
+from __future__ import annotations
+
import pytest
-from generalresearch.models import MAX_INT32
+from generalresearch.models.definitions import MAX_INT32
from generalresearch.models.thl.user_profile import UserMetadata
diff --git a/tests/models/thl/test_user_streak.py b/tests/models/thl/test_user_streak.py
index 0cacd3e..8300474 100644
--- a/tests/models/thl/test_user_streak.py
+++ b/tests/models/thl/test_user_streak.py
@@ -1,8 +1,8 @@
from datetime import datetime, timedelta
+from zoneinfo import ZoneInfo
import pytest
from pydantic import ValidationError
-from zoneinfo import ZoneInfo
from generalresearch.models.thl.user_streak import (
StreakFulfillment,
@@ -71,6 +71,7 @@ def test_user_streak_remaining():
)
print(f"{now.isoformat()=}, {end_of_today.isoformat()=}")
expected = (end_of_today - now).total_seconds()
+ assert isinstance(us.time_remaining_in_period, timedelta)
assert us.time_remaining_in_period.total_seconds() == pytest.approx(expected, abs=1)
@@ -92,5 +93,6 @@ def test_user_streak_remaining_month():
).replace(day=1)
print(f"{now.isoformat()=}, {end_of_month.isoformat()=}")
expected = (end_of_month - now).total_seconds()
+ assert isinstance(us.time_remaining_in_period, timedelta)
assert us.time_remaining_in_period.total_seconds() == pytest.approx(expected, abs=1)
print(us.time_remaining_in_period)
diff --git a/tests/models/thl/test_wall.py b/tests/models/thl/test_wall.py
index 8398c81..61ca11d 100644
--- a/tests/models/thl/test_wall.py
+++ b/tests/models/thl/test_wall.py
@@ -1,11 +1,13 @@
-from datetime import datetime, timedelta, timezone
+from __future__ import annotations
+
+from datetime import UTC, datetime, timedelta
from decimal import Decimal
from uuid import uuid4
import pytest
from pydantic import ValidationError
-from generalresearch.models import Source
+from generalresearch.models.definitions import Source
from generalresearch.models.thl.definitions import (
Status,
StatusCode1,
@@ -27,8 +29,8 @@ class TestWall:
ext_status_code_1="1.0",
status=Status.FAIL,
status_code_1=StatusCode1.BUYER_FAIL,
- started=datetime(2023, 1, 1, 0, 0, 1, tzinfo=timezone.utc),
- finished=datetime(2023, 1, 1, 0, 10, 1, tzinfo=timezone.utc),
+ started=datetime(2023, 1, 1, 0, 0, 1, tzinfo=UTC),
+ finished=datetime(2023, 1, 1, 0, 10, 1, tzinfo=UTC),
)
s = w.to_json()
w2 = Wall.from_json(s)
@@ -45,8 +47,8 @@ class TestWall:
survey_id="yyy",
status=Status.FAIL,
status_code_1=StatusCode1.BUYER_FAIL,
- started=datetime.now(timezone.utc),
- finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ started=datetime.now(UTC),
+ finished=datetime.now(UTC) + timedelta(seconds=1),
)
Wall(
user_id=1,
@@ -58,8 +60,8 @@ class TestWall:
status=Status.FAIL,
status_code_1=StatusCode1.MARKETPLACE_FAIL,
status_code_2=WallStatusCode2.COMPLETE_TOO_FAST,
- started=datetime.now(timezone.utc),
- finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ started=datetime.now(UTC),
+ finished=datetime.now(UTC) + timedelta(seconds=1),
)
with pytest.raises(expected_exception=ValidationError) as e:
Wall(
@@ -71,8 +73,8 @@ class TestWall:
survey_id="yyy",
status=Status.FAIL,
status_code_1=StatusCode1.GRS_ABANDON,
- started=datetime.now(timezone.utc),
- finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ started=datetime.now(UTC),
+ finished=datetime.now(UTC) + timedelta(seconds=1),
)
assert "If status is f, status_code_1 should be in" in str(e.value)
@@ -87,8 +89,8 @@ class TestWall:
status=Status.FAIL,
status_code_1=StatusCode1.GRS_ABANDON,
status_code_2=WallStatusCode2.COMPLETE_TOO_FAST,
- started=datetime.now(timezone.utc),
- finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ started=datetime.now(UTC),
+ finished=datetime.now(UTC) + timedelta(seconds=1),
)
assert "If status is f, status_code_1 should be in" in str(e.value)
@@ -104,8 +106,8 @@ class TestWall:
status=Status.FAIL,
status_code_1=StatusCode1.MARKETPLACE_FAIL,
status_code_2=WallStatusCode2.COMPLETE_TOO_FAST,
- started=datetime.now(timezone.utc),
- finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ started=datetime.now(UTC),
+ finished=datetime.now(UTC) + timedelta(seconds=1),
)
Wall(
user_id=1,
@@ -117,8 +119,8 @@ class TestWall:
status=Status.FAIL,
status_code_1=StatusCode1.BUYER_FAIL,
status_code_2=None,
- started=datetime.now(timezone.utc),
- finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ started=datetime.now(UTC),
+ finished=datetime.now(UTC) + timedelta(seconds=1),
)
Wall(
user_id=1,
@@ -130,8 +132,8 @@ class TestWall:
status=Status.COMPLETE,
status_code_1=StatusCode1.COMPLETE,
status_code_2=None,
- started=datetime.now(timezone.utc),
- finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ started=datetime.now(UTC),
+ finished=datetime.now(UTC) + timedelta(seconds=1),
)
with pytest.raises(expected_exception=ValidationError) as e:
@@ -145,8 +147,8 @@ class TestWall:
status=Status.FAIL,
status_code_1=StatusCode1.BUYER_FAIL,
status_code_2=WallStatusCode2.COMPLETE_TOO_FAST,
- started=datetime.now(timezone.utc),
- finished=datetime.now(timezone.utc) + timedelta(seconds=1),
+ started=datetime.now(UTC),
+ finished=datetime.now(UTC) + timedelta(seconds=1),
)
assert "If status_code_1 is 1, status_code_2 should be in" in str(e.value)
diff --git a/tests/models/thl/test_wall_session.py b/tests/models/thl/test_wall_session.py
index 1208c56..40d3619 100644
--- a/tests/models/thl/test_wall_session.py
+++ b/tests/models/thl/test_wall_session.py
@@ -1,9 +1,11 @@
-from datetime import datetime, timedelta, timezone
+from __future__ import annotations
+
+from datetime import UTC, datetime, timedelta
from decimal import Decimal
import pytest
-from generalresearch.models import Source
+from generalresearch.models.definitions import Source
from generalresearch.models.thl.definitions import Status, StatusCode1
from generalresearch.models.thl.session import Session, Wall
from generalresearch.models.thl.user import User
@@ -12,7 +14,7 @@ from generalresearch.models.thl.user import User
class TestWallSession:
def test_session_with_no_wall_events(self):
- started = datetime(2023, 1, 1, tzinfo=timezone.utc)
+ started = datetime(2023, 1, 1, tzinfo=UTC)
s = Session(user=User(user_id=1), started=started)
assert s.status is None
assert s.status_code_1 is None
@@ -24,7 +26,7 @@ class TestWallSession:
# assert s.status_code_1 == StatusCode1.SESSION_START_FAIL
def test_session_timeout_with_only_grs(self):
- started = datetime(2023, 1, 1, tzinfo=timezone.utc)
+ started = datetime(2023, 1, 1, tzinfo=UTC)
s = Session(user=User(user_id=1), started=started)
w = Wall(
user_id=1,
@@ -53,7 +55,7 @@ class TestWallSession:
# assert s.status_code_1 == StatusCode1.GRS_FAIL
def test_session_with_only_grs_complete(self):
- started = datetime(year=2023, month=1, day=1, tzinfo=timezone.utc)
+ started = datetime(year=2023, month=1, day=1, tzinfo=UTC)
# A Session is started
s = Session(user=User(user_id=1), started=started)
@@ -98,7 +100,7 @@ class TestWallSession:
# assert s.status_code_1 is None
def test_session_with_only_non_grs_fail(self):
- started = datetime(year=2023, month=1, day=1, tzinfo=timezone.utc)
+ started = datetime(year=2023, month=1, day=1, tzinfo=UTC)
s = Session(user=User(user_id=1), started=started)
w = Wall(
@@ -119,7 +121,7 @@ class TestWallSession:
assert s.payout is None
def test_session_with_only_non_grs_timeout(self):
- started = datetime(year=2023, month=1, day=1, tzinfo=timezone.utc)
+ started = datetime(year=2023, month=1, day=1, tzinfo=UTC)
s = Session(user=User(user_id=1), started=started)
w = Wall(
@@ -139,7 +141,7 @@ class TestWallSession:
assert s.payout is None
def test_session_with_grs_and_external(self):
- started = datetime(year=2023, month=1, day=1, tzinfo=timezone.utc)
+ started = datetime(year=2023, month=1, day=1, tzinfo=UTC)
s = Session(user=User(user_id=1), started=started)
w = Wall(
@@ -168,7 +170,7 @@ class TestWallSession:
s.append_wall_event(w)
w.finish(
status=Status.ABANDON,
- finished=datetime.now(tz=timezone.utc) + timedelta(minutes=10),
+ finished=datetime.now(tz=UTC) + timedelta(minutes=10),
status_code_1=StatusCode1.BUYER_ABANDON,
)
status, status_code_1 = s.determine_session_status()
@@ -206,7 +208,7 @@ class TestWallSession:
assert s.payout is None
def test_session_marketplace_fail(self):
- started = datetime(2023, 1, 1, tzinfo=timezone.utc)
+ started = datetime(2023, 1, 1, tzinfo=UTC)
s = Session(user=User(user_id=1), started=started)
w = Wall(
@@ -229,7 +231,7 @@ class TestWallSession:
assert StatusCode1.SESSION_CONTINUE_QUALITY_FAIL == s.status_code_1
def test_session_unknown(self):
- started = datetime(2023, 1, 1, tzinfo=timezone.utc)
+ started = datetime(2023, 1, 1, tzinfo=UTC)
s = Session(user=User(user_id=1), started=started)
w = Wall(