diff --git a/packages/discovery-provider/integration_tests/challenges/test_challenges.py b/packages/discovery-provider/integration_tests/challenges/test_challenges.py deleted file mode 100644 index ca849af4f19..00000000000 --- a/packages/discovery-provider/integration_tests/challenges/test_challenges.py +++ /dev/null @@ -1,569 +0,0 @@ -import logging -from datetime import datetime, timezone -from typing import Dict, List, Optional - -from sqlalchemy.orm.session import Session - -from integration_tests.queries.test_get_challenges import DefaultUpdater -from integration_tests.utils import populate_mock_db_blocks -from src.challenges.challenge import ( - ChallengeManager, - ChallengeUpdater, - FullEventMetadata, -) -from src.challenges.challenge_event_bus import ChallengeEventBus -from src.models.indexing.block import Block -from src.models.rewards.challenge import Challenge, ChallengeType -from src.models.rewards.user_challenge import UserChallenge -from src.queries.get_challenges import get_challenges -from src.utils.config import shared_config -from src.utils.db_session import get_db -from src.utils.helpers import model_to_dictionary -from src.utils.redis_connection import get_redis - -logger = logging.getLogger(__name__) - -AGGREGATE_CHALLENGE_REWARD_AMOUNT = 5 -AGGREGATE_CHALLENGE_STEP_COUNT = 5 -TEST_BLOCK_DATETIME = datetime.now() - - -def get_created_at(): - return datetime.strptime( - "2023-10-13 11:15:10.627328+00", "%Y-%m-%d %H:%M:%S.%f+00" - ).replace(tzinfo=timezone.utc) - - -def setup_challenges(app): - with app.app_context(): - db = get_db() - challenges = [ - Challenge( - id="test_challenge_1", - type=ChallengeType.numeric, - amount="5", - step_count=3, - active=True, - starting_block=100, - ), - Challenge( - id="test_challenge_2", - type=ChallengeType.boolean, - amount="5", - active=True, - starting_block=100, - ), - Challenge( - id="test_challenge_3", - type=ChallengeType.aggregate, - amount=str(AGGREGATE_CHALLENGE_REWARD_AMOUNT), - step_count=AGGREGATE_CHALLENGE_REWARD_AMOUNT - * AGGREGATE_CHALLENGE_STEP_COUNT, - active=True, - starting_block=100, - ), - Challenge( - id="some_inactive_challenge", - type=ChallengeType.numeric, - amount="5", - step_count=5, - active=False, - starting_block=0, - ), - ] - user_challenges = [ - UserChallenge( - challenge_id="test_challenge_1", - user_id=1, - specifier="1", - is_complete=False, - current_step_count=1, - amount=5, - created_at=get_created_at(), - ), - UserChallenge( - challenge_id="test_challenge_1", - user_id=2, - specifier="2", - is_complete=True, - completed_at=TEST_BLOCK_DATETIME, - current_step_count=3, - completed_blocknumber=100, - amount=5, - created_at=get_created_at(), - ), - UserChallenge( - challenge_id="test_challenge_1", - user_id=3, - specifier="3", - current_step_count=2, - is_complete=False, - amount=5, - created_at=get_created_at(), - ), - UserChallenge( - challenge_id="test_challenge_2", - user_id=4, - specifier="4", - is_complete=True, - completed_at=TEST_BLOCK_DATETIME, - amount=5, - created_at=get_created_at(), - ), - UserChallenge( - challenge_id="test_challenge_1", - user_id=5, - specifier="5", - is_complete=False, - current_step_count=2, - amount=5, - created_at=get_created_at(), - ), - ] - - with db.scoped_session() as session: - # Wipe any existing challenges in the DB from running migrations, etc - session.query(Challenge).delete() - session.add_all(challenges) - session.flush() - session.add_all(user_challenges) - - -class TestUpdater(ChallengeUpdater): - def update_user_challenges( - self, - session: Session, - event: str, - user_challenges: List[UserChallenge], - step_count: Optional[int], - event_metadatas: List[FullEventMetadata], - starting_block: Optional[int], - ): - for user_challenge in user_challenges: - if step_count is not None and user_challenge.current_step_count is not None: - user_challenge.current_step_count += 1 - if user_challenge.current_step_count >= step_count: - user_challenge.is_complete = True - - -def test_handle_event(app): - setup_challenges(app) - - with app.app_context(): - db = get_db() - - populate_mock_db_blocks(db, 99, 110) - - with db.scoped_session() as session: - my_challenge = ChallengeManager("test_challenge_1", TestUpdater()) - # First try an event with a insufficient block_number - # to ensure that nothing happens - my_challenge.process( - session, - "test_event", - [ - { - "user_id": 1, - "block_number": 99, - "block_datetime": TEST_BLOCK_DATETIME, - "extra": {}, - }, - ], - ) - session.flush() - actual = ( - session.query(UserChallenge) - .filter( - UserChallenge.challenge_id == "test_challenge_1", - UserChallenge.user_id == 1, - ) - .first() - ) - expected = { - "challenge_id": "test_challenge_1", - "user_id": 1, - "specifier": "1", - "is_complete": False, - "completed_at": None, - "current_step_count": 1, - "completed_blocknumber": None, - "amount": 5, - "created_at": get_created_at(), - } - assert model_to_dictionary(actual) == expected - - # Now process events and make sure things change as expected - my_challenge.process( - session, - "test_event", - [ - { - "user_id": 1, - "block_number": 100, - "block_datetime": TEST_BLOCK_DATETIME, - "extra": {}, - }, - { - "user_id": 2, - "block_number": 100, - "block_datetime": TEST_BLOCK_DATETIME, - "extra": {}, - }, - { - "user_id": 3, - "block_number": 100, - "block_datetime": TEST_BLOCK_DATETIME, - "extra": {}, - }, - # Attempt to add id 6 twice to - # ensure that it doesn't cause a collision - { - "user_id": 6, - "block_number": 100, - "block_datetime": TEST_BLOCK_DATETIME, - "extra": {}, - }, - { - "user_id": 6, - "block_number": 100, - "block_datetime": TEST_BLOCK_DATETIME, - "extra": {}, - }, - ], - ) - session.flush() - - updated_complete = ( - session.query(UserChallenge) - .filter(UserChallenge.challenge_id == "test_challenge_1") - .all() - ) - res_dicts = list(map(model_to_dictionary, updated_complete)) - expected = [ - # Should have incremented step count + 1 - { - "challenge_id": "test_challenge_1", - "user_id": 1, - "specifier": "1", - "is_complete": False, - "completed_at": None, - "current_step_count": 2, - "completed_blocknumber": None, - "amount": 5, - "created_at": get_created_at(), - }, - # Should be unchanged b/c it was already complete - { - "challenge_id": "test_challenge_1", - "user_id": 2, - "specifier": "2", - "is_complete": True, - "completed_at": TEST_BLOCK_DATETIME, - "current_step_count": 3, - "completed_blocknumber": 100, - "amount": 5, - "created_at": get_created_at(), - }, - # Should be newly complete - { - "challenge_id": "test_challenge_1", - "user_id": 3, - "specifier": "3", - "is_complete": True, - "completed_at": TEST_BLOCK_DATETIME, - "current_step_count": 3, - "completed_blocknumber": 100, - "amount": 5, - "created_at": get_created_at(), - }, - # Should be untouched bc user 5 wasn't included - { - "challenge_id": "test_challenge_1", - "user_id": 5, - "specifier": "5", - "is_complete": False, - "completed_at": None, - "current_step_count": 2, - "completed_blocknumber": None, - "amount": 5, - "created_at": get_created_at(), - }, - # Should have created a brand new user 6 - { - "challenge_id": "test_challenge_1", - "user_id": 6, - "specifier": "6", - "is_complete": False, - "completed_at": None, - "current_step_count": 1, - "completed_blocknumber": None, - "amount": 5, - }, - ] - # the last challenge was just created so we don't know the created_at time - del res_dicts[-1]["created_at"] - assert expected == res_dicts - - -class AggregateUpdater(ChallengeUpdater): - def update_user_challenges( - self, - session: Session, - event: str, - user_challenges: List[UserChallenge], - step_count: Optional[int], - event_metadatas: List[FullEventMetadata], - starting_block: Optional[int], - ): - pass - - def generate_specifier(self, session: Session, user_id: int, extra: Dict) -> str: - return f"{user_id}-{extra['referred_id']}" - - -REDIS_URL = shared_config["redis"]["url"] - - -def test_aggregates(app): - setup_challenges(app) - - with app.app_context(): - db = get_db() - - redis_conn = get_redis() - - with db.scoped_session() as session: - bus = ChallengeEventBus(redis_conn) - agg_challenge = ChallengeManager("test_challenge_3", AggregateUpdater()) - agg_challenge.process(session, "test_event", []) - TEST_EVENT = "TEST_EVENT" - - bus.register_listener( - TEST_EVENT, ChallengeManager("test_challenge_1", DefaultUpdater()) - ) - bus.register_listener( - TEST_EVENT, ChallengeManager("test_challenge_2", DefaultUpdater()) - ) - # - Multiple events with the same user_id but diff specifiers get created - bus.register_listener(TEST_EVENT, agg_challenge) - bus.dispatch( - TEST_EVENT, 100, TEST_BLOCK_DATETIME, 1, {"referred_id": 2, "amount": 4} - ) - bus.dispatch( - TEST_EVENT, 100, TEST_BLOCK_DATETIME, 1, {"referred_id": 3, "amount": 6} - ) - bus.flush() - bus.process_events(session) - state = agg_challenge.get_user_challenge_state(session, ["1-2", "1-3"]) - assert len(state) == 2 - # Test different amounts for the same challenge - assert state[0].amount == 4 - assert state[1].amount == 6 - - # Also make sure the thing is incomplete - res = get_challenges(1, False, session, bus) - agg_chal = {c["challenge_id"]: c for c in res}["test_challenge_3"] - assert agg_chal["is_complete"] == False - - # - Multiple events with the same specifier get deduped - bus.dispatch(TEST_EVENT, 100, TEST_BLOCK_DATETIME, 1, {"referred_id": 4}) - bus.dispatch(TEST_EVENT, 100, TEST_BLOCK_DATETIME, 1, {"referred_id": 4}) - bus.flush() - bus.process_events(session) - state = agg_challenge.get_user_challenge_state(session, ["1-4"]) - state = agg_challenge.get_user_challenge_state(session, ["1-4"]) - assert len(state) == 1 - - def get_user_challenges(): - return ( - session.query(UserChallenge) - .filter( - UserChallenge.challenge_id == "test_challenge_3", - UserChallenge.user_id == 1, - ) - .all() - ) - - # - If we've maxed the # of challenges, don't create any more - # (AGGREGATE_CHALLENGE_STEP_COUNT = 5) - bus.dispatch(TEST_EVENT, 100, TEST_BLOCK_DATETIME, 1, {"referred_id": 5}) - bus.dispatch(TEST_EVENT, 100, TEST_BLOCK_DATETIME, 1, {"referred_id": 6}) - bus.flush() - bus.process_events(session) - assert len(get_user_challenges()) == AGGREGATE_CHALLENGE_STEP_COUNT - bus.dispatch(TEST_EVENT, 100, TEST_BLOCK_DATETIME, 1, {"referred_id": 7}) - bus.flush() - bus.process_events(session) - assert len(get_user_challenges()) == AGGREGATE_CHALLENGE_STEP_COUNT - - # Test get_challenges - res = get_challenges(1, False, session, bus) - agg_chal = {c["challenge_id"]: c for c in res}["test_challenge_3"] - assert agg_chal["is_complete"] == True - # Assert all user challenges have proper finishing block # - user_challenges = get_user_challenges() - for uc in user_challenges: - assert uc.completed_blocknumber == 100 - - -def test_in_memory_queue(app): - setup_challenges(app) - - with app.app_context(): - db = get_db() - - redis_conn = get_redis() - - bus = ChallengeEventBus(redis_conn) - with db.scoped_session() as session, bus.use_scoped_dispatch_queue(): - agg_challenge = ChallengeManager("test_challenge_3", AggregateUpdater()) - agg_challenge.process(session, "test_event", []) - TEST_EVENT = "TEST_EVENT" - - bus.register_listener( - TEST_EVENT, ChallengeManager("test_challenge_1", DefaultUpdater()) - ) - bus.register_listener( - TEST_EVENT, ChallengeManager("test_challenge_2", DefaultUpdater()) - ) - # - Multiple events with the same user_id but diff specifiers get created - bus.register_listener(TEST_EVENT, agg_challenge) - bus.dispatch(TEST_EVENT, 100, TEST_BLOCK_DATETIME, 1, {"referred_id": 2}) - bus.dispatch(TEST_EVENT, 100, TEST_BLOCK_DATETIME, 1, {"referred_id": 3}) - bus.process_events(session) - - # no events should be processed because we haven't dispatched yet - state = agg_challenge.get_user_challenge_state(session, ["1-2", "1-3"]) - assert len(state) == 0 - - bus.process_events(session) - state = agg_challenge.get_user_challenge_state(session, ["1-2", "1-3"]) - assert len(state) == 2 - # Also make sure the thing is incomplete - res = get_challenges(1, False, session, bus) - agg_chal = {c["challenge_id"]: c for c in res}["test_challenge_3"] - assert agg_chal["is_complete"] == False - - redis_conn = get_redis() - - -def test_inactive_challenge(app): - setup_challenges(app) - with app.app_context(): - db = get_db() - - redis_conn = get_redis() - - bus = ChallengeEventBus(redis_conn) - with db.scoped_session() as session: - mgr = ChallengeManager("some_inactive_challenge", DefaultUpdater()) - TEST_EVENT = "TEST_EVENT" - bus.register_listener(TEST_EVENT, mgr) - with bus.use_scoped_dispatch_queue(): - bus.dispatch(TEST_EVENT, 100, TEST_BLOCK_DATETIME, 1, {}) - bus.process_events(session) - state = mgr.get_user_challenge_state(session, ["1"]) - # We should not have any UserChallenges created for the - # inactive challenge!! - assert len(state) == 0 - - -def test_rejects_invalid_events(app): - setup_challenges(app) - with app.app_context(): - db = get_db() - - redis_conn = get_redis() - - bus = ChallengeEventBus(redis_conn) - with db.scoped_session() as session: - mgr = ChallengeManager("test_challenge_1", DefaultUpdater()) - TEST_EVENT = "TEST_EVENT" - bus.register_listener(TEST_EVENT, mgr) - with bus.use_scoped_dispatch_queue(): - bus.dispatch(TEST_EVENT, None, TEST_BLOCK_DATETIME, 1) - bus.dispatch(TEST_EVENT, 1, TEST_BLOCK_DATETIME, None) - bus.dispatch(TEST_EVENT, 1, TEST_BLOCK_DATETIME, 1, 1) - (count, did_error) = bus.process_events(session) - assert count == 0 - assert did_error == False - - -class BrokenUpdater(ChallengeUpdater): - # This should trigger a postgres exceptions - def on_after_challenge_creation(self, session, metadatas: List[FullEventMetadata]): - block1 = Block(blockhash="0x99", number=1) - block2 = Block(blockhash="0x99", number=1) - session.add_all([block1, block2]) - session.flush() - - -def test_catches_exceptions_in_single_processor(app): - """Ensure that if a single processor fails, the others still succeed""" - with app.app_context(): - db = get_db() - - redis_conn = get_redis() - - bus = ChallengeEventBus(redis_conn) - with db.scoped_session() as session: - session.add_all( - [ - Challenge( - id="test_challenge_1", - type=ChallengeType.numeric, - amount="5", - step_count=3, - active=True, - ), - Challenge( - id="test_challenge_2", - type=ChallengeType.numeric, - amount="5", - step_count=3, - active=True, - ), - ] - ) - session.commit() - - correct_manager = ChallengeManager("test_challenge_1", DefaultUpdater()) - broken_manager = ChallengeManager("test_challenge_2", BrokenUpdater()) - TEST_EVENT = "TEST_EVENT" - TEST_EVENT_2 = "TEST_EVENT_2" - bus.register_listener(TEST_EVENT, correct_manager) - bus.register_listener(TEST_EVENT_2, broken_manager) - - with bus.use_scoped_dispatch_queue(): - # dispatch the broken one first - bus.dispatch(TEST_EVENT_2, 101, TEST_BLOCK_DATETIME, 1) - bus.dispatch(TEST_EVENT, 101, TEST_BLOCK_DATETIME, 1) - try: - bus.process_events(session) - except: - # pylint: disable=W0707 - raise Exception("Shouldn't have propogated error!") - challenge_1_state = correct_manager.get_user_challenge_state(session, ["1"]) - # Make sure that the 'correct_manager' still executes - assert len(challenge_1_state) == 1 - assert challenge_1_state[0].current_step_count == 1 - # Make sure broken manager didn't do anything - challenge_2_state = broken_manager.get_user_challenge_state(session, ["1"]) - assert len(challenge_2_state) == 0 - - # Try the other order - with bus.use_scoped_dispatch_queue(): - # dispatch the correct one first - bus.dispatch(TEST_EVENT, 101, TEST_BLOCK_DATETIME, 1) - bus.dispatch(TEST_EVENT_2, 101, TEST_BLOCK_DATETIME, 1) - try: - bus.process_events(session) - except: - # pylint: disable=W0707 - raise Exception("Shouldn't have propogated error!") - challenge_1_state = correct_manager.get_user_challenge_state(session, ["1"]) - assert len(challenge_1_state) == 1 - assert challenge_1_state[0].current_step_count == 2 - # Make sure broken manager didn't do anything - challenge_2_state = broken_manager.get_user_challenge_state(session, ["1"]) - assert len(challenge_2_state) == 0 diff --git a/packages/discovery-provider/integration_tests/queries/test_get_aggregate_app_metrics.py b/packages/discovery-provider/integration_tests/queries/test_get_aggregate_app_metrics.py deleted file mode 100644 index ce37466e974..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_aggregate_app_metrics.py +++ /dev/null @@ -1,152 +0,0 @@ -from datetime import date, timedelta - -from src.models.metrics.aggregate_daily_app_name_metrics import ( - AggregateDailyAppNameMetric, -) -from src.models.metrics.aggregate_monthly_app_name_metrics import ( - AggregateMonthlyAppNameMetric, -) -from src.queries.get_app_name_metrics import _get_aggregate_app_metrics -from src.utils.db_session import get_db - -limit = 2 -today = date.today() -yesterday = today - timedelta(days=1) - - -def test_get_aggregate_app_metrics_week(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateDailyAppNameMetric( - application_name="will-not-return-because-too-old", - count=3, - timestamp=today - timedelta(days=8), - ), - AggregateDailyAppNameMetric( - application_name="top-app", - count=4, - timestamp=yesterday - timedelta(days=1), - ), - AggregateDailyAppNameMetric( - application_name="will-not-return-because-outside-limit", - count=1, - timestamp=yesterday, - ), - AggregateDailyAppNameMetric( - application_name="best-app", count=5, timestamp=yesterday - ), - AggregateDailyAppNameMetric( - application_name="top-app", count=3, timestamp=yesterday - ), - AggregateDailyAppNameMetric( - application_name="will-not-return-because-too-recent", - count=3, - timestamp=today, - ), - ] - ) - - aggregate_metrics = _get_aggregate_app_metrics(session, "week", limit) - - assert len(aggregate_metrics) == limit - assert aggregate_metrics[0]["name"] == "top-app" - assert aggregate_metrics[0]["count"] == 7 - assert aggregate_metrics[1]["name"] == "best-app" - assert aggregate_metrics[1]["count"] == 5 - - -def test_get_aggregate_app_metrics_month(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateDailyAppNameMetric( - application_name="will-not-return-because-too-old", - count=20, - timestamp=today - timedelta(days=31), - ), - AggregateDailyAppNameMetric( - application_name="best-app", - count=20, - timestamp=today - timedelta(days=31), - ), - AggregateDailyAppNameMetric( - application_name="will-not-return-because-outside-limit", - count=1, - timestamp=yesterday, - ), - AggregateDailyAppNameMetric( - application_name="top-app", - count=5, - timestamp=yesterday - timedelta(days=8), - ), - AggregateDailyAppNameMetric( - application_name="best-app", count=5, timestamp=yesterday - ), - AggregateDailyAppNameMetric( - application_name="top-app", count=7, timestamp=yesterday - ), - AggregateDailyAppNameMetric( - application_name="will-not-return-because-too-recent", - count=20, - timestamp=today, - ), - ] - ) - - aggregate_metrics = _get_aggregate_app_metrics(session, "month", limit) - - assert len(aggregate_metrics) == limit - assert aggregate_metrics[0]["name"] == "top-app" - assert aggregate_metrics[0]["count"] == 12 - assert aggregate_metrics[1]["name"] == "best-app" - assert aggregate_metrics[1]["count"] == 5 - - -def test_get_aggregate_app_metrics_all_time(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateMonthlyAppNameMetric( - application_name="awesome-app", - count=6, - timestamp=today - timedelta(days=367), - ), - AggregateMonthlyAppNameMetric( - application_name="will-not-return-because-outside-limit", - count=1, - timestamp=yesterday, - ), - AggregateMonthlyAppNameMetric( - application_name="best-app", count=5, timestamp=yesterday - ), - AggregateMonthlyAppNameMetric( - application_name="top-app", count=15, timestamp=yesterday - ), - AggregateMonthlyAppNameMetric( - application_name="awesome-app", count=7, timestamp=yesterday - ), - AggregateMonthlyAppNameMetric( - application_name="will-not-return-because-too-recent", - count=20, - timestamp=today, - ), - ] - ) - - aggregate_metrics = _get_aggregate_app_metrics(session, "all_time", limit) - - assert len(aggregate_metrics) == limit - assert aggregate_metrics[0]["name"] == "top-app" - assert aggregate_metrics[0]["count"] == 15 - assert aggregate_metrics[1]["name"] == "awesome-app" - assert aggregate_metrics[1]["count"] == 13 diff --git a/packages/discovery-provider/integration_tests/queries/test_get_aggregate_route_metrics.py b/packages/discovery-provider/integration_tests/queries/test_get_aggregate_route_metrics.py deleted file mode 100644 index 22fbbfe5558..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_aggregate_route_metrics.py +++ /dev/null @@ -1,215 +0,0 @@ -from datetime import date, timedelta - -from src.models.metrics.aggregate_daily_total_users_metrics import ( - AggregateDailyTotalUsersMetrics, -) -from src.models.metrics.aggregate_daily_unique_users_metrics import ( - AggregateDailyUniqueUsersMetrics, -) -from src.models.metrics.aggregate_monthly_total_users_metrics import ( - AggregateMonthlyTotalUsersMetric, -) -from src.models.metrics.aggregate_monthly_unique_users_metrics import ( - AggregateMonthlyUniqueUsersMetric, -) -from src.queries.get_aggregate_route_metrics import _get_aggregate_route_metrics -from src.utils.db_session import get_db - -limit = 2 -today = date.today() -yesterday = today - timedelta(days=1) - - -def test_get_aggregate_route_metrics_week(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateDailyUniqueUsersMetrics( - count=1, summed_count=2, timestamp=today - timedelta(days=8) - ), - AggregateDailyUniqueUsersMetrics( - count=2, summed_count=3, timestamp=yesterday - timedelta(days=1) - ), - AggregateDailyUniqueUsersMetrics( - count=3, summed_count=4, timestamp=yesterday - ), - AggregateDailyUniqueUsersMetrics( - count=4, summed_count=5, timestamp=today - ), - AggregateDailyTotalUsersMetrics( - count=2, timestamp=today - timedelta(days=8) - ), - AggregateDailyTotalUsersMetrics( - count=4, timestamp=yesterday - timedelta(days=1) - ), - AggregateDailyTotalUsersMetrics(count=6, timestamp=yesterday), - AggregateDailyTotalUsersMetrics(count=8, timestamp=today), - ] - ) - - aggregate_metrics = _get_aggregate_route_metrics(session, "week", "day") - - assert len(aggregate_metrics) == 2 - assert aggregate_metrics[0]["unique_count"] == 2 - assert aggregate_metrics[0]["summed_unique_count"] == 3 - assert aggregate_metrics[0]["total_count"] == 4 - assert aggregate_metrics[1]["unique_count"] == 3 - assert aggregate_metrics[1]["summed_unique_count"] == 4 - assert aggregate_metrics[1]["total_count"] == 6 - - -def test_get_aggregate_route_metrics_month_daily_bucket(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateDailyUniqueUsersMetrics( - count=1, summed_count=2, timestamp=today - timedelta(days=31) - ), - AggregateDailyUniqueUsersMetrics( - count=2, summed_count=3, timestamp=today - timedelta(days=8) - ), - AggregateDailyUniqueUsersMetrics( - count=3, summed_count=4, timestamp=yesterday - ), - AggregateDailyUniqueUsersMetrics( - count=4, summed_count=5, timestamp=today - ), - AggregateDailyTotalUsersMetrics( - count=2, timestamp=today - timedelta(days=31) - ), - AggregateDailyTotalUsersMetrics( - count=4, timestamp=today - timedelta(days=8) - ), - AggregateDailyTotalUsersMetrics(count=6, timestamp=yesterday), - AggregateDailyTotalUsersMetrics(count=8, timestamp=today), - ] - ) - - aggregate_metrics = _get_aggregate_route_metrics(session, "month", "day") - - assert len(aggregate_metrics) == 2 - assert aggregate_metrics[0]["unique_count"] == 2 - assert aggregate_metrics[0]["summed_unique_count"] == 3 - assert aggregate_metrics[0]["total_count"] == 4 - assert aggregate_metrics[1]["unique_count"] == 3 - assert aggregate_metrics[1]["summed_unique_count"] == 4 - assert aggregate_metrics[1]["total_count"] == 6 - - -def test_get_aggregate_route_metrics_month_weekly_bucket(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateDailyUniqueUsersMetrics( - count=1, summed_count=2, timestamp=today - timedelta(days=31) - ), - AggregateDailyUniqueUsersMetrics( - count=2, summed_count=3, timestamp=today - timedelta(days=8) - ), - AggregateDailyUniqueUsersMetrics( - count=3, summed_count=4, timestamp=yesterday - ), - AggregateDailyUniqueUsersMetrics( - count=4, summed_count=5, timestamp=today - ), - AggregateDailyTotalUsersMetrics( - count=2, timestamp=today - timedelta(days=31) - ), - AggregateDailyTotalUsersMetrics( - count=4, timestamp=today - timedelta(days=8) - ), - AggregateDailyTotalUsersMetrics(count=6, timestamp=yesterday), - AggregateDailyTotalUsersMetrics(count=8, timestamp=today), - ] - ) - - aggregate_metrics = _get_aggregate_route_metrics(session, "month", "week") - - assert len(aggregate_metrics) == 2 - assert aggregate_metrics[0]["unique_count"] == 2 - assert aggregate_metrics[0]["summed_unique_count"] == 3 - assert aggregate_metrics[0]["total_count"] == 4 - assert aggregate_metrics[1]["unique_count"] == 3 - assert aggregate_metrics[1]["summed_unique_count"] == 4 - assert aggregate_metrics[1]["total_count"] == 6 - - -def test_get_aggregate_route_metrics_all_time_monthly_bucket(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateMonthlyUniqueUsersMetric( - count=1, summed_count=2, timestamp=today - timedelta(days=367) - ), - AggregateMonthlyUniqueUsersMetric( - count=2, summed_count=3, timestamp=today - timedelta(days=100) - ), - AggregateMonthlyUniqueUsersMetric( - count=3, summed_count=4, timestamp=today - ), - AggregateMonthlyTotalUsersMetric( - count=2, timestamp=today - timedelta(days=367) - ), - AggregateMonthlyTotalUsersMetric( - count=4, timestamp=today - timedelta(days=100) - ), - AggregateMonthlyTotalUsersMetric(count=6, timestamp=today), - ] - ) - - aggregate_metrics = _get_aggregate_route_metrics(session, "all_time", "month") - - assert len(aggregate_metrics) == 2 - assert aggregate_metrics[0]["unique_count"] == 1 - assert aggregate_metrics[0]["summed_unique_count"] == 2 - assert aggregate_metrics[0]["total_count"] == 2 - assert aggregate_metrics[1]["unique_count"] == 2 - assert aggregate_metrics[1]["summed_unique_count"] == 3 - assert aggregate_metrics[1]["total_count"] == 4 - - -def test_get_aggregate_route_metrics_all_time_weekly_bucket(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateDailyUniqueUsersMetrics( - count=1, summed_count=2, timestamp=today - timedelta(days=367) - ), - AggregateDailyUniqueUsersMetrics( - count=2, summed_count=3, timestamp=yesterday - ), - AggregateDailyUniqueUsersMetrics( - count=3, summed_count=4, timestamp=today - ), - AggregateDailyTotalUsersMetrics( - count=2, timestamp=today - timedelta(days=367) - ), - AggregateDailyTotalUsersMetrics(count=4, timestamp=yesterday), - AggregateDailyTotalUsersMetrics(count=6, timestamp=today), - ] - ) - - aggregate_metrics = _get_aggregate_route_metrics(session, "all_time", "week") - - assert len(aggregate_metrics) == 2 - assert aggregate_metrics[0]["unique_count"] == 1 - assert aggregate_metrics[0]["summed_unique_count"] == 2 - assert aggregate_metrics[0]["total_count"] == 2 - assert aggregate_metrics[1]["unique_count"] == 2 - assert aggregate_metrics[1]["summed_unique_count"] == 3 - assert aggregate_metrics[1]["total_count"] == 4 diff --git a/packages/discovery-provider/integration_tests/queries/test_get_aggregate_route_metrics_trailing_month.py b/packages/discovery-provider/integration_tests/queries/test_get_aggregate_route_metrics_trailing_month.py deleted file mode 100644 index 61a81e9fe75..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_aggregate_route_metrics_trailing_month.py +++ /dev/null @@ -1,51 +0,0 @@ -from datetime import date, timedelta - -from src.models.metrics.aggregate_daily_total_users_metrics import ( - AggregateDailyTotalUsersMetrics, -) -from src.models.metrics.aggregate_daily_unique_users_metrics import ( - AggregateDailyUniqueUsersMetrics, -) -from src.queries.get_trailing_metrics import _get_aggregate_route_metrics_trailing -from src.utils.db_session import get_db - -limit = 2 -today = date.today() -yesterday = today - timedelta(days=1) - - -def test_get_aggregate_route_metrics_trailing_month(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateDailyUniqueUsersMetrics( - count=1, summed_count=2, timestamp=today - timedelta(days=31) - ), - AggregateDailyUniqueUsersMetrics( - count=2, summed_count=3, timestamp=today - timedelta(days=30) - ), - AggregateDailyUniqueUsersMetrics( - count=3, summed_count=4, timestamp=yesterday - ), - AggregateDailyUniqueUsersMetrics( - count=4, summed_count=5, timestamp=today - ), - AggregateDailyTotalUsersMetrics( - count=2, timestamp=today - timedelta(days=31) - ), - AggregateDailyTotalUsersMetrics( - count=4, timestamp=today - timedelta(days=30) - ), - AggregateDailyTotalUsersMetrics(count=6, timestamp=yesterday), - AggregateDailyTotalUsersMetrics(count=8, timestamp=today), - ] - ) - - aggregate_metrics = _get_aggregate_route_metrics_trailing(session, "month") - - assert aggregate_metrics["unique_count"] == 5 - assert aggregate_metrics["summed_unique_count"] == 7 - assert aggregate_metrics["total_count"] == 10 diff --git a/packages/discovery-provider/integration_tests/queries/test_get_attestation.py b/packages/discovery-provider/integration_tests/queries/test_get_attestation.py deleted file mode 100644 index 009ad3b27cf..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_attestation.py +++ /dev/null @@ -1,326 +0,0 @@ -from unittest.mock import patch - -import pytest -from eth_keys import keys -from eth_utils.conversions import to_bytes -from hexbytes import HexBytes -from solders.pubkey import Pubkey -from web3 import Web3 - -from integration_tests.queries.test_get_challenges import setup_db -from integration_tests.utils import populate_mock_db -from src.models.rewards.challenge import ChallengeType -from src.queries.get_attestation import ( - ADD_SENDER_MESSAGE_PREFIX, - POOL_EXHAUSTED, - REWARDS_MANAGER_ACCOUNT, - Attestation, - AttestationError, - get_attestation, - get_create_sender_attestation, -) -from src.tasks.index_oracles import oracle_addresses_key -from src.utils.config import shared_config -from src.utils.db_session import get_db -from src.utils.redis_connection import get_redis - -REDIS_URL = shared_config["redis"]["url"] -redis_handle = get_redis() - - -DUMMY_ORACLE_ADDRESS = "0x32a10e91820fd10366AC363eD0DEa40B2e598D22" - - -def test_get_attestation(app): - with app.app_context(): - db = get_db() - with db.scoped_session() as session: - setup_db(session) - - # Tests: - # - Happy path - # - No user_challenge - # - Challenge not finished - # - No disbursement - # - Invalid oracle - oracle_address = DUMMY_ORACLE_ADDRESS - redis_handle.set(oracle_addresses_key, oracle_address) - - delegate_owner_wallet, signature = get_attestation( - session, - user_id=1, - challenge_id="boolean_challenge_2", - oracle_address=oracle_address, - specifier="1", - ) - - attestation = Attestation( - amount="5", - oracle_address=oracle_address, - # Wallet from user 2 in setup_db - user_address="0x38C68fF3926bf4E68289672F75ee1543117dD9B3", - challenge_id="boolean_challenge_2", - challenge_specifier="1", - ) - - # Test happy path - - # confirm the attestation is what we think it should be - config_owner_wallet = shared_config["delegate"]["owner_wallet"] - config_private_key = shared_config["delegate"]["private_key"] - - # Ensure we returned the correct owner wallet - assert delegate_owner_wallet == config_owner_wallet - - # Ensure we can derive the owner wallet from the signed stringified attestation - attestation_bytes = attestation.get_attestation_bytes() - to_sign_hash = Web3.keccak(attestation_bytes) - private_key = keys.PrivateKey(HexBytes(config_private_key)) - public_key = keys.PublicKey.from_private(private_key) - signture_bytes = to_bytes(hexstr=signature) - msg_signature = keys.Signature(signature_bytes=signture_bytes, vrs=None) - - recovered_pubkey = public_key.recover_from_msg_hash( - message_hash=to_sign_hash, signature=msg_signature - ) - - assert ( - Web3.to_checksum_address(recovered_pubkey.to_address()) - == config_owner_wallet - ) - - # Test no matching user challenge - with pytest.raises(AttestationError): - get_attestation( - session, - user_id=1, - challenge_id="boolean_challenge_2", - oracle_address=oracle_address, - specifier="xyz", - ) - - # Test challenge not finished - with pytest.raises(AttestationError): - get_attestation( - session, - user_id=1, - challenge_id="boolean_challenge_3", - oracle_address=oracle_address, - specifier="1", - ) - - # Test challenge already disbursed - with pytest.raises(AttestationError): - get_attestation( - session, - user_id=1, - challenge_id="boolean_challenge_1", - oracle_address=oracle_address, - specifier="1", - ) - - # Test with bad AAO - with pytest.raises(AttestationError): - get_attestation( - session, - user_id=1, - challenge_id="boolean_challenge_2", - oracle_address="wrong_oracle_address", - specifier="1", - ) - - -def test_get_attestation_weekly_pool_exhausted(app): - with app.app_context(): - db = get_db() - - entities = { - "users": [ - {"user_id": 1, "wallet": "0x38C68fF3926bf4E68289672F75ee1543117dDAAA"}, - {"user_id": 2, "wallet": "0x38C68fF3926bf4E68289672F75ee1543117dD9B3"}, - {"user_id": 3, "wallet": "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"}, - ], - "challenges": [ - { - "id": "not-exhausted", - "type": ChallengeType.numeric, - "active": True, - "amount": 5, - "weekly_pool": 10, - }, - { - "id": "exhausted", - "type": ChallengeType.numeric, - "active": True, - "amount": 10, - "weekly_pool": 20, - }, - ], - "user_challenges": [ - { - "challenge_id": "not-exhausted", - "user_id": 1, - "specifier": "1", - "is_complete": True, - "amount": 5, - }, - { - "challenge_id": "not-exhausted", - "user_id": 2, - "specifier": "2", - "is_complete": True, - "amount": 5, - }, - { - "challenge_id": "exhausted", - "user_id": 1, - "specifier": "1", - "is_complete": True, - "amount": 10, - }, - { - "challenge_id": "exhausted", - "user_id": 2, - "specifier": "2", - "is_complete": True, - "amount": 10, - }, - # User 3 has completed challenge `exhausted` but will not be able to disburse - { - "challenge_id": "exhausted", - "user_id": 3, - "specifier": "3", - "is_complete": True, - "amount": 10, - }, - ], - "challenge_disbursements": [ - { - "challenge_id": "not-exhausted", - "specifier": "1", - "user_id": 1, - "amount": "500000000", - }, - { - "challenge_id": "exhausted", - "specifier": "1", - "user_id": 1, - "amount": "1000000000", - }, - { - "challenge_id": "exhausted", - "specifier": "2", - "user_id": 2, - "amount": "1000000000", - }, - ], - } - populate_mock_db(db, entities) - with db.scoped_session() as session: - oracle_address = DUMMY_ORACLE_ADDRESS - redis_handle.set(oracle_addresses_key, oracle_address) - - delegate_owner_wallet, signature = get_attestation( - session, - user_id=2, - challenge_id="not-exhausted", - oracle_address=oracle_address, - specifier="2", - ) - - attestation = Attestation( - amount="5", - oracle_address=oracle_address, - user_address=entities["users"][1]["wallet"], - challenge_id="not-exhausted", - challenge_specifier="2", - ) - - # User 2 should still be able to disburse the non-exhausted challenge - - # confirm the attestation is what we think it should be - config_owner_wallet = shared_config["delegate"]["owner_wallet"] - config_private_key = shared_config["delegate"]["private_key"] - - # Ensure we returned the correct owner wallet - assert delegate_owner_wallet == config_owner_wallet - - # Ensure we can derive the owner wallet from the signed stringified attestation - attestation_bytes = attestation.get_attestation_bytes() - to_sign_hash = Web3.keccak(attestation_bytes) - private_key = keys.PrivateKey(HexBytes(config_private_key)) - public_key = keys.PublicKey.from_private(private_key) - signture_bytes = to_bytes(hexstr=signature) - msg_signature = keys.Signature(signature_bytes=signture_bytes, vrs=None) - - recovered_pubkey = public_key.recover_from_msg_hash( - message_hash=to_sign_hash, signature=msg_signature - ) - - assert ( - Web3.to_checksum_address(recovered_pubkey.to_address()) - == config_owner_wallet - ) - - with pytest.raises(AttestationError) as e: - get_attestation( - session, - user_id=3, - challenge_id="exhausted", - oracle_address=oracle_address, - specifier="3", - ) - assert str(e.value) == POOL_EXHAUSTED - - -@pytest.fixture -def patch_get_all_nodes(): - with patch( - "src.queries.get_attestation.get_all_discovery_nodes_cached", - return_value=[ - {"delegateOwnerWallet": "0x94e140D27F3d5EE9EcA0109A71CcBa0109964DCa"} - ], - ): - yield - - -def test_get_create_sender_attestation(app, patch_get_all_nodes): - new_sender_address = "0x94e140D27F3d5EE9EcA0109A71CcBa0109964DCa" - owner_wallet, sender_attestation = get_create_sender_attestation(new_sender_address) - - # confirm the attestation is what we think it should be - config_owner_wallet = shared_config["delegate"]["owner_wallet"] - config_private_key = shared_config["delegate"]["private_key"] - - # Ensure we returned the correct owner wallet - assert owner_wallet == config_owner_wallet - - # Ensure we can derive the owner wallet from the signed stringified attestation - items = [ - to_bytes(text=ADD_SENDER_MESSAGE_PREFIX), - bytes(Pubkey.from_string(REWARDS_MANAGER_ACCOUNT)), - to_bytes(hexstr=new_sender_address), - ] - attestation_bytes = to_bytes(text="").join(items) - to_sign_hash = Web3.keccak(attestation_bytes) - private_key = keys.PrivateKey(HexBytes(config_private_key)) - public_key = keys.PublicKey.from_private(private_key) - signture_bytes = to_bytes(hexstr=sender_attestation) - msg_signature = keys.Signature(signature_bytes=signture_bytes, vrs=None) - - recovered_pubkey = public_key.recover_from_msg_hash( - message_hash=to_sign_hash, signature=msg_signature - ) - - assert ( - Web3.to_checksum_address(recovered_pubkey.to_address()) == config_owner_wallet - ) - - -def test_get_create_sender_attestation_not_registered(app, patch_get_all_nodes): - new_sender_address = "0x04e140D27F3d5EE9EcA0109A71CcBa0109964DCa" - with pytest.raises( - Exception, - match=r"Expected 0x04e140D27F3d5EE9EcA0109A71CcBa0109964DCa to be registered on chain", - ): - get_create_sender_attestation(new_sender_address) diff --git a/packages/discovery-provider/integration_tests/queries/test_get_challenges.py b/packages/discovery-provider/integration_tests/queries/test_get_challenges.py deleted file mode 100644 index 6b7662fed93..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_challenges.py +++ /dev/null @@ -1,886 +0,0 @@ -from datetime import datetime, timedelta -from typing import List, Optional - -from sqlalchemy.orm import Session - -from src.challenges.challenge import ( - ChallengeManager, - ChallengeUpdater, - FullEventMetadata, -) -from src.challenges.challenge_event_bus import ChallengeEventBus -from src.challenges.listen_streak_endless_challenge import ( - listen_streak_endless_challenge_manager, -) -from src.challenges.referral_challenge import ( - referral_challenge_manager, - verified_referral_challenge_manager, -) -from src.models.indexing.block import Block -from src.models.rewards.challenge import Challenge, ChallengeType -from src.models.rewards.challenge_disbursement import ChallengeDisbursement -from src.models.rewards.listen_streak_challenge import ChallengeListenStreak -from src.models.rewards.user_challenge import UserChallenge -from src.models.users.user import User -from src.queries.get_challenges import get_challenges -from src.utils.config import shared_config -from src.utils.db_session import get_db -from src.utils.redis_connection import get_redis - -REDIS_URL = shared_config["redis"]["url"] -DEFAULT_EVENT = "" -AGGREGATE_CHALLENGES_AMOUNT = 5 -AGGREGATE_CHALLENGE_1_STEP_COUNT = 3 -AGGREGATE_CHALLENGE_2_3_STEP_COUNT = 2 - - -class DefaultUpdater(ChallengeUpdater): - def update_user_challenges( - self, - session: Session, - event: str, - user_challenges: List[UserChallenge], - step_count: Optional[int], - event_metadatas: List[FullEventMetadata], - starting_block: Optional[int], - ): - for challenge in user_challenges: - if not challenge.current_step_count: - challenge.current_step_count = 0 - challenge.current_step_count += 1 - if challenge.current_step_count == step_count: - challenge.is_complete = True - - -class NumericCustomUpdater(ChallengeUpdater): - def get_default_metadata(self): - return {"default_state": True} - - def get_metadata(self, session: Session, specifiers: List[str]): - return [{"special_metadata": s} for s in specifiers] - - -def setup_db(session): - blocks = [Block(blockhash="0x1", number=1, parenthash="", is_current=True)] - users = [ - User( - blockhash="0x1", - blocknumber=1, - user_id=1, - is_current=True, - wallet="0x38C68fF3926bf4E68289672F75ee1543117dD9B3", - created_at=datetime.now(), - updated_at=datetime.now(), - ) - ] - challenges = [ - Challenge( - id="boolean_challenge_1", - type=ChallengeType.boolean, - active=True, - amount="5", - ), - Challenge( - id="boolean_challenge_2", - type=ChallengeType.boolean, - active=True, - amount="5", - ), - Challenge( - id="boolean_challenge_3", - type=ChallengeType.boolean, - active=True, - amount="5", - ), - # No progress on this, but active - # should be returned - Challenge( - id="boolean_challenge_4", - type=ChallengeType.boolean, - active=True, - amount="5", - ), - # Inactive, with no progress - Challenge( - id="boolean_challenge_5", - type=ChallengeType.boolean, - active=False, - amount="5", - ), - # Inactive, WITH progress - Challenge( - id="boolean_challenge_6", - type=ChallengeType.boolean, - active=False, - amount="5", - ), - Challenge( - id="trending_challenge_1", - type=ChallengeType.trending, - active=True, - amount="5", - ), - Challenge( - id="aggregate_challenge_1", - type=ChallengeType.aggregate, - active=True, - amount="5", - step_count=AGGREGATE_CHALLENGE_1_STEP_COUNT * AGGREGATE_CHALLENGES_AMOUNT, - ), - Challenge( - id="aggregate_challenge_2", - type=ChallengeType.aggregate, - active=True, - amount="5", - step_count=AGGREGATE_CHALLENGE_2_3_STEP_COUNT * AGGREGATE_CHALLENGES_AMOUNT, - ), - Challenge( - id="aggregate_challenge_3", - type=ChallengeType.aggregate, - active=True, - amount="5", - step_count=AGGREGATE_CHALLENGE_2_3_STEP_COUNT * AGGREGATE_CHALLENGES_AMOUNT, - ), - Challenge( - id="trending_1", type=ChallengeType.trending, active=True, amount="5" - ), - Challenge( - id="trending_2", type=ChallengeType.trending, active=True, amount="5" - ), - Challenge( - id="trending_3", type=ChallengeType.trending, active=True, amount="5" - ), - ] - user_challenges = [ - # Finished the first challenge, disbursed - UserChallenge( - challenge_id="boolean_challenge_1", - user_id=1, - specifier="1", - is_complete=True, - amount=5, - completed_at=datetime.now(), - ), - # Did finish the second challenge, did not disburse - UserChallenge( - challenge_id="boolean_challenge_2", - user_id=1, - specifier="1", - is_complete=True, - amount=5, - completed_at=datetime.now(), - ), - # Did not finish challenge 3 - UserChallenge( - challenge_id="boolean_challenge_3", - user_id=1, - specifier="1", - is_complete=False, - amount=5, - completed_at=datetime.now(), - ), - # Inactive challenge - UserChallenge( - challenge_id="boolean_challenge_6", - user_id=1, - specifier="1", - is_complete=True, - amount=5, - completed_at=datetime.now(), - ), - UserChallenge( - challenge_id="aggregate_challenge_1", - user_id=1, - specifier="1-2", # compound specifiers, like if user1 invites user2 - is_complete=True, - amount=AGGREGATE_CHALLENGES_AMOUNT, - completed_at=datetime.now(), - ), - # Ensure that a non-complete user-challenge isn't counted towards - # aggregate challenge score - UserChallenge( - challenge_id="aggregate_challenge_1", - user_id=1, - specifier="1-3", - is_complete=False, - amount=AGGREGATE_CHALLENGES_AMOUNT, - completed_at=datetime.now(), - ), - UserChallenge( - challenge_id="aggregate_challenge_2", - user_id=1, - specifier="1-2", - is_complete=True, - amount=AGGREGATE_CHALLENGES_AMOUNT, - completed_at=datetime.now(), - ), - UserChallenge( - challenge_id="aggregate_challenge_2", - user_id=1, - specifier="1-3", - is_complete=True, - amount=AGGREGATE_CHALLENGES_AMOUNT, - completed_at=datetime.now(), - ), - # Trending 1 should be finished and included - UserChallenge( - challenge_id="trending_1", - user_id=1, - specifier="06-01-2020", - is_complete=True, - amount=5, - completed_at=datetime.now(), - ), - # Trending 2 should not be included - UserChallenge( - challenge_id="trending_2", - user_id=1, - specifier="06-01-2020", - is_complete=False, - amount=5, - completed_at=datetime.now(), - ), - ] - disbursements = [ - ChallengeDisbursement( - challenge_id="boolean_challenge_1", - user_id=1, - amount="5", - signature="1", - slot=1, - specifier="1", - ) - ] - - # Wipe any existing challenges in the DB from running migrations, etc - session.query(Challenge).delete() - session.commit() - session.add_all(blocks) - session.commit() - session.add_all(users) - session.commit() - session.add_all(challenges) - session.commit() - session.add_all(user_challenges) - session.commit() - session.add_all(disbursements) - - redis_conn = get_redis() - bus = ChallengeEventBus(redis_conn) - challenge_types = [ - "boolean_challenge_1", - "boolean_challenge_2", - "boolean_challenge_3", - "boolean_challenge_4", - "boolean_challenge_5", - "boolean_challenge_6", - "trending_challenge_1", - "aggregate_challenge_1", - "aggregate_challenge_2", - "aggregate_challenge_3", - "trending_1", - "trending_2", - "trending_3", - ] - for ct in challenge_types: - bus.register_listener( - DEFAULT_EVENT, - ChallengeManager(ct, DefaultUpdater()), - ) - return bus - - -def test_get_challenges(app): - with app.app_context(): - db = get_db() - with db.scoped_session() as session: - bus = setup_db(session) - - # Setup registry - - # Try to get the challenges, not historical - res = get_challenges(1, False, session, bus) - - # We don't care about the order of the challenges returned - # so make a map - res_map = {challenge["challenge_id"]: challenge for challenge in res} - # Ensure correct num of challenges - # not returned are: - # - inactive, with no user_challenge - # - inactive, with user_challenge - # - trending, with no user_challenge - # - trending, with one unfinished user challenge (doesn't even make sense, but it could happen) - assert len(res) == 8 - - # Base case - an active, completed, disbursed chal - chal_1 = res_map["boolean_challenge_1"] - assert chal_1["is_disbursed"] - assert chal_1["is_active"] - assert chal_1["is_complete"] - - # Active, complete, non disbursed - chal_2 = res_map["boolean_challenge_2"] - assert chal_2["is_disbursed"] == False - assert chal_2["is_active"] - assert chal_2["is_complete"] - - # active, incomplete, nondisbursed - chal_3 = res_map["boolean_challenge_3"] - assert chal_3["is_disbursed"] == False - assert chal_3["is_active"] - assert chal_3["is_complete"] == False - - # no user progress, but active, so should be returned - chal_4 = res_map["boolean_challenge_4"] - assert chal_4["is_disbursed"] == False - assert chal_4["is_active"] - assert chal_4["is_complete"] == False - - # aggregate challenge with one completion, so not fully complete - chal_agg_1 = res_map["aggregate_challenge_1"] - assert ( - chal_agg_1["is_disbursed"] == False - ) # This field doesn't matter since we can't roll up disbursions - assert chal_agg_1["is_active"] - assert chal_agg_1["is_complete"] == False - assert chal_agg_1["current_step_count"] == 5 - assert ( - chal_agg_1["max_steps"] - == AGGREGATE_CHALLENGE_1_STEP_COUNT * AGGREGATE_CHALLENGES_AMOUNT - ) - - # aggregate challenge with 2 completions, FULLY complete - chal_agg_2 = res_map["aggregate_challenge_2"] - assert ( - chal_agg_2["is_disbursed"] == False - ) # This field doesn't matter since we can't roll up disbursions - assert chal_agg_2["is_active"] - assert chal_agg_2["is_complete"] == True - assert chal_agg_2["current_step_count"] == 10 - assert ( - chal_agg_2["max_steps"] - == AGGREGATE_CHALLENGE_2_3_STEP_COUNT * AGGREGATE_CHALLENGES_AMOUNT - ) - - # aggregate challenge with no completions - chal_agg_3 = res_map["aggregate_challenge_3"] - assert ( - chal_agg_3["is_disbursed"] == False - ) # This field doesn't matter since we can't roll up disbursions - assert chal_agg_3["is_active"] - assert chal_agg_3["is_complete"] == False - assert chal_agg_3["current_step_count"] == 0 - assert ( - chal_agg_3["max_steps"] - == AGGREGATE_CHALLENGE_2_3_STEP_COUNT * AGGREGATE_CHALLENGES_AMOUNT - ) - - # complete trending challenge - chal_trend_1 = res_map["trending_1"] - assert chal_trend_1["is_disbursed"] == False - assert chal_trend_1["is_active"] - assert chal_trend_1["is_complete"] - - # Try to get the challenges, this time historical - res = get_challenges(1, True, session, bus) - - # The only difference is that we should have shown - # inactive but complete challenges - assert len(res) == 9 - res_map = {challenge["challenge_id"]: challenge for challenge in res} - historical = res_map["boolean_challenge_6"] - assert historical["is_active"] == False - assert historical["is_complete"] - assert historical["is_disbursed"] == False - - -def setup_extra_metadata_test(session): - blocks = [Block(blockhash="0x1", number=1, parenthash="", is_current=True)] - users = [ - User( - blockhash="0x1", - blocknumber=1, - user_id=1, - is_current=True, - wallet="0x38C68fF3926bf4E68289672F75ee1543117dD9B3", - created_at=datetime.now(), - updated_at=datetime.now(), - ) - ] - challenges = [ - # Test numeric challenges - # Numeric 1 with default extra data, no completion - Challenge( - id="numeric_1", - type=ChallengeType.numeric, - active=True, - amount="5", - step_count=5, - ), - # Numeric 2 with some extra data - Challenge( - id="numeric_2", - type=ChallengeType.numeric, - active=True, - amount="5", - step_count=5, - ), - ] - - user_challenges = [ - UserChallenge( - challenge_id="numeric_2", - user_id=1, - specifier="1", - is_complete=False, - current_step_count=5, - amount=5, - completed_at=datetime.now(), - ), - ] - - session.query(Challenge).delete() - session.commit() - session.add_all(blocks) - session.commit() - session.add_all(users) - session.commit() - session.add_all(challenges) - session.commit() - session.add_all(user_challenges) - session.commit() - - redis_conn = get_redis() - bus = ChallengeEventBus(redis_conn) - bus.register_listener( - DEFAULT_EVENT, ChallengeManager("numeric_1", NumericCustomUpdater()) - ) - bus.register_listener( - DEFAULT_EVENT, ChallengeManager("numeric_2", NumericCustomUpdater()) - ) - return bus - - -def test_extra_metadata(app): - with app.app_context(): - db = get_db() - with db.scoped_session() as session: - bus = setup_extra_metadata_test(session) - - res = get_challenges(1, False, session, bus) - challenge_1 = [r for r in res if r["challenge_id"] == "numeric_1"][0] - challenge_2 = [r for r in res if r["challenge_id"] == "numeric_2"][0] - assert challenge_1["metadata"] == {"default_state": True} - assert challenge_2["metadata"] == {"special_metadata": "1"} - - -# Testing getting verified/unverified referral challenges - - -def setup_verified_test(session): - # Setup - blocks = [ - Block(blockhash="0x1", number=1, parenthash="", is_current=False), - Block(blockhash="0x2", number=2, parenthash="", is_current=True), - ] - users = [ - User( - blockhash="0x1", - blocknumber=1, - user_id=1, - is_current=True, - wallet="0xFakeWallet1", - created_at=datetime.now(), - updated_at=datetime.now(), - is_verified=False, - ), - User( - blockhash="0x2", - blocknumber=2, - user_id=2, - is_current=True, - wallet="0xFakeWallet2", - created_at=datetime.now(), - updated_at=datetime.now(), - is_verified=True, - ), - ] - - challenges = [ - Challenge( - id="r", - type=ChallengeType.aggregate, - active=True, - amount="1", - step_count=5, - ), - Challenge( - id="rv", - type=ChallengeType.aggregate, - active=True, - amount="1", - step_count=500, - ), - ] - - # Wipe any existing challenges in the DB from running migrations, etc - session.query(Challenge).delete() - session.commit() - session.add_all(blocks) - session.commit() - session.add_all(users) - session.add_all(challenges) - session.commit() - - redis_conn = get_redis() - bus = ChallengeEventBus(redis_conn) - bus.register_listener(DEFAULT_EVENT, referral_challenge_manager) - bus.register_listener(DEFAULT_EVENT, verified_referral_challenge_manager) - return bus - - -def test_nonverified_referrals_invisible_to_verified_user(app): - with app.app_context(): - db = get_db() - with db.scoped_session() as session: - bus = setup_verified_test(session) - - non_verified = get_challenges(1, False, session, bus) - assert len(non_verified) == 1 - assert non_verified[0]["challenge_id"] == "r" - - -def test_verified_referrals_invisible_to_nonverified_user(app): - with app.app_context(): - db = get_db() - with db.scoped_session() as session: - bus = setup_verified_test(session) - - verified = get_challenges(2, False, session, bus) - assert len(verified) == 1 - assert verified[0]["challenge_id"] == "rv" - - -# Testing getting listen streak challenges with override - - -def setup_listen_streak_challenge(session): - # Setup - blocks = [ - Block(blockhash="0x1", number=1, parenthash="", is_current=False), - Block(blockhash="0x2", number=2, parenthash="", is_current=False), - Block(blockhash="0x3", number=3, parenthash="", is_current=False), - Block(blockhash="0x4", number=4, parenthash="", is_current=False), - Block(blockhash="0x5", number=5, parenthash="", is_current=False), - Block(blockhash="0x6", number=6, parenthash="", is_current=True), - ] - users = [ - User( - blockhash="0x1", - blocknumber=1, - user_id=1, - is_current=True, - wallet="0xFakeWallet1", - created_at=datetime.now(), - updated_at=datetime.now(), - is_verified=False, - ), - User( - blockhash="0x2", - blocknumber=2, - user_id=2, - is_current=True, - wallet="0xFakeWallet2", - created_at=datetime.now(), - updated_at=datetime.now(), - is_verified=True, - ), - User( - blockhash="0x3", - blocknumber=3, - user_id=3, - is_current=True, - wallet="0xFakeWallet3", - created_at=datetime.now(), - updated_at=datetime.now(), - is_verified=False, - ), - User( - blockhash="0x4", - blocknumber=4, - user_id=4, - is_current=True, - wallet="0xFakeWallet4", - created_at=datetime.now(), - updated_at=datetime.now(), - is_verified=False, - ), - User( - blockhash="0x5", - blocknumber=5, - user_id=5, - is_current=True, - wallet="0xFakeWallet5", - created_at=datetime.now(), - updated_at=datetime.now(), - is_verified=False, - ), - User( - blockhash="0x6", - blocknumber=6, - user_id=6, - is_current=True, - wallet="0xFakeWallet6", - created_at=datetime.now(), - updated_at=datetime.now(), - is_verified=False, - ), - ] - - challenges = [ - Challenge( - id="e", - type=ChallengeType.aggregate, - active=True, - amount="1", - step_count=2147483647, - ) - ] - - user_challenges = [ - UserChallenge( - challenge_id="e", - user_id=1, - specifier="1", - is_complete=False, - current_step_count=5, - amount=1, - created_at=datetime.now(), - ), - UserChallenge( - challenge_id="e", - user_id=2, - specifier="2", - is_complete=False, - current_step_count=5, - amount=1, - created_at=datetime.now(), - ), - # User 3 has just started a new streak for the first time - UserChallenge( - challenge_id="e", - user_id=3, - specifier="3x0", - is_complete=False, - current_step_count=3, - amount=7, - created_at=datetime.now(), - ), - # User 4 has an ongoing listen streak of 9 - UserChallenge( - challenge_id="e", - user_id=4, - specifier="4x0", - is_complete=True, - current_step_count=7, - amount=7, - completed_at=datetime.now() - timedelta(days=2), - created_at=datetime.now() - timedelta(days=9), - ), - UserChallenge( - challenge_id="e", - user_id=4, - specifier="4x1", - is_complete=True, - current_step_count=1, - amount=1, - completed_at=datetime.now() - timedelta(days=1), - created_at=datetime.now() - timedelta(days=1), - ), - UserChallenge( - challenge_id="e", - user_id=4, - specifier="4x2", - is_complete=True, - current_step_count=1, - amount=1, - completed_at=datetime.now(), - created_at=datetime.now(), - ), - # User 5 has one complete listen streak of 8, and an in-progress streak of 3 - UserChallenge( - challenge_id="e", - user_id=5, - specifier="5x0", - is_complete=True, - current_step_count=7, - amount=7, - completed_at=datetime.now() - timedelta(days=30), - created_at=datetime.now() - timedelta(days=37), - ), - UserChallenge( - challenge_id="e", - user_id=5, - specifier="5x1", - is_complete=True, - current_step_count=1, - amount=1, - completed_at=datetime.now() - timedelta(days=29), - created_at=datetime.now() - timedelta(days=29), - ), - UserChallenge( - challenge_id="e", - user_id=5, - specifier="5x2", - is_complete=False, - current_step_count=3, - amount=3, - created_at=datetime.now(), - ), - # User 6 has one complete streak of 8, and a broken streak of 2 - UserChallenge( - challenge_id="e", - user_id=6, - specifier="6x0", - is_complete=True, - current_step_count=7, - amount=7, - completed_at=datetime.now() - timedelta(days=30), - created_at=datetime.now() - timedelta(days=37), - ), - UserChallenge( - challenge_id="e", - user_id=6, - specifier="6x1", - is_complete=True, - current_step_count=1, - amount=1, - completed_at=datetime.now() - timedelta(days=29), - created_at=datetime.now() - timedelta(days=29), - ), - UserChallenge( - challenge_id="e", - user_id=6, - specifier="6x2", - is_complete=False, - current_step_count=2, - amount=7, - created_at=datetime.now() - timedelta(days=4), - ), - ] - - listen_streak_challenges = [ - # User 1 has a completed listen streak of 5 - ChallengeListenStreak( - user_id=1, - last_listen_date=datetime.now() - timedelta(hours=12), - listen_streak=5, - ), - # User 2 has a broken listen streak of 5 - override should take effect - ChallengeListenStreak( - user_id=2, - last_listen_date=datetime.now() - timedelta(hours=50), - listen_streak=5, - ), - # User 3 has a new listen streak of 3 - ChallengeListenStreak( - user_id=3, - last_listen_date=datetime.now() - timedelta(hours=12), - listen_streak=3, - ), - # User 4 has an ongoing listen streak of 9 - ChallengeListenStreak( - user_id=4, - last_listen_date=datetime.now() - timedelta(hours=12), - listen_streak=9, - ), - # User 5 has an ongoing listen streak of 3 - ChallengeListenStreak( - user_id=5, - last_listen_date=datetime.now() - timedelta(hours=12), - listen_streak=3, - ), - # User 6 has a broken listen streak of 3 - override should take effect - ChallengeListenStreak( - user_id=6, - last_listen_date=datetime.now() - timedelta(days=3), - listen_streak=2, - ), - ] - - # Wipe any existing challenges in the DB from running migrations, etc - session.query(Challenge).delete() - session.commit() - session.add_all(blocks) - session.commit() - session.add_all(users) - session.add_all(challenges) - session.commit() - session.add_all(user_challenges) - session.commit() - session.add_all(listen_streak_challenges) - session.commit() - - redis_conn = get_redis() - bus = ChallengeEventBus(redis_conn) - bus.register_listener(DEFAULT_EVENT, listen_streak_endless_challenge_manager) - return bus - - -def test_get_challenges_with_no_override_step_count(app): - with app.app_context(): - db = get_db() - with db.scoped_session() as session: - bus = setup_listen_streak_challenge(session) - - challenges = get_challenges(1, False, session, bus) - assert len(challenges) == 1 - assert challenges[0]["challenge_id"] == "e" - assert challenges[0]["current_step_count"] == 5 - - -def test_get_challenges_with_override_step_count(app): - with app.app_context(): - db = get_db() - with db.scoped_session() as session: - bus = setup_listen_streak_challenge(session) - - challenges = get_challenges(2, False, session, bus) - assert len(challenges) == 1 - assert challenges[0]["challenge_id"] == "e" - assert challenges[0]["current_step_count"] == 0 - - -def test_listen_streak_endless_challenge(app): - with app.app_context(): - db = get_db() - with db.scoped_session() as session: - bus = setup_listen_streak_challenge(session) - - challenges = get_challenges(3, False, session, bus) - assert len(challenges) == 1 - assert challenges[0]["challenge_id"] == "e" - assert challenges[0]["is_complete"] == False - assert challenges[0]["amount"] == "1" - assert challenges[0]["current_step_count"] == 3 - - challenges = get_challenges(4, False, session, bus) - assert len(challenges) == 1 - assert challenges[0]["challenge_id"] == "e" - assert challenges[0]["is_complete"] == True - assert challenges[0]["amount"] == "1" - assert challenges[0]["current_step_count"] == 9 - - challenges = get_challenges(5, False, session, bus) - assert len(challenges) == 1 - assert challenges[0]["challenge_id"] == "e" - assert challenges[0]["is_complete"] == True - assert challenges[0]["current_step_count"] == 3 - assert challenges[0]["amount"] == "1" - - challenges = get_challenges(6, False, session, bus) - assert len(challenges) == 1 - assert challenges[0]["challenge_id"] == "e" - assert challenges[0]["is_complete"] == True - assert challenges[0]["current_step_count"] == 0 - assert challenges[0]["amount"] == "1" diff --git a/packages/discovery-provider/integration_tests/queries/test_get_collections_library.py b/packages/discovery-provider/integration_tests/queries/test_get_collections_library.py deleted file mode 100644 index e3947a0201c..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_collections_library.py +++ /dev/null @@ -1,323 +0,0 @@ -from datetime import datetime - -from integration_tests.utils import populate_mock_db -from src.queries.get_collection_library import ( - CollectionType, - GetCollectionLibraryArgs, - LibraryFilterType, - _get_collection_library, -) -from src.queries.get_track_library import SortDirection, SortMethod -from src.utils.db_session import get_db - - -def populate_entries(db): - test_entities = { - "tracks": [ - { - "track_id": 1, - "title": "track 1", - "owner_id": 1, - "release_date": datetime(2019, 6, 17), - "created_at": datetime(2019, 6, 17), - }, - { - "track_id": 2, - "title": "track 2", - "owner_id": 2, - "release_date": datetime(2019, 6, 15), - "created_at": datetime(2019, 6, 15), - }, - { - "track_id": 3, - "title": "track 3", - "owner_id": 2, - "release_date": datetime(2019, 6, 15), - "created_at": datetime(2019, 6, 15), - }, - { - "track_id": 4, - "title": "track 4", - "owner_id": 1, - "is_unlisted": True, - "release_date": datetime(2019, 6, 15), - "created_at": datetime(2019, 6, 15), - }, - { - "track_id": 5, - "title": "track 5", - "owner_id": 2, - "is_unlisted": True, - "release_date": datetime(2019, 6, 15), - "created_at": datetime(2019, 6, 15), - }, - ], - "playlists": [ - # playlists ----- - { - "playlist_id": 1, - "playlist_owner_id": 1, - "is_album": False, - "created_at": datetime(2019, 6, 17), - "playlist_contents": {"track_ids": [{"track": 1}]}, - }, - # user 1 hidden playlist - { - "playlist_id": 5, - "playlist_owner_id": 1, - "is_album": False, - "is_private": True, - "created_at": datetime(2019, 6, 18), - "playlist_name": "asdf", - "playlist_contents": {"track_ids": [{"track": 1}, {"track": 2}]}, - }, - # user 1 playlist of all hidden tracks - { - "playlist_id": 6, - "playlist_owner_id": 1, - "is_album": False, - "created_at": datetime(2019, 6, 19), - "playlist_name": "asdf", - "playlist_contents": {"track_ids": [{"track": 4}]}, - }, - # user 2 hidden playlist - { - "playlist_id": 7, - "playlist_owner_id": 2, - "is_album": False, - "is_private": True, - "created_at": datetime(2019, 6, 17), - "playlist_name": "asdf", - "playlist_contents": {"track_ids": [{"track": 1}, {"track": 2}]}, - }, - # user 2 playlist of all hidden tracks - { - "playlist_id": 8, - "playlist_owner_id": 2, - "is_album": False, - "created_at": datetime(2019, 6, 18), - "playlist_name": "asdf", - "playlist_contents": {"track_ids": [{"track": 5}]}, - }, - # reposted - { - "playlist_id": 3, - "playlist_owner_id": 2, - "is_album": False, - "created_at": datetime(2019, 6, 15), - "playlist_contents": {"track_ids": [{"track": 3}]}, - }, - # albums ------- - { - "playlist_id": 2, - "playlist_owner_id": 1, - "is_album": True, - "created_at": datetime(2019, 6, 17), - "playlist_name": "asdf", - "playlist_contents": {"track_ids": [{"track": 1}]}, - }, - # saved - { - "playlist_id": 4, - "playlist_owner_id": 2, - "is_album": True, - "created_at": datetime(2019, 6, 15), - "playlist_name": "xyz", - "playlist_contents": {"track_ids": [{"track": 2}]}, - }, - ], - "users": [ - {"user_id": 1}, - {"user_id": 2, "name": "testhandle123"}, - {"user_id": 3}, - {"user_id": 4}, - ], - } - test_social_feature_entities = { - "reposts": [ - { - "repost_item_id": 3, - "repost_type": "playlist", - "user_id": 1, - "created_at": datetime(2019, 6, 16), - } - ], - "saves": [ - { - "save_item_id": 4, - "save_type": "album", - "user_id": 1, - "created_at": datetime(2019, 6, 16), - }, - # saved hidden playlist - { - "save_item_id": 7, - "save_type": "playlist", - "user_id": 1, - "created_at": datetime(2019, 6, 17), - }, - # saved playlist of all hidden tracks - { - "save_item_id": 8, - "save_type": "playlist", - "user_id": 1, - "created_at": datetime(2019, 6, 18), - }, - ], - } - populate_mock_db(db, test_entities) - populate_mock_db(db, test_social_feature_entities) - - -def with_collection_library_setup(test_fn): - def wrapper(app): - with app.app_context(): - db = get_db() - populate_entries(db) - with db.scoped_session() as session: - test_fn(session) - - return wrapper - - -def assert_correct_collection(collections, index, playlist_id, note): - assert collections[index]["playlist_id"] == playlist_id, note - - -@with_collection_library_setup -def test_distinguishes_albums_vs_favorites(session): - args = GetCollectionLibraryArgs( - user_id=1, - limit=10, - offset=0, - filter_type=LibraryFilterType.all, - collection_type=CollectionType.album, - ) - - # Default to descending sort on item_created_at - res = _get_collection_library(args, session) - assert len(res) == 2 - assert_correct_collection(res, 0, 2, "should get only albums") - assert_correct_collection(res, 1, 4, "should get only albums") - - args["collection_type"] = CollectionType.playlist - res = _get_collection_library(args, session) - assert len(res) == 4 - assert_correct_collection( - res, - 0, - 6, - "should return playlists of all hidden tracks belonging to current user", - ) - assert_correct_collection( - res, 1, 5, "should return hidden playlists belonging to user" - ) - assert_correct_collection(res, 2, 1, "should get only playlists") - assert_correct_collection(res, 3, 3, "should get only playlists") - - -@with_collection_library_setup -def test_filter_methods(session): - args = GetCollectionLibraryArgs( - user_id=1, - limit=10, - offset=0, - filter_type=LibraryFilterType.favorite, - collection_type=CollectionType.album, - ) - - # Default to descending sort on item_created_at - # Favorite should also return 'own' items - res = _get_collection_library(args, session) - assert len(res) == 2 - assert_correct_collection(res, 0, 2, "should get only saved albums") - assert_correct_collection(res, 1, 4, "should get only saved albums") - - args["filter_type"] = LibraryFilterType.repost - args["collection_type"] = CollectionType.playlist - - res = _get_collection_library(args, session) - assert len(res) == 1 - assert_correct_collection(res, 0, 3, "should get only reposted albums") - - -# Distinguishes albums and playlists -@with_collection_library_setup -def test_sort_orders(session): - # Test by sort method: added_date, reposts, saves - - # Test added date - args = GetCollectionLibraryArgs( - user_id=1, - limit=10, - offset=0, - filter_type=LibraryFilterType.all, - collection_type=CollectionType.album, - sort_direction=SortDirection.desc, - sort_method=SortMethod.added_date, - ) - - res = _get_collection_library(args, session) - assert len(res) == 2 - assert_correct_collection(res, 0, 2, "sort by desc item_created_at") - assert_correct_collection(res, 1, 4, "sort by desc item_created_at") - - # Test sort by fav count - args["sort_method"] = SortMethod.saves - res = _get_collection_library(args, session) - assert len(res) == 2 - assert_correct_collection(res, 0, 4, "sort by desc save") - assert_correct_collection(res, 1, 2, "sort by desc save") - - # Test sort by repost count - args["sort_method"] = SortMethod.reposts - args["collection_type"] = CollectionType.playlist - res = _get_collection_library(args, session) - assert len(res) == 4 - assert_correct_collection(res, 0, 3, "sort by desc repost") - assert_correct_collection(res, 1, 1, "sort by desc repost") - - -@with_collection_library_setup -def test_query(session): - # query by self playlist tile - args = GetCollectionLibraryArgs( - user_id=1, - limit=10, - offset=0, - filter_type=LibraryFilterType.all, - collection_type=CollectionType.album, - sort_direction=SortDirection.desc, - sort_method=SortMethod.added_date, - query="asdf", - ) - - res = _get_collection_library(args, session) - assert len(res) == 1 - assert_correct_collection(res, 0, 2, "matches track title query for self playlist") - - # By saved playlist title - args = GetCollectionLibraryArgs( - user_id=1, - limit=10, - offset=0, - filter_type=LibraryFilterType.all, - collection_type=CollectionType.album, - sort_direction=SortDirection.desc, - sort_method=SortMethod.added_date, - query="xyz", - ) - - res = _get_collection_library(args, session) - assert len(res) == 1 - assert_correct_collection( - res, 0, 4, "matches track title query for saved playlist " - ) - - # query by user handle - args["sort_method"] = SortMethod.reposts - args["collection_type"] = CollectionType.playlist - args["query"] = "testhandle123" - res = _get_collection_library(args, session) - assert len(res) == 1 - assert_correct_collection(res, 0, 3, "matches user handle") diff --git a/packages/discovery-provider/integration_tests/queries/test_get_genre_metrics.py b/packages/discovery-provider/integration_tests/queries/test_get_genre_metrics.py deleted file mode 100644 index a703048e8a0..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_genre_metrics.py +++ /dev/null @@ -1,99 +0,0 @@ -from datetime import datetime, timedelta - -from src.models.indexing.block import Block -from src.models.tracks.track import Track -from src.queries.get_genre_metrics import _get_genre_metrics -from src.utils.db_session import get_db - - -def populate_mock_db(db, test_tracks, date): - """Helper function to populate thee mock DB with plays""" - with db.scoped_session() as session: - for i, track_meta in enumerate(test_tracks): - blockhash = hex(i) - block = Block( - blockhash=blockhash, - number=i, - parenthash="0x01", - is_current=(i == 0), - ) - track = Track( - blockhash=hex(i), - blocknumber=i, - track_id=i, - is_current=track_meta.get("is_current", True), - is_delete=track_meta.get("is_delete", False), - owner_id=300, - route_id="", - track_segments=[], - genre=track_meta.get("genre", ""), - updated_at=track_meta.get("updated_at", date), - created_at=track_meta.get("created_at", date), - is_unlisted=track_meta.get("is_unlisted", False), - ) - # add block and then flush before - # adding track, bc track.blocknumber foreign key - # references block - session.add(block) - session.flush() - session.add(track) - - -def test_get_genre_metrics(app): - """Tests that genre metrics can be queried""" - with app.app_context(): - db = get_db() - - test_tracks = [{"genre": "Electronic"}, {"genre": "Pop"}, {"genre": "Electronic"}] - - date = datetime(2020, 10, 4, 10, 35, 0) - before_date = date + timedelta(hours=-1) - populate_mock_db(db, test_tracks, date) - - args = {"start_time": before_date} - - with db.scoped_session() as session: - metrics = _get_genre_metrics(session, args) - - assert metrics[0]["name"] == "Electronic" - assert metrics[0]["count"] == 2 - assert metrics[1]["name"] == "Pop" - assert metrics[1]["count"] == 1 - - -def test_get_genre_metrics_for_month(app): - """Tests that genre metrics can be queried over a large time range""" - date = datetime(2020, 10, 4, 10, 35, 0) - long_before_date = date + timedelta(days=-12) - before_date = date + timedelta(days=-1) - - with app.app_context(): - db = get_db() - - test_tracks = [ - {"genre": "Electronic", "created_at": date}, - {"genre": "Pop", "created_at": date}, - {"genre": "Electronic", "created_at": date}, - {"genre": "Electronic", "created_at": before_date}, - ] - populate_mock_db(db, test_tracks, date) - - args = {"start_time": before_date} - - with db.scoped_session() as session: - metrics = _get_genre_metrics(session, args) - - assert metrics[0]["name"] == "Electronic" - assert metrics[0]["count"] == 2 - assert metrics[1]["name"] == "Pop" - assert metrics[1]["count"] == 1 - - args2 = {"start_time": long_before_date} - - with db.scoped_session() as session: - metrics = _get_genre_metrics(session, args2) - - assert metrics[0]["name"] == "Electronic" - assert metrics[0]["count"] == 3 - assert metrics[1]["name"] == "Pop" - assert metrics[1]["count"] == 1 diff --git a/packages/discovery-provider/integration_tests/queries/test_get_historical_app_metrics.py b/packages/discovery-provider/integration_tests/queries/test_get_historical_app_metrics.py deleted file mode 100644 index 3351b5fb6cd..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_historical_app_metrics.py +++ /dev/null @@ -1,127 +0,0 @@ -from datetime import date, timedelta - -from src.models.metrics.aggregate_daily_app_name_metrics import ( - AggregateDailyAppNameMetric, -) -from src.models.metrics.aggregate_monthly_app_name_metrics import ( - AggregateMonthlyAppNameMetric, -) -from src.queries.get_app_name_metrics import _get_historical_app_metrics -from src.utils.db_session import get_db - -limit = 2 -today = date.today() -yesterday = today - timedelta(days=1) -day_before_yesterday = today - timedelta(days=2) -thirty_days_ago = today - timedelta(days=30) - - -def test_get_historical_app_metrics(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateDailyAppNameMetric( - application_name="top-app", - count=1, - timestamp=thirty_days_ago - timedelta(days=1), - ), - AggregateDailyAppNameMetric( - application_name="top-app", count=2, timestamp=thirty_days_ago - ), - AggregateDailyAppNameMetric( - application_name="best-app", count=1, timestamp=thirty_days_ago - ), - AggregateDailyAppNameMetric( - application_name="best-app", count=3, timestamp=yesterday - ), - AggregateDailyAppNameMetric( - application_name="best-app", count=4, timestamp=today - ), - AggregateMonthlyAppNameMetric( - application_name="top-app", - count=2, - timestamp=today - timedelta(days=367), - ), - AggregateMonthlyAppNameMetric( - application_name="best-app", - count=4, - timestamp=today - timedelta(days=100), - ), - AggregateMonthlyAppNameMetric( - application_name="other-app", - count=5, - timestamp=today - timedelta(days=100), - ), - AggregateMonthlyAppNameMetric( - application_name="top-app", count=6, timestamp=today - ), - ] - ) - - aggregate_metrics = _get_historical_app_metrics(session, 0) - daily_aggregate_metrics = aggregate_metrics["daily"] - monthly_aggregate_metrics = aggregate_metrics["monthly"] - - assert len(daily_aggregate_metrics.items()) == 2 - assert daily_aggregate_metrics[str(thirty_days_ago)]["top-app"] == 2 - assert daily_aggregate_metrics[str(thirty_days_ago)]["best-app"] == 1 - assert daily_aggregate_metrics[str(yesterday)]["best-app"] == 3 - - assert len(daily_aggregate_metrics.items()) == 2 - assert ( - monthly_aggregate_metrics[str(today - timedelta(days=367))]["top-app"] == 2 - ) - assert ( - monthly_aggregate_metrics[str(today - timedelta(days=100))]["best-app"] == 4 - ) - assert ( - monthly_aggregate_metrics[str(today - timedelta(days=100))]["other-app"] - == 5 - ) - - -def test_get_historical_app_metrics_with_min_count(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateDailyAppNameMetric( - application_name="best-app", count=4, timestamp=yesterday - ), - AggregateDailyAppNameMetric( - application_name="best-app", count=3, timestamp=day_before_yesterday - ), - AggregateDailyAppNameMetric( - application_name="top-app", count=6, timestamp=day_before_yesterday - ), - AggregateMonthlyAppNameMetric( - application_name="other-app", - count=3, - timestamp=today - timedelta(days=100), - ), - AggregateMonthlyAppNameMetric( - application_name="top-app", - count=6, - timestamp=today - timedelta(days=100), - ), - ] - ) - - # Min count of 4 - aggregate_metrics = _get_historical_app_metrics(session, 4) - daily_aggregate_metrics = aggregate_metrics["daily"] - monthly_aggregate_metrics = aggregate_metrics["monthly"] - - assert len(daily_aggregate_metrics.items()) == 2 - assert daily_aggregate_metrics[str(yesterday)]["best-app"] == 4 - assert daily_aggregate_metrics[str(day_before_yesterday)]["top-app"] == 6 - - assert len(monthly_aggregate_metrics.items()) == 1 - assert ( - monthly_aggregate_metrics[str(today - timedelta(days=100))]["top-app"] == 6 - ) diff --git a/packages/discovery-provider/integration_tests/queries/test_get_historical_route_metrics.py b/packages/discovery-provider/integration_tests/queries/test_get_historical_route_metrics.py deleted file mode 100644 index de8993bca8b..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_historical_route_metrics.py +++ /dev/null @@ -1,110 +0,0 @@ -from datetime import date, timedelta - -from src.models.metrics.aggregate_daily_total_users_metrics import ( - AggregateDailyTotalUsersMetrics, -) -from src.models.metrics.aggregate_daily_unique_users_metrics import ( - AggregateDailyUniqueUsersMetrics, -) -from src.models.metrics.aggregate_monthly_total_users_metrics import ( - AggregateMonthlyTotalUsersMetric, -) -from src.models.metrics.aggregate_monthly_unique_users_metrics import ( - AggregateMonthlyUniqueUsersMetric, -) -from src.queries.get_historical_route_metrics import _get_historical_route_metrics -from src.utils.db_session import get_db - -limit = 2 -today = date.today() -yesterday = today - timedelta(days=1) -thirty_days_ago = today - timedelta(days=30) - - -def test_get_historical_route_metrics(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateDailyUniqueUsersMetrics( - count=1, - summed_count=2, - timestamp=thirty_days_ago - timedelta(days=1), - ), - AggregateDailyUniqueUsersMetrics( - count=2, summed_count=3, timestamp=thirty_days_ago - ), - AggregateDailyUniqueUsersMetrics( - count=3, summed_count=4, timestamp=yesterday - ), - AggregateDailyUniqueUsersMetrics( - count=4, summed_count=5, timestamp=today - ), - AggregateDailyTotalUsersMetrics( - count=2, timestamp=thirty_days_ago - timedelta(days=1) - ), - AggregateDailyTotalUsersMetrics(count=4, timestamp=thirty_days_ago), - AggregateDailyTotalUsersMetrics(count=6, timestamp=yesterday), - AggregateDailyTotalUsersMetrics(count=8, timestamp=today), - AggregateMonthlyUniqueUsersMetric( - count=1, summed_count=2, timestamp=today - timedelta(days=367) - ), - AggregateMonthlyUniqueUsersMetric( - count=2, summed_count=3, timestamp=today - timedelta(days=100) - ), - AggregateMonthlyUniqueUsersMetric( - count=3, summed_count=4, timestamp=today - ), - AggregateMonthlyTotalUsersMetric( - count=2, timestamp=today - timedelta(days=367) - ), - AggregateMonthlyTotalUsersMetric( - count=4, timestamp=today - timedelta(days=100) - ), - AggregateMonthlyTotalUsersMetric(count=6, timestamp=today), - ] - ) - - aggregate_metrics = _get_historical_route_metrics(session) - daily_aggregate_metrics = aggregate_metrics["daily"] - monthly_aggregate_metrics = aggregate_metrics["monthly"] - - assert len(daily_aggregate_metrics.items()) == 2 - assert daily_aggregate_metrics[str(thirty_days_ago)]["unique_count"] == 2 - assert daily_aggregate_metrics[str(thirty_days_ago)]["summed_unique_count"] == 3 - assert daily_aggregate_metrics[str(thirty_days_ago)]["total_count"] == 4 - assert daily_aggregate_metrics[str(yesterday)]["unique_count"] == 3 - assert daily_aggregate_metrics[str(yesterday)]["summed_unique_count"] == 4 - assert daily_aggregate_metrics[str(yesterday)]["total_count"] == 6 - - assert len(monthly_aggregate_metrics.items()) == 2 - assert ( - monthly_aggregate_metrics[str(today - timedelta(days=367))]["unique_count"] - == 1 - ) - assert ( - monthly_aggregate_metrics[str(today - timedelta(days=367))][ - "summed_unique_count" - ] - == 2 - ) - assert ( - monthly_aggregate_metrics[str(today - timedelta(days=367))]["total_count"] - == 2 - ) - assert ( - monthly_aggregate_metrics[str(today - timedelta(days=100))]["unique_count"] - == 2 - ) - assert ( - monthly_aggregate_metrics[str(today - timedelta(days=100))][ - "summed_unique_count" - ] - == 3 - ) - assert ( - monthly_aggregate_metrics[str(today - timedelta(days=100))]["total_count"] - == 4 - ) diff --git a/packages/discovery-provider/integration_tests/queries/test_get_personal_route_metrics.py b/packages/discovery-provider/integration_tests/queries/test_get_personal_route_metrics.py deleted file mode 100644 index d9221a693bb..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_personal_route_metrics.py +++ /dev/null @@ -1,223 +0,0 @@ -from datetime import date, timedelta - -from src.models.metrics.aggregate_daily_total_users_metrics import ( - AggregateDailyTotalUsersMetrics, -) -from src.models.metrics.aggregate_daily_unique_users_metrics import ( - AggregateDailyUniqueUsersMetrics, -) -from src.models.metrics.aggregate_monthly_total_users_metrics import ( - AggregateMonthlyTotalUsersMetric, -) -from src.models.metrics.aggregate_monthly_unique_users_metrics import ( - AggregateMonthlyUniqueUsersMetric, -) -from src.queries.get_personal_route_metrics import _get_personal_route_metrics -from src.utils.db_session import get_db - -limit = 2 -today = date.today() -yesterday = today - timedelta(days=1) - - -def test_get_personal_route_metrics_week(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateDailyUniqueUsersMetrics( - count=1, personal_count=2, timestamp=today - timedelta(days=8) - ), - AggregateDailyUniqueUsersMetrics( - count=2, personal_count=1, timestamp=yesterday - timedelta(days=1) - ), - AggregateDailyUniqueUsersMetrics( - count=3, personal_count=3, timestamp=yesterday - ), - AggregateDailyUniqueUsersMetrics( - count=4, personal_count=2, timestamp=today - ), - AggregateDailyTotalUsersMetrics( - count=2, personal_count=1, timestamp=today - timedelta(days=8) - ), - AggregateDailyTotalUsersMetrics( - count=4, personal_count=4, timestamp=yesterday - timedelta(days=1) - ), - AggregateDailyTotalUsersMetrics( - count=6, personal_count=5, timestamp=yesterday - ), - AggregateDailyTotalUsersMetrics( - count=8, personal_count=8, timestamp=today - ), - ] - ) - - personal_metrics = _get_personal_route_metrics(session, "week", "day") - - assert len(personal_metrics) == 2 - assert personal_metrics[0]["unique_count"] == 1 - assert personal_metrics[0]["total_count"] == 4 - assert personal_metrics[1]["unique_count"] == 3 - assert personal_metrics[1]["total_count"] == 5 - - -def test_get_personal_route_metrics_month_daily_bucket(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateDailyUniqueUsersMetrics( - count=1, personal_count=1, timestamp=today - timedelta(days=31) - ), - AggregateDailyUniqueUsersMetrics( - count=2, personal_count=1, timestamp=today - timedelta(days=8) - ), - AggregateDailyUniqueUsersMetrics( - count=3, personal_count=2, timestamp=yesterday - ), - AggregateDailyUniqueUsersMetrics( - count=4, personal_count=5, timestamp=today - ), - AggregateDailyTotalUsersMetrics( - count=2, personal_count=2, timestamp=today - timedelta(days=31) - ), - AggregateDailyTotalUsersMetrics( - count=4, personal_count=3, timestamp=today - timedelta(days=8) - ), - AggregateDailyTotalUsersMetrics( - count=6, personal_count=6, timestamp=yesterday - ), - AggregateDailyTotalUsersMetrics( - count=8, personal_count=5, timestamp=today - ), - ] - ) - - personal_metrics = _get_personal_route_metrics(session, "month", "day") - - assert len(personal_metrics) == 2 - assert personal_metrics[0]["unique_count"] == 1 - assert personal_metrics[0]["total_count"] == 3 - assert personal_metrics[1]["unique_count"] == 2 - assert personal_metrics[1]["total_count"] == 6 - - -def test_get_personal_route_metrics_month_weekly_bucket(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateDailyUniqueUsersMetrics( - count=2, personal_count=1, timestamp=today - timedelta(days=31) - ), - AggregateDailyUniqueUsersMetrics( - count=2, personal_count=2, timestamp=today - timedelta(days=8) - ), - AggregateDailyUniqueUsersMetrics( - count=3, personal_count=3, timestamp=yesterday - ), - AggregateDailyUniqueUsersMetrics( - count=4, personal_count=4, timestamp=today - ), - AggregateDailyTotalUsersMetrics( - count=2, personal_count=2, timestamp=today - timedelta(days=31) - ), - AggregateDailyTotalUsersMetrics( - count=4, personal_count=3, timestamp=today - timedelta(days=8) - ), - AggregateDailyTotalUsersMetrics( - count=6, personal_count=6, timestamp=yesterday - ), - AggregateDailyTotalUsersMetrics( - count=8, personal_count=7, timestamp=today - ), - ] - ) - - personal_metrics = _get_personal_route_metrics(session, "month", "week") - - assert len(personal_metrics) == 2 - assert personal_metrics[0]["unique_count"] == 2 - assert personal_metrics[0]["total_count"] == 3 - assert personal_metrics[1]["unique_count"] == 3 - assert personal_metrics[1]["total_count"] == 6 - - -def test_get_personal_route_metrics_all_time_monthly_bucket(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateMonthlyUniqueUsersMetric( - count=1, personal_count=1, timestamp=today - timedelta(days=367) - ), - AggregateMonthlyUniqueUsersMetric( - count=2, personal_count=2, timestamp=today - timedelta(days=100) - ), - AggregateMonthlyUniqueUsersMetric( - count=3, personal_count=3, timestamp=today - ), - AggregateMonthlyTotalUsersMetric( - count=2, personal_count=2, timestamp=today - timedelta(days=367) - ), - AggregateMonthlyTotalUsersMetric( - count=4, personal_count=3, timestamp=today - timedelta(days=100) - ), - AggregateMonthlyTotalUsersMetric( - count=6, personal_count=4, timestamp=today - ), - ] - ) - - personal_metrics = _get_personal_route_metrics(session, "all_time", "month") - - assert len(personal_metrics) == 2 - assert personal_metrics[0]["unique_count"] == 1 - assert personal_metrics[0]["total_count"] == 2 - assert personal_metrics[1]["unique_count"] == 2 - assert personal_metrics[1]["total_count"] == 3 - - -def test_get_personal_route_metrics_all_time_weekly_bucket(app): - with app.app_context(): - db_mock = get_db() - - with db_mock.scoped_session() as session: - session.bulk_save_objects( - [ - AggregateDailyUniqueUsersMetrics( - count=1, personal_count=1, timestamp=today - timedelta(days=367) - ), - AggregateDailyUniqueUsersMetrics( - count=2, personal_count=2, timestamp=yesterday - ), - AggregateDailyUniqueUsersMetrics( - count=3, personal_count=3, timestamp=today - ), - AggregateDailyTotalUsersMetrics( - count=2, personal_count=1, timestamp=today - timedelta(days=367) - ), - AggregateDailyTotalUsersMetrics( - count=4, personal_count=3, timestamp=yesterday - ), - AggregateDailyTotalUsersMetrics( - count=6, personal_count=6, timestamp=today - ), - ] - ) - - personal_metrics = _get_personal_route_metrics(session, "all_time", "week") - - assert len(personal_metrics) == 2 - assert personal_metrics[0]["unique_count"] == 1 - assert personal_metrics[0]["total_count"] == 1 - assert personal_metrics[1]["unique_count"] == 2 - assert personal_metrics[1]["total_count"] == 3 diff --git a/packages/discovery-provider/integration_tests/queries/test_get_plays_metrics.py b/packages/discovery-provider/integration_tests/queries/test_get_plays_metrics.py deleted file mode 100644 index d0cd8c3646b..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_plays_metrics.py +++ /dev/null @@ -1,158 +0,0 @@ -import time -from datetime import datetime, timedelta - -from integration_tests.utils import populate_mock_db -from src.queries.get_plays_metrics import GetPlayMetricsArgs, _get_plays_metrics -from src.tasks.index_hourly_play_counts import _index_hourly_play_counts -from src.utils.db_session import get_db - -DAYS_IN_A_YEAR = 365 - - -def format_date(date): - return int(time.mktime(date.timetuple())) - - -def test_get_plays_metrics(app): - """Tests that plays metrics can be queried""" - - with app.app_context(): - db = get_db() - - date = datetime(2020, 10, 4).replace(minute=0, second=0, microsecond=0) - test_entities = { - "tracks": [ - {"track_id": 1, "title": "track 1"}, - {"track_id": 2, "title": "track 2"}, - {"track_id": 3, "title": "track 3"}, - ], - "plays": [ - {"item_id": 1, "created_at": date + timedelta(hours=-1)}, - {"item_id": 1, "created_at": date + timedelta(hours=-1)}, - {"item_id": 1, "created_at": date + timedelta(days=-2)}, - {"item_id": 2, "created_at": date + timedelta(hours=-1)}, - {"item_id": 2, "created_at": date + timedelta(days=-2)}, - {"item_id": 3, "created_at": date + timedelta(days=-2)}, - {"item_id": 3, "created_at": date + timedelta(days=-2)}, - {"item_id": 3, "created_at": date + timedelta(days=-2)}, - ], - } - - populate_mock_db(db, test_entities) - - args = GetPlayMetricsArgs( - limit=10, - start_time=date + timedelta(days=-3), - bucket_size="hour", - ) - - with db.scoped_session() as session: - _index_hourly_play_counts(session) - metrics = _get_plays_metrics(session, args) - - assert len(metrics) == 2 - assert metrics[0]["timestamp"] == format_date(date + timedelta(hours=-1)) - assert metrics[0]["count"] == 3 - assert metrics[1]["timestamp"] == format_date(date + timedelta(days=-2)) - assert metrics[1]["count"] == 5 - - -def test_get_plays_metrics_with_weekly_buckets(app): - """Tests that plays metrics can be queried with weekly buckets""" - - with app.app_context(): - db = get_db() - - # A Thursday - date = datetime(2020, 10, 1).replace(minute=0, second=0, microsecond=0) - test_entities = { - "tracks": [ - {"track_id": 1, "title": "track 1"}, - {"track_id": 2, "title": "track 2"}, - {"track_id": 3, "title": "track 3"}, - ], - "plays": [ - {"item_id": 1, "created_at": date + timedelta(hours=-1)}, - {"item_id": 1, "created_at": date + timedelta(hours=-1)}, - {"item_id": 1, "created_at": date + timedelta(days=-2)}, - {"item_id": 2, "created_at": date + timedelta(hours=-1)}, - {"item_id": 2, "created_at": date + timedelta(days=-2)}, - {"item_id": 3, "created_at": date + timedelta(days=-2)}, - {"item_id": 3, "created_at": date + timedelta(days=-2)}, - {"item_id": 3, "created_at": date + timedelta(days=-2)}, - ], - } - - populate_mock_db(db, test_entities) - - start_time = date + timedelta(days=-3) - args = GetPlayMetricsArgs( - limit=10, - start_time=date + timedelta(days=-3), - bucket_size="week", - ) - - with db.scoped_session() as session: - _index_hourly_play_counts(session) - metrics = _get_plays_metrics(session, args) - - assert len(metrics) == 1 - assert metrics[0]["count"] == 8 - assert metrics[0]["timestamp"] == format_date(start_time) - - -def test_get_plays_metrics_with_yearly_buckets(app): - """Tests that plays metrics can be queried""" - - with app.app_context(): - db = get_db() - - date = datetime(2020, 10, 4).replace(minute=0, second=0, microsecond=0) - test_entities = { - "tracks": [ - {"track_id": 1, "title": "track 1"}, - {"track_id": 2, "title": "track 2"}, - {"track_id": 3, "title": "track 3"}, - ], - "plays": [ - {"item_id": 1, "created_at": date + timedelta(days=-3 * DAYS_IN_A_YEAR)}, - {"item_id": 1, "created_at": date + timedelta(days=-3 * DAYS_IN_A_YEAR)}, - {"item_id": 3, "created_at": date + timedelta(days=-3 * DAYS_IN_A_YEAR)}, - {"item_id": 1, "created_at": date + timedelta(days=-2 * DAYS_IN_A_YEAR)}, - {"item_id": 2, "created_at": date + timedelta(days=-2 * DAYS_IN_A_YEAR)}, - {"item_id": 3, "created_at": date + timedelta(days=-1 * DAYS_IN_A_YEAR)}, - {"item_id": 3, "created_at": date + timedelta(days=-1)}, - {"item_id": 3, "created_at": date + timedelta(weeks=-1)}, - ], - } - - populate_mock_db(db, test_entities) - - start_time = date + timedelta( - days=-3 * DAYS_IN_A_YEAR - 1 - ) # -1 extra day to be inclusive - args = GetPlayMetricsArgs( - limit=10, - start_time=start_time, - bucket_size="year", - ) - - with db.scoped_session() as session: - _index_hourly_play_counts(session) - metrics = _get_plays_metrics(session, args) - - assert len(metrics) == 4 - assert metrics[0]["count"] == 2 - assert metrics[0]["timestamp"] == format_date(date.replace(day=1, month=1)) - assert metrics[1]["count"] == 1 - assert metrics[1]["timestamp"] == format_date( - (date + timedelta(days=-1 * DAYS_IN_A_YEAR)).replace(day=1, month=1) - ) - assert metrics[2]["count"] == 2 - assert metrics[2]["timestamp"] == format_date( - (date + timedelta(days=-2 * DAYS_IN_A_YEAR)).replace(day=1, month=1) - ) - assert metrics[3]["count"] == 3 - assert metrics[3]["timestamp"] == format_date( - (date + timedelta(days=-3 * DAYS_IN_A_YEAR)).replace(day=1, month=1) - ) diff --git a/packages/discovery-provider/integration_tests/queries/test_get_prev_track_entries.py b/packages/discovery-provider/integration_tests/queries/test_get_prev_track_entries.py deleted file mode 100644 index b2a7dc18f58..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_prev_track_entries.py +++ /dev/null @@ -1,132 +0,0 @@ -from integration_tests.utils import populate_mock_db -from src.models.tracks.track import Track -from src.queries.get_prev_track_entries import get_prev_track_entries -from src.utils.db_session import get_db - - -def test_get_prev_track_entries(app): - """Tests that prev track entries query properly returns previous tracks""" - with app.app_context(): - db = get_db() - - test_entities = { - "tracks": [ - { - "track_id": 1, - "is_current": False, - "is_unlisted": True, - "remix_of": None, - }, # Block 0 - { - "track_id": 2, - "is_current": True, - "is_unlisted": False, - "remix_of": None, - }, # Block 1 - { - "track_id": 3, - "is_current": False, - "is_unlisted": False, - "remix_of": None, - }, # Block 2 - { - "track_id": 4, - "is_current": False, - "is_unlisted": False, - "remix_of": None, - }, # Block 3 - { - "track_id": 5, - "is_current": False, - "is_unlisted": False, - "remix_of": None, - }, # Block 4 - { - "track_id": 6, - "is_current": False, - "is_unlisted": True, - "remix_of": None, - }, # Block 5 - { - "track_id": 1, - "is_current": True, - "is_unlisted": False, - "remix_of": None, - }, # Block 6 - { - "track_id": 3, - "is_current": False, - "is_unlisted": False, - "remix_of": None, - }, # Block 7 - { - "track_id": 6, - "is_current": True, - "is_unlisted": False, - "remix_of": None, - }, # Block 8 - { - "track_id": 4, - "is_current": True, - "is_unlisted": False, - "remix_of": None, - }, # Block 9 - { - "track_id": 3, - "is_current": True, - "is_unlisted": False, - "remix_of": None, - }, # Block 10 - { - "track_id": 5, - "is_current": False, - "is_unlisted": False, - "remix_of": None, - }, # Block 11 - { - "track_id": 5, - "is_current": True, - "is_unlisted": False, - "remix_of": None, - }, # Block 12 - ], - } - - populate_mock_db(db, test_entities) - - with db.scoped_session() as session: - # Make sure it doesn't return tracks if none are passed in - empty_entries = get_prev_track_entries(session, []) - - assert len(empty_entries) == 0 - - # Make sure that it fetches all previous tracks - entries = [ - Track(track_id=6, blocknumber=8), - Track(track_id=6, blocknumber=8), - Track(track_id=3, blocknumber=10), - Track(track_id=1, blocknumber=6), - Track(track_id=4, blocknumber=9), - Track(track_id=5, blocknumber=12), - ] - prev_entries = get_prev_track_entries(session, entries) - - assert len(prev_entries) <= len(entries) - - for prev_entry in prev_entries: - entry = next(e for e in entries if e.track_id == prev_entry.track_id) - assert entry.track_id == prev_entry.track_id - assert entry.blocknumber > prev_entry.blocknumber - # previous track with id 3 should have a block number of 7, not 2 - if prev_entry.track_id == 3: - assert prev_entry.blocknumber == 7 - # previous track with id 5 should have a block number of 11, not 4 - if prev_entry.track_id == 5: - assert prev_entry.blocknumber == 11 - - # Make sure that it properly fetches the track before the one passed - single_entry = [Track(track_id=5, blocknumber=11)] - prev_id_5_track = get_prev_track_entries(session, single_entry)[0] - - assert prev_id_5_track.track_id == 5 - assert prev_id_5_track.blocknumber < 11 diff --git a/packages/discovery-provider/integration_tests/queries/test_get_related_artists.py b/packages/discovery-provider/integration_tests/queries/test_get_related_artists.py deleted file mode 100644 index fd2f67bcf53..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_related_artists.py +++ /dev/null @@ -1,167 +0,0 @@ -import logging - -from integration_tests.utils import populate_mock_db -from src.queries.get_related_artists import get_related_artists -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - - -def test_get_related_artists(app): - """Test getting related artists based on genre similarity""" - with app.app_context(): - db = get_db() - - test_entities = { - "tracks": [ - # User 1's tracks - mostly Electronic - { - "track_id": 1, - "owner_id": 1, - "genre": "Electronic", - }, - { - "track_id": 2, - "owner_id": 1, - "genre": "Electronic", - }, - { - "track_id": 3, - "owner_id": 1, - "genre": "Pop", - }, - # User 2's tracks - only Electronic - { - "track_id": 4, - "owner_id": 2, - "genre": "Electronic", - }, - { - "track_id": 5, - "owner_id": 2, - "genre": "Electronic", - }, - # User 3's tracks - only Pop - { - "track_id": 6, - "owner_id": 3, - "genre": "Pop", - }, - { - "track_id": 7, - "owner_id": 3, - "genre": "Pop", - }, - # User 4's tracks - mix of genres - { - "track_id": 8, - "owner_id": 4, - "genre": "Electronic", - }, - { - "track_id": 9, - "owner_id": 4, - "genre": "Pop", - }, - ], - "users": [ - {"user_id": 1, "handle": "user1"}, - {"user_id": 2, "handle": "user2"}, - {"user_id": 3, "handle": "user3"}, - {"user_id": 4, "handle": "user4"}, - ], - "follows": [ - { - "follower_user_id": 1, - "followee_user_id": 2, - }, # User 1 follows User 2 - ], - "aggregate_user": [ - {"user_id": 1, "follower_count": 100, "dominant_genre": "Electronic"}, - { - "user_id": 2, - "follower_count": 50, - "dominant_genre": "Electronic", - }, # Should be recommended (similar genre, lower follower count) - { - "user_id": 3, - "follower_count": 400, - "dominant_genre": "Pop", - }, # Should not be recommended (too many followers) - { - "user_id": 4, - "follower_count": 75, - "dominant_genre": "Electronic", - }, # Should be recommended - ], - } - - populate_mock_db(db, test_entities) - - # Test basic related artists functionality - related = get_related_artists(1, None) - assert len(related) == 2 # Should get users 2 and 4 - assert ( - related[0]["user_id"] == 4 - ) # User 2 should be first (same genre_rank but higher follower count) - assert ( - related[1]["user_id"] == 2 - ) # User 4 should be second (same genre_rank but lower follower count) - - # Test with filter_followed=True and current_user_id - related = get_related_artists(1, 1, filter_followed=True) - assert len(related) == 1 # Should just be 4 since user 1 follows user 2 - user_ids = [user["user_id"] for user in related] - assert 4 in user_ids - - # Test pagination - related = get_related_artists(1, None, limit=1) - assert len(related) == 1 - assert ( - related[0]["user_id"] == 4 - ) # First related artist should be user 2 (same genre_rank but lower follower count) - - related = get_related_artists(1, None, limit=1, offset=1) - assert len(related) == 1 - assert ( - related[0]["user_id"] == 2 - ) # Second related artist should be user 4 (same genre_rank but higher follower count) - - -def test_get_related_artists_edge_cases(app): - """Test edge cases for getting related artists""" - with app.app_context(): - db = get_db() - - test_entities = { - "tracks": [ - # User with no tracks (user 1) - # User with deleted/unlisted tracks (user 2) - {"track_id": 1, "owner_id": 2, "genre": "Electronic"}, - {"track_id": 2, "owner_id": 2, "genre": "Electronic"}, - # User with valid tracks (user 3) - {"track_id": 3, "owner_id": 3, "genre": "Electronic"}, - ], - "users": [ - {"user_id": 1, "handle": "user1"}, - {"user_id": 2, "handle": "user2"}, - {"user_id": 3, "handle": "user3"}, - ], - "aggregate_user": [ - {"user_id": 1, "follower_count": 100, "dominant_genre": "Electronic"}, - {"user_id": 2, "follower_count": 50, "dominant_genre": "Electronic"}, - {"user_id": 3, "follower_count": 75, "dominant_genre": "Electronic"}, - ], - } - - populate_mock_db(db, test_entities) - - # Test user with no tracks - related = get_related_artists(1, None) - assert ( - len(related) == 0 - ) # Should get no users since user 1 has no valid tracks to determine genre from - - # Test with non-existent user - related = get_related_artists(999, None) - assert len(related) == 0 diff --git a/packages/discovery-provider/integration_tests/queries/test_get_repost_feed_for_user.py b/packages/discovery-provider/integration_tests/queries/test_get_repost_feed_for_user.py deleted file mode 100644 index 03f6aa46e26..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_repost_feed_for_user.py +++ /dev/null @@ -1,119 +0,0 @@ -from integration_tests.utils import populate_mock_db -from src.queries.get_repost_feed_for_user import _get_repost_feed_for_user -from src.utils.db_session import get_db - - -def test_get_repost_feed_for_user(app): - """Tests that a repost feed for a user can be queried""" - with app.app_context(): - db = get_db() - - test_entities = { - "reposts": [ - # Note these reposts are in chronological order in addition - # so the repost feed should pull them "backwards" for reverse chronological - # sort order. - {"user_id": 1, "repost_item_id": 5, "repost_type": "track"}, - {"user_id": 1, "repost_item_id": 2, "repost_type": "track"}, - {"user_id": 1, "repost_item_id": 3, "repost_type": "track"}, - {"user_id": 1, "repost_item_id": 1, "repost_type": "track"}, - {"user_id": 1, "repost_item_id": 4, "repost_type": "track"}, - {"user_id": 1, "repost_item_id": 4, "repost_type": "playlist"}, - {"user_id": 1, "repost_item_id": 8, "repost_type": "album"}, - {"user_id": 1, "repost_item_id": 6, "repost_type": "track"}, - ], - "tracks": [ - {"track_id": 1, "title": "track 1"}, - {"track_id": 2, "title": "track 2"}, - {"track_id": 3, "title": "track 3"}, - {"track_id": 4, "title": "track 4"}, - {"track_id": 5, "title": "track 5"}, - {"track_id": 6, "title": "track 6"}, - {"track_id": 7, "title": "track 7"}, - {"track_id": 8, "title": "track 8"}, - ], - "playlists": [ - {"playlist_id": 1, "playlist_name": "playlist 1"}, - {"playlist_id": 2, "playlist_name": "playlist 2"}, - {"playlist_id": 3, "playlist_name": "playlist 3"}, - {"playlist_id": 4, "playlist_name": "playlist 4"}, - {"playlist_id": 5, "playlist_name": "playlist 5"}, - {"playlist_id": 6, "playlist_name": "playlist 6"}, - {"playlist_id": 7, "playlist_name": "playlist 7"}, - {"playlist_id": 8, "playlist_name": "album 8"}, - ], - } - - populate_mock_db(db, test_entities) - - with db.scoped_session() as session: - repost_feed = _get_repost_feed_for_user(session, 1, {"limit": 10, "offset": 0}) - - assert repost_feed[0]["title"] == "track 6" - assert repost_feed[1]["playlist_name"] == "album 8" - assert repost_feed[2]["playlist_name"] == "playlist 4" - assert repost_feed[3]["title"] == "track 4" - assert repost_feed[4]["title"] == "track 1" - assert repost_feed[5]["title"] == "track 3" - assert repost_feed[6]["title"] == "track 2" - assert repost_feed[7]["title"] == "track 5" - - -def test_get_repost_feed_for_user_limit_bounds(app): - """ - Tests that a repost feed for a user can be queried and respect a limit - with deleted tracks. - """ - with app.app_context(): - db = get_db() - - test_entities = { - "reposts": [ - # Note these reposts are in chronological order in addition - # so the repost feed should pull them "backwards" for reverse chronological - # sort order. - {"user_id": 1, "repost_item_id": 5, "repost_type": "track"}, - {"user_id": 1, "repost_item_id": 2, "repost_type": "track"}, - {"user_id": 1, "repost_item_id": 3, "repost_type": "track"}, - {"user_id": 1, "repost_item_id": 1, "repost_type": "track"}, - {"user_id": 1, "repost_item_id": 4, "repost_type": "track"}, - {"user_id": 1, "repost_item_id": 4, "repost_type": "playlist"}, - {"user_id": 1, "repost_item_id": 8, "repost_type": "album"}, - {"user_id": 1, "repost_item_id": 6, "repost_type": "track"}, - ], - "tracks": [ - {"track_id": 1, "title": "track 1", "is_delete": True}, - {"track_id": 2, "title": "track 2"}, - {"track_id": 3, "title": "track 3"}, - {"track_id": 4, "title": "track 4"}, - {"track_id": 5, "title": "track 5"}, - {"track_id": 6, "title": "track 6"}, - {"track_id": 7, "title": "track 7"}, - {"track_id": 8, "title": "track 8"}, - ], - "playlists": [ - {"playlist_id": 1, "playlist_name": "playlist 1"}, - {"playlist_id": 2, "playlist_name": "playlist 2"}, - {"playlist_id": 3, "playlist_name": "playlist 3"}, - {"playlist_id": 4, "playlist_name": "playlist 4"}, - {"playlist_id": 5, "playlist_name": "playlist 5"}, - {"playlist_id": 6, "playlist_name": "playlist 6"}, - {"playlist_id": 7, "playlist_name": "playlist 7"}, - {"playlist_id": 8, "playlist_name": "album 8"}, - ], - } - - populate_mock_db(db, test_entities) - - with db.scoped_session() as session: - repost_feed = _get_repost_feed_for_user(session, 1, {"limit": 5, "offset": 0}) - - # Query for 5 reposts. The problem is the 5th one was deleted, so - # we only return 4 here. This is broken. - # TODO fix me. - assert repost_feed[0]["title"] == "track 6" - assert repost_feed[1]["playlist_name"] == "album 8" - assert repost_feed[2]["playlist_name"] == "playlist 4" - assert repost_feed[3]["title"] == "track 4" - # Should skip track 1 because it is deleted - assert repost_feed[4]["title"] == "track 3" diff --git a/packages/discovery-provider/integration_tests/queries/test_get_tips.py b/packages/discovery-provider/integration_tests/queries/test_get_tips.py deleted file mode 100644 index a0afbc7e5bd..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_tips.py +++ /dev/null @@ -1,81 +0,0 @@ -import logging - -import pytest - -from integration_tests.utils import populate_mock_db -from src.queries.get_tips import GetTipsArgs, get_tips -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - - -@pytest.fixture -def test_entities(): - return { - "users": [ - {"user_id": 1, "handle": "user1"}, - {"user_id": 2, "handle": "user2"}, - {"user_id": 3, "handle": "DontShowMyTips"}, - ], - "follows": [ - {"follower_user_id": 1, "followee_user_id": 2}, - {"follower_user_id": 1, "followee_user_id": 3}, - ], - "user_tips": [ - { - "slot": 0, - "signature": "abcdefg", - "sender_user_id": 1, - "receiver_user_id": 2, - "amount": 100000000, - }, - { - "slot": 1, - "signature": "abcdefg", - "sender_user_id": 1, - "receiver_user_id": 3, - "amount": 100000000, - }, - ], - "aggregate_user_tips": [ - {"sender_user_id": 1, "receiver_user_id": 2, "amount": 100000000}, - {"sender_user_id": 1, "receiver_user_id": 3, "amount": 100000000}, - ], - } - - -def test_exclude_receivers_from_query(app, test_entities): - # Add pagination variables to only retrieve first tip result - with app.test_request_context("?limit=1&offset=0"): - db = get_db() - populate_mock_db(db, test_entities) - with db.scoped_session(): - # Test first without filtering, should get the most recent tip - tips_unfiltered = get_tips( - GetTipsArgs( - user_id=1, - current_user_follows=True, - unique_by="receiver", - ) - ) - - assert len(tips_unfiltered) == 1 - tip = tips_unfiltered[0] - assert tip["sender"]["user_id"] == 1 - assert tip["receiver"]["user_id"] == 3 - - # Filter out recipient with most recent tip, should get the next - # most recent tip - tips = get_tips( - GetTipsArgs( - user_id=1, - current_user_follows=True, - unique_by="receiver", - exclude_recipients=[3], - ) - ) - - assert len(tips) == 1 - tip = tips[0] - assert tip["sender"]["user_id"] == 1 - assert tip["receiver"]["user_id"] == 2 diff --git a/packages/discovery-provider/integration_tests/queries/test_get_top_genre_users.py b/packages/discovery-provider/integration_tests/queries/test_get_top_genre_users.py deleted file mode 100644 index e8de56e46f5..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_top_genre_users.py +++ /dev/null @@ -1,46 +0,0 @@ -import logging - -from integration_tests.utils import populate_mock_db -from src.queries.get_top_genre_users import _get_top_genre_users -from src.tasks.update_aggregates import _update_aggregates -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - - -def test_get_top_genre_users(app): - with app.app_context(): - db = get_db() - - entities = { - "tracks": [ - {"track_id": 1, "owner_id": 1, "genre": "Electronic"}, - {"track_id": 2, "owner_id": 2, "genre": "Electronic"}, - {"track_id": 3, "owner_id": 2, "genre": "Pop"}, - {"track_id": 4, "owner_id": 2, "genre": "Pop"}, - {"track_id": 5, "owner_id": 3, "genre": "Pop"}, - {"track_id": 6, "owner_id": 4, "genre": "Electronic"}, - ], - "users": [{"user_id": 1}, {"user_id": 2}, {"user_id": 3}, {"user_id": 4}], - "follows": [ - {"follower_user_id": 1, "followee_user_id": 2}, - {"follower_user_id": 2, "followee_user_id": 1}, - {"follower_user_id": 2, "followee_user_id": 3}, - {"follower_user_id": 3, "followee_user_id": 2}, - {"follower_user_id": 3, "followee_user_id": 4}, - ], - } - - populate_mock_db(db, entities) - with db.scoped_session() as session: - _update_aggregates(session) - - with db.scoped_session() as session: - users = _get_top_genre_users(session, {"genre": "Pop"}) - assert users[0]["user_id"] == 2 - assert users[1]["user_id"] == 3 - - users = _get_top_genre_users(session, {"genre": "Electronic"}) - # Tie break goes to user 1 over user 4 - assert users[0]["user_id"] == 1 - assert users[1]["user_id"] == 4 diff --git a/packages/discovery-provider/integration_tests/queries/test_get_top_user_track_tags.py b/packages/discovery-provider/integration_tests/queries/test_get_top_user_track_tags.py deleted file mode 100644 index 7272f931d85..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_top_user_track_tags.py +++ /dev/null @@ -1,34 +0,0 @@ -from integration_tests.utils import populate_mock_db -from src.queries.get_top_user_track_tags import _get_top_user_track_tags -from src.utils.db_session import get_db - - -def test_get_top_user_track_tags(app): - """Tests that top tags for users can be queried""" - with app.app_context(): - db = get_db() - - test_entities = { - "tracks": [ - {"tags": ""}, - {}, - {"tags": "pop,rock,electric"}, - {"tags": "pop,rock"}, - {"tags": "funk,pop"}, - ] - } - - populate_mock_db(db, test_entities) - - with db.scoped_session() as session: - session.execute("REFRESH MATERIALIZED VIEW tag_track_user") - user_1_tags = _get_top_user_track_tags(session, {"user_id": 1}) - user_2_tags = _get_top_user_track_tags(session, {"user_id": 2}) - - assert len(user_1_tags) == 4 - assert user_1_tags[0] == "pop" - assert user_1_tags[1] == "rock" - assert "electric" in user_1_tags - assert "funk" in user_1_tags - - assert not user_2_tags diff --git a/packages/discovery-provider/integration_tests/queries/test_get_total_plays.py b/packages/discovery-provider/integration_tests/queries/test_get_total_plays.py deleted file mode 100644 index 73746318863..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_total_plays.py +++ /dev/null @@ -1,41 +0,0 @@ -from integration_tests.utils import populate_mock_db -from src.queries.get_total_plays import _get_total_plays -from src.utils.db_session import get_db - - -def test_get_total_plays(app): - """Tests that total plays can be queried""" - with app.app_context(): - db = get_db() - - # Set up test data - test_entities = { - "aggregate_plays": [ - {"count": 100}, - {"count": 200}, - {"count": 300}, - ] - } - - populate_mock_db(db, test_entities) - - with db.scoped_session() as session: - total = _get_total_plays(session) - - assert total == 600 - - -def test_get_total_plays_empty(app): - """Tests that total plays returns 0 when no plays exist""" - with app.app_context(): - db = get_db() - - # Set up empty test data - test_entities = {"aggregate_plays": []} - - populate_mock_db(db, test_entities) - - with db.scoped_session() as session: - total = _get_total_plays(session) - - assert total == 0 diff --git a/packages/discovery-provider/integration_tests/queries/test_get_tracks.py b/packages/discovery-provider/integration_tests/queries/test_get_tracks.py deleted file mode 100644 index 9ac592c5c6a..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_tracks.py +++ /dev/null @@ -1,575 +0,0 @@ -from datetime import datetime - -from integration_tests.utils import populate_mock_db -from src.queries.get_remixable_tracks import get_remixable_tracks -from src.queries.get_remixes_of import get_remixes_of -from src.queries.get_tracks import _get_tracks -from src.queries.query_helpers import SortDirection, SortMethod -from src.utils.db_session import get_db - - -def populate_tracks(db): - test_entities = { - "tracks": [ - { - "track_id": 1, - "title": "track 1", - "owner_id": 1287289, - "release_date": datetime(2019, 12, 20), - "created_at": datetime(2018, 5, 17), - }, - { - "track_id": 2, - "title": "track 2", - "owner_id": 1287289, - "created_at": datetime(2018, 5, 18), - }, - { - "track_id": 3, - "title": "track 3", - "owner_id": 1287289, - "release_date": datetime(2019, 12, 18), - "created_at": datetime(2020, 5, 17), - "ai_attribution_user_id": 1287289, - }, - { - "track_id": 4, - "title": "track 4", - "owner_id": 1287289, - "release_date": None, - "created_at": datetime(2018, 5, 19), - }, - { - "track_id": 5, - "title": "track 5", - "owner_id": 1287289, - "release_date": None, - "created_at": datetime(2018, 5, 20), - }, - { - "track_id": 6, - "title": "track 6", - "owner_id": 4, - "release_date": datetime(2019, 12, 18), - "created_at": datetime(2020, 5, 17), - }, - { - "track_id": 7, - "title": "track 7", - "owner_id": 4, - "release_date": None, - "created_at": datetime(2018, 5, 19), - }, - { - "track_id": 8, - "title": "track 8", - "owner_id": 4, - "release_date": None, - "created_at": datetime(2018, 5, 20), - }, - { - "track_id": 9, - "title": "track 9", - "owner_id": 4, - "release_date": datetime(2019, 12, 25), - "created_at": datetime(2018, 5, 20), - "is_unlisted": True, - }, - { - "track_id": 10, - "title": "track 10", - "owner_id": 4, - "release_date": datetime(2019, 12, 25), - "created_at": datetime(2018, 5, 21), - "is_delete": True, - }, - { - "track_id": 11, - "title": "track 11", - "owner_id": 1287289, - "release_date": datetime(2019, 12, 19), - "created_at": datetime(2018, 5, 17), - "is_unlisted": True, - }, - { - "track_id": 12, - "title": "track 12", - "owner_id": 5, - "release_date": datetime(2020, 6, 19), - "created_at": datetime(2018, 5, 21), - "ai_attribution_user_id": 1287289, - }, - { - "track_id": 13, - "title": "track 13", - "owner_id": 5, - "release_date": datetime(2022, 10, 7), - "created_at": datetime(2018, 5, 17), - }, - { - "track_id": 14, - "title": "track 14", - "owner_id": 5, - "release_date": datetime(2019, 12, 25), - "created_at": datetime(2020, 5, 17), - }, - { - "track_id": 15, - "title": "track 15", - "owner_id": 1287289, - "release_date": None, - "created_at": datetime(2017, 5, 19), - }, - { - "track_id": 16, - "title": "track 16", - "owner_id": 1287289, - "release_date": datetime(2017, 5, 19), - "created_at": datetime(2017, 5, 19), - }, - ], - "track_routes": [ - {"slug": "track-1", "owner_id": 1287289}, - {"slug": "track-2", "owner_id": 1287289}, - { - "slug": "different-track", - "owner_id": 4, - "track_id": 6, - }, - { - "slug": "track-1", - "owner_id": 4, - "track_id": 7, - }, - { - "slug": "track-2", - "owner_id": 4, - "track_id": 8, - }, - { - "slug": "hidden-track", - "owner_id": 4, - "track_id": 9, - }, - ], - "users": [ - {"user_id": 1287289, "handle": "some-test-user"}, - {"user_id": 4, "wallet": "0xuser4wallet", "handle": "some-other-user"}, - { - "user_id": 5, - "handle": "test-user-5", - "artist_pick_track_id": 12, - "allow_ai_attribution": True, - }, - ], - "grants": [ - { - "user_id": 1287289, - "grantee_address": "0xuser4wallet", - "is_approved": True, - "is_revoked": False, - }, - ], - } - - populate_mock_db(db, test_entities) - - -def test_get_tracks_by_date(app): - """Test getting tracks ordering by date""" - - with app.app_context(): - db = get_db() - - populate_tracks(db) - - with db.scoped_session() as session: - tracks = _get_tracks( - session, {"user_id": 1287289, "offset": 0, "limit": 10, "sort": "date"} - ) - - assert len(tracks) == 7 - assert tracks[0]["track_id"] == 1 - assert tracks[1]["track_id"] == 3 - assert tracks[2]["track_id"] == 5 - assert tracks[3]["track_id"] == 4 - assert tracks[4]["track_id"] == 2 - - # tracks created on the same day, with one missing 'release_date` - # should fall back to sorting by id - assert tracks[5]["track_id"] == 15 - assert tracks[6]["track_id"] == 16 - - assert tracks[0]["permalink"] == "/some-test-user/track-1" - assert tracks[4]["permalink"] == "/some-test-user/track-2" - - -def test_get_tracks_with_query(app): - """Test getting tracks with a query""" - - with app.app_context(): - db = get_db() - - populate_tracks(db) - - with db.scoped_session() as session: - tracks = _get_tracks( - session, {"user_id": 1287289, "offset": 0, "limit": 10, "query": "track 5"} - ) - - assert len(tracks) == 1 - assert tracks[0]["track_id"] == 5 - - -def test_get_tracks_by_date_authed(app): - """ - Test getting tracks ordering by date with an authed user. - This test should produce unlisted tracks. - """ - - with app.app_context(): - db = get_db() - - populate_tracks(db) - - with db.scoped_session() as session: - # test as authed user matching owner - tracks = _get_tracks( - session, - { - "user_id": 1287289, - "authed_user_id": 1287289, - "offset": 0, - "limit": 10, - "sort": "date", - }, - ) - - assert len(tracks) == 8 - assert tracks[0]["track_id"] == 1 - assert tracks[1]["track_id"] == 11 - assert tracks[2]["track_id"] == 3 - assert tracks[3]["track_id"] == 5 - assert tracks[4]["track_id"] == 4 - assert tracks[5]["track_id"] == 2 - - # test as authed user managing owner - tracks = _get_tracks( - session, - { - "user_id": 1287289, - "current_user_id": 1287289, - "authed_user_id": 4, - "offset": 0, - "limit": 10, - "sort": "date", - }, - ) - - assert len(tracks) == 8 - assert tracks[0]["track_id"] == 1 - assert tracks[1]["track_id"] == 11 - assert tracks[2]["track_id"] == 3 - assert tracks[3]["track_id"] == 5 - assert tracks[4]["track_id"] == 4 - assert tracks[5]["track_id"] == 2 - - -def test_get_tracks_with_pinned_track(app): - """ - Test getting tracks for a user with a pinned track. The - pinned track should be the first result, with all other tracks - sorted according to the sort parameter. - """ - with app.app_context(): - db = get_db() - - populate_tracks(db) - - with db.scoped_session() as session: - tracks = _get_tracks( - session, {"user_id": 5, "offset": 0, "limit": 10, "sort": "date"} - ) - - assert len(tracks) == 3 - assert tracks[0]["track_id"] == 12 - assert tracks[1]["track_id"] == 13 - assert tracks[2]["track_id"] == 14 - - -def test_get_tracks_with_pinned_track_and_sort_method(app): - """ - Test getting tracks for a user with a pinned track. All tracks - should be sorted according the sort method. The pinned track is - not necessarily the first result. - """ - with app.app_context(): - db = get_db() - - populate_tracks(db) - - with db.scoped_session() as session: - tracks = _get_tracks( - session, - { - "user_id": 5, - "offset": 0, - "limit": 10, - "sort_method": SortMethod.release_date, - "sort_direction": SortDirection.desc, - }, - ) - - assert len(tracks) == 3 - assert tracks[0]["track_id"] == 13 - assert tracks[1]["track_id"] == 12 - assert tracks[2]["track_id"] == 14 - - -def test_get_track_by_route(app): - """Test getting track by user handle and slug for route resolution""" - with app.app_context(): - db = get_db() - - populate_tracks(db) - - with db.scoped_session() as session: - tracks = _get_tracks( - session, - { - "routes": [{"owner_id": 1287289, "slug": "track-1"}], - "offset": 0, - "limit": 10, - }, - ) - - assert len(tracks) == 1, "track-1 is found for some-test-user" - assert tracks[0]["owner_id"] == 1287289 - assert tracks[0]["permalink"] == "/some-test-user/track-1" - - tracks = _get_tracks( - session, - { - "routes": [{"owner_id": 4, "slug": "track-1"}], - "offset": 0, - "limit": 10, - }, - ) - assert len(tracks) == 1, "track-1 is still found for some-other-user" - assert tracks[0]["owner_id"] == 4 - assert tracks[0]["permalink"] == "/some-other-user/track-1" - - # Get an unlisted track - tracks = _get_tracks( - session, - { - "routes": [{"owner_id": 4, "slug": "hidden-track"}], - "offset": 0, - "limit": 10, - }, - ) - assert len(tracks) == 1 - assert tracks[0]["owner_id"] == 4 - assert tracks[0]["permalink"] == "/some-other-user/hidden-track" - - # Make sure unlisted tracks are hidden without slug - tracks = _get_tracks( - session, - {"user_id": 4, "id": [9], "offset": 0, "limit": 10}, - ) - assert len(tracks) == 0 - - -def test_get_remixable_tracks(app): - with app.app_context(): - db = get_db() - - populate_tracks(db) - populate_mock_db( - db, - { - "remixes": [ - {"parent_track_id": 9, "child_track_id": 1}, - {"parent_track_id": 8, "child_track_id": 1}, - ], - "stems": [ - {"parent_track_id": 7, "child_track_id": 1}, - {"parent_track_id": 6, "child_track_id": 1}, - # Verify that tracks with deleted stems are not returned - {"parent_track_id": 5, "child_track_id": 10}, - ], - "saves": [{"user_id": 4, "save_item_id": 1}], - "reposts": [{"user_id": 4, "repost_item_id": 1}], - }, - ) - - tracks = get_remixable_tracks({"with_users": True}) - assert len(tracks) == 2 - assert tracks[0]["user"] - - -def test_get_remixes_of(app): - with app.app_context(): - db = get_db() - - populate_tracks(db) - populate_mock_db( - db, - { - "remixes": [ - {"parent_track_id": 1, "child_track_id": 2}, - {"parent_track_id": 1, "child_track_id": 3}, - {"parent_track_id": 1, "child_track_id": 4}, - ], - "aggregate_plays": [ - {"play_item_id": 2, "count": 100}, - {"play_item_id": 4, "count": 50}, - {"play_item_id": 3, "count": 2}, - ], - "saves": [ - {"user_id": 1287289, "save_item_id": 2}, - ], - "reposts": [ - {"user_id": 1287289, "repost_item_id": 4}, - ], - "events": [ - { - "user_id": 1287289, - "entity_id": 1, - "created_at": datetime(2018, 5, 19), - "end_date": datetime(2018, 5, 20), - } - ], - }, - ) - - tracks = get_remixes_of({"track_id": 1, "sort_method": "plays"})["tracks"] - assert len(tracks) == 3 - assert tracks[0]["track_id"] == 2 - assert tracks[1]["track_id"] == 4 - assert tracks[2]["track_id"] == 3 - - tracks = get_remixes_of({"track_id": 1, "only_cosigns": True})["tracks"] - assert len(tracks) == 2 - assert tracks[0]["track_id"] == 4 - assert tracks[1]["track_id"] == 2 - - tracks = get_remixes_of({"track_id": 1, "only_contest_entries": True})["tracks"] - assert len(tracks) == 1 - assert tracks[0]["track_id"] == 4 - - -def test_get_remixes_of_with_multiple_events(app): - """Test that get_remixes_of doesn't return duplicate tracks when there are multiple active events for the same track. - Note that this should not happen, but we've encountered this bug before so worth adding a test case. - """ - with app.app_context(): - db = get_db() - - populate_tracks(db) - populate_mock_db( - db, - { - "tracks": [ - { - "track_id": 2, - "title": "remix track 2", - "owner_id": 1287289, - "created_at": datetime( - 2018, 5, 10 - ), # Outside contest period (before 2018-05-15) - "remix_of": {"tracks": [{"parent_track_id": 1}]}, - }, - { - "track_id": 3, - "title": "remix track 3", - "owner_id": 1287289, - "created_at": datetime(2020, 5, 17), - "remix_of": {"tracks": [{"parent_track_id": 1}]}, - }, - { - "track_id": 4, - "title": "remix track 4", - "owner_id": 1287289, - "created_at": datetime( - 2018, 5, 20 - ), # Within the contest period 2018-05-15 to 2018-05-30 - "remix_of": {"tracks": [{"parent_track_id": 1}]}, - }, - ], - "remixes": [ - {"parent_track_id": 1, "child_track_id": 2}, - {"parent_track_id": 1, "child_track_id": 3}, - {"parent_track_id": 1, "child_track_id": 4}, - ], - "aggregate_plays": [ - {"play_item_id": 2, "count": 100}, - {"play_item_id": 4, "count": 50}, - {"play_item_id": 3, "count": 2}, - ], - "saves": [ - {"user_id": 1287289, "save_item_id": 2}, - ], - "reposts": [ - {"user_id": 1287289, "repost_item_id": 4}, - ], - # Multiple active events for the same track - this would cause duplicates without DISTINCT ON fix - "events": [ - { - "user_id": 1287289, - "entity_id": 1, - "event_type": "remix_contest", - "is_deleted": False, - "created_at": datetime(2018, 5, 15), - "end_date": datetime(2018, 5, 30), - }, - { - "user_id": 1287289, - "entity_id": 1, - "event_type": "remix_contest", - "is_deleted": False, - "created_at": datetime(2018, 5, 15), - "end_date": datetime(2018, 5, 30), - }, - { - "user_id": 1287289, - "entity_id": 1, - "event_type": "remix_contest", - "is_deleted": False, - "created_at": datetime(2018, 5, 15), - "end_date": datetime(2018, 5, 30), - }, - ], - }, - ) - - # Test basic query - should return 3 unique tracks, not 9 (3 tracks × 3 events) - tracks = get_remixes_of({"track_id": 1, "sort_method": "plays"})["tracks"] - assert ( - len(tracks) == 3 - ), f"Expected 3 unique tracks, got {len(tracks)} - possible duplication from multiple events" - assert tracks[0]["track_id"] == 2 - assert tracks[1]["track_id"] == 4 - assert tracks[2]["track_id"] == 3 - - # Test cosigns filter - should return 2 unique tracks, not 6 - tracks = get_remixes_of({"track_id": 1, "only_cosigns": True})["tracks"] - assert ( - len(tracks) == 2 - ), f"Expected 2 unique tracks with cosigns, got {len(tracks)} - possible duplication from multiple events" - assert tracks[0]["track_id"] == 4 - assert tracks[1]["track_id"] == 2 - - # Test contest entries filter - should return 1 unique track that falls within ANY contest period - # Track 4 (created 2018-05-19) falls within the second contest period (2018-05-19 to 2018-05-22) - tracks = get_remixes_of({"track_id": 1, "only_contest_entries": True})["tracks"] - assert ( - len(tracks) == 1 - ), f"Expected 1 track matching contest criteria, got {len(tracks)} - possible duplication from multiple events" - assert tracks[0]["track_id"] == 4 - - # Test that count in response is also correct (not inflated by duplicates) - response = get_remixes_of({"track_id": 1, "sort_method": "plays"}) - assert ( - response["count"] == 3 - ), f"Expected count of 3, got {response['count']} - count may be inflated by duplicates" diff --git a/packages/discovery-provider/integration_tests/queries/test_get_usdc_purchases.py b/packages/discovery-provider/integration_tests/queries/test_get_usdc_purchases.py deleted file mode 100644 index 8728ae7dbf2..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_usdc_purchases.py +++ /dev/null @@ -1,233 +0,0 @@ -from datetime import datetime - -from integration_tests.utils import populate_mock_db -from src.models.users.usdc_purchase import PurchaseType -from src.queries.get_usdc_purchases import get_usdc_purchases -from src.queries.query_helpers import PurchaseSortMethod, SortDirection -from src.utils.db_session import get_db - -test_entities = { - "users": [ - {"user_id": 10, "name": "a"}, - {"user_id": 20, "name": "c"}, - {"user_id": 30, "name": "b"}, - ], - "tracks": [ - { - "track_id": 1, - "title": "a", - "owner_id": 10, - "is_stream_gated": True, - "stream_conditions": { - "usdc_purchase": { - "price": 100, - "splits": {"some_user_bank": 1000000}, - } - }, - }, - { - "track_id": 2, - "title": "f", - "owner_id": 10, - "is_stream_gated": True, - "stream_conditions": { - "usdc_purchase": { - "price": 299, - "splits": {"some_user_bank": 2990000}, - } - }, - }, - { - "track_id": 3, - "title": "c", - "owner_id": 10, - "is_stream_gated": True, - "stream_conditions": { - "usdc_purchase": { - "price": 199, - "splits": {"some_user_bank": 1990000}, - } - }, - }, - { - "track_id": 4, - "title": "b", - "owner_id": 10, - "is_stream_gated": True, - "stream_conditions": { - "usdc_purchase": { - "price": 350, - "splits": {"some_user_bank": 3500000}, - } - }, - }, - { - "track_id": 5, - "title": "d", - "owner_id": 20, - "is_stream_gated": True, - "stream_conditions": { - "usdc_purchase": { - "price": 100, - "splits": {"some_user_bank": 1000000}, - } - }, - }, - { - "track_id": 6, - "title": "zzz", - "owner_id": 20, - "is_stream_gated": True, - "stream_conditions": { - "usdc_purchase": { - "price": 100, - "splits": {"some_user_bank": 1000000}, - } - }, - }, - ], - "usdc_purchases": [ - { - "slot": 5, - "buyer_user_id": 20, - "seller_user_id": 10, - "amount": 350, - "extra_amount": 0, - "content_type": PurchaseType.track, - "content_id": 4, - "created_at": datetime(2023, 8, 11), - "splits": [{"user_id": 10, "amount": 3500000, "percentage": 100}], - }, - { - "slot": 4, - "buyer_user_id": 20, - "seller_user_id": 10, - "amount": 199, - "extra_amount": 0, - "content_type": PurchaseType.track, - "content_id": 3, - "created_at": datetime(2023, 8, 10), - "splits": [{"user_id": 10, "amount": 1990000, "percentage": 100}], - }, - { - "slot": 8, - "buyer_user_id": 30, - "seller_user_id": 20, - "amount": 100, - "extra_amount": 0, - "content_type": PurchaseType.track, - "content_id": 5, - "created_at": datetime(2023, 8, 14), - "splits": [{"user_id": 20, "amount": 1000000, "percentage": 100}], - }, - { - "slot": 7, - "buyer_user_id": 30, - "seller_user_id": 10, - "amount": 199, - "extra_amount": 0, - "content_type": PurchaseType.track, - "content_id": 3, - "created_at": datetime(2023, 8, 13, 12), - "splits": [{"user_id": 10, "amount": 1990000, "percentage": 100}], - }, - { - "slot": 6, - "buyer_user_id": 30, - "seller_user_id": 10, - "amount": 299, - "extra_amount": 0, - "content_type": PurchaseType.track, - "content_id": 2, - "created_at": datetime(2023, 8, 12), - "splits": [{"user_id": 10, "amount": 2990000, "percentage": 100}], - }, - { - "slot": 9, - "buyer_user_id": 20, - "seller_user_id": 10, - "amount": 299, - "extra_amount": 0, - "content_type": PurchaseType.track, - "content_id": 2, - "created_at": datetime(2023, 8, 13), - "splits": [{"user_id": 10, "amount": 2990000, "percentage": 100}], - }, - ], -} - - -def test_get_purchases(app): - with app.app_context(): - db = get_db() - populate_mock_db(db, test_entities) - sales = get_usdc_purchases( - {"seller_user_id": 10, "sort_method": PurchaseSortMethod.date} - ) - assert len(sales) == 5 - assert sales[0]["content_id"] == 3 - assert sales[1]["content_id"] == 2 - assert not any(purchase["content_id"] == 1 for purchase in sales) - assert not any(purchase["content_id"] == 5 for purchase in sales) - assert all(purchase["seller_user_id"] == 10 for purchase in sales) - - purchases = get_usdc_purchases( - {"buyer_user_id": 20, "sort_method": PurchaseSortMethod.date} - ) - assert len(purchases) == 3 - assert all(purchase["buyer_user_id"] == 20 for purchase in purchases) - - specific_sale = get_usdc_purchases( - { - "content_ids": [3], - "content_type": PurchaseType.track, - "sort_method": PurchaseSortMethod.date, - } - ) - assert len(specific_sale) == 2 - assert specific_sale[0]["content_id"] == 3 - assert specific_sale[1]["content_id"] == 3 - - purchases_by_title_asc = get_usdc_purchases( - { - "content_type": PurchaseType.track, - "sort_method": PurchaseSortMethod.content_title, - "sort_direction": SortDirection.asc, - } - ) - - by_title_mapped = [ - next( - track["title"] - for track in test_entities["tracks"] - if track["track_id"] == purchase["content_id"] - ) - for purchase in purchases_by_title_asc - ] - assert by_title_mapped == sorted(by_title_mapped) - - purchases_by_artist_desc = get_usdc_purchases( - { - "content_type": PurchaseType.track, - "sort_method": PurchaseSortMethod.artist_name, - "sort_direction": SortDirection.desc, - } - ) - - by_artist_owner_ids = [ - next( - track["owner_id"] - for track in test_entities["tracks"] - if track["track_id"] == purchase["content_id"] - ) - for purchase in purchases_by_artist_desc - ] - by_artist_mapped = [ - next( - user["name"] - for user in test_entities["users"] - if user["user_id"] == owner_id - ) - for owner_id in by_artist_owner_ids - ] - assert by_artist_mapped == list(reversed(sorted(by_artist_mapped))) diff --git a/packages/discovery-provider/integration_tests/queries/test_get_usdc_transactions_history.py b/packages/discovery-provider/integration_tests/queries/test_get_usdc_transactions_history.py deleted file mode 100644 index 5c9fdf47060..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_usdc_transactions_history.py +++ /dev/null @@ -1,146 +0,0 @@ -import pytest - -from integration_tests.utils import populate_mock_db -from src.models.users.usdc_transactions_history import USDCTransactionMethod -from src.queries.get_usdc_transactions_history import ( - GetUSDCTransactionsArgs, - GetUSDCTransactionsCountArgs, - get_usdc_transactions_history, - get_usdc_transactions_history_count, -) -from src.queries.query_helpers import SortDirection, TransactionSortMethod -from src.utils.db_session import get_db - -user1_id = 10 -user1_name = "user1" -user1_eth_address = "0xe66402f9a6714a874a539fb1689b870dd271dfb2" -user1_user_bank_address = "7G1angvMtUZLFMyrMDGj7bxsduB4bjLD7VXRR7N4FXqe" - -external_recipient_address = "3nmLmEzwBBjERiV9UU1a4JXzESwDjrKZdSjP1KG4M9Mc" -internal_recipient_address = "HXLN9UWwAjMPgHaFZDfgabT79SmLSdTeu2fUha2xHz9W" - - -@pytest.fixture -def test_entities(): - return { - "users": [ - {"user_id": 1, "handle": "user1", "wallet": user1_eth_address}, - ], - "usdc_user_bank_accounts": [ - { - "signature": "unused1", - "ethereum_address": user1_eth_address, - "bank_account": user1_user_bank_address, - } - ], - "usdc_transactions_history": [ - { - "user_bank": user1_user_bank_address, - "signature": "manualTransferSignature", - "transaction_type": "transfer", - "method": "send", - "change": -1000000, - "balance": 1000000, - "tx_metadata": external_recipient_address, - }, - { - "user_bank": user1_user_bank_address, - "signature": "firstPrepareWithdrawalSignature", - "transaction_type": "prepare_withdrawal", - "method": "send", - "change": -1000000, - "balance": 0, - "tx_metadata": internal_recipient_address, - }, - { - "user_bank": user1_user_bank_address, - "signature": "recoverWithdrawalSignature", - "transaction_type": "recover_withdrawal", - "method": "send", - "change": 1000000, - "balance": 1000000, - "tx_metadata": internal_recipient_address, - }, - { - "user_bank": user1_user_bank_address, - "signature": "secondPrepareWithdrawalSignature", - "transaction_type": "prepare_withdrawal", - "method": "send", - "change": -1000000, - "balance": 0, - "tx_metadata": internal_recipient_address, - }, - { - "user_bank": user1_user_bank_address, - "signature": "withdrawalSignature", - "transaction_type": "withdrawal", - "method": "send", - "change": -1000000, - "balance": 0, - "tx_metadata": external_recipient_address, - }, - ], - } - - -def test_get_usdc_transactions_history_default_excludes_system_transactions( - app, test_entities -): - with app.test_request_context( - # Request context and args are required for passing - # pagination info into paginate_query inside the query function - data={"limit": 10, "offset": 0}, - ): - db = get_db() - populate_mock_db(db, test_entities) - transactions = get_usdc_transactions_history( - GetUSDCTransactionsArgs( - user_id=1, - transaction_method=USDCTransactionMethod.send, - sort_method=TransactionSortMethod.date, - sort_direction=SortDirection.asc, - ) - ) - count = get_usdc_transactions_history_count( - GetUSDCTransactionsCountArgs( - user_id=1, - transaction_method=USDCTransactionMethod.send, - ) - ) - assert len(transactions) == 2 - assert count == 2 - assert transactions[0]["signature"] == "manualTransferSignature" - assert transactions[1]["signature"] == "withdrawalSignature" - - -def test_get_usdc_transactions_including_system_transactions(app, test_entities): - with app.test_request_context( - # Request context and args are required for passing - # pagination info into paginate_query inside the query function - data={"limit": 10, "offset": 0}, - ): - db = get_db() - populate_mock_db(db, test_entities) - transactions = get_usdc_transactions_history( - GetUSDCTransactionsArgs( - user_id=1, - transaction_method=USDCTransactionMethod.send, - sort_method=TransactionSortMethod.date, - sort_direction=SortDirection.asc, - include_system_transactions=True, - ) - ) - count = get_usdc_transactions_history_count( - GetUSDCTransactionsCountArgs( - user_id=1, - transaction_method=USDCTransactionMethod.send, - include_system_transactions=True, - ) - ) - assert len(transactions) == 5 - assert count == 5 - assert transactions[0]["signature"] == "manualTransferSignature" - assert transactions[1]["signature"] == "firstPrepareWithdrawalSignature" - assert transactions[2]["signature"] == "recoverWithdrawalSignature" - assert transactions[3]["signature"] == "secondPrepareWithdrawalSignature" - assert transactions[4]["signature"] == "withdrawalSignature" diff --git a/packages/discovery-provider/integration_tests/queries/test_get_user_listen_counts_monthly.py b/packages/discovery-provider/integration_tests/queries/test_get_user_listen_counts_monthly.py deleted file mode 100644 index 8f6303f7af3..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_user_listen_counts_monthly.py +++ /dev/null @@ -1,73 +0,0 @@ -from datetime import datetime - -from integration_tests.utils import populate_mock_db -from src.queries.get_user_listen_counts_monthly import ( - GetUserListenCountsMonthlyArgs, - get_user_listen_counts_monthly, -) -from src.utils.db_session import get_db - -test_entities = { - "aggregate_monthly_plays": [ - { - "play_item_id": 1, - "timestamp": "2022-01-01", - "count": 7, - }, - { - "play_item_id": 2, - "timestamp": "2022-01-01", - "count": 2, - }, - { - "play_item_id": 1, - "timestamp": "2021-12-31", - "count": 7, - }, - { - "play_item_id": 1, - "timestamp": "2023-01-01", - "count": 2, - }, - { - "play_item_id": 4, - "timestamp": "2022-02-01", - "count": 10, - }, - ], - "tracks": [ - {"track_id": 1, "title": "track 1", "owner_id": 1}, - {"track_id": 4, "title": "track 1", "owner_id": 1}, - {"track_id": 2, "title": "track 2", "owner_id": 2}, - ], - "users": [ - {"user_id": 1, "handle": "user-1"}, - {"user_id": 2, "handle": "user-2"}, - ], -} - - -def test_get_user_listen_counts_monthly_query(app): - """Tests happy path of getting user listen counts""" - with app.app_context(): - db = get_db() - - populate_mock_db(db, test_entities) - - with db.scoped_session(): - user_listen_counts_monthly = get_user_listen_counts_monthly( - GetUserListenCountsMonthlyArgs( - user_id=1, - start_time="2022-01-01", - end_time="2023-01-01", - ), - ) - assert len(user_listen_counts_monthly) == 2 - # User 1 only owns track ids 1 and 4 - for listen_count in user_listen_counts_monthly: - listen_count_timestamp = datetime.combine( - listen_count["timestamp"], datetime.min.time() - ) - assert listen_count["play_item_id"] in [1, 4] - assert listen_count_timestamp >= datetime.strptime("2022-01-01", "%Y-%m-%d") - assert listen_count_timestamp < datetime.strptime("2023-01-01", "%Y-%m-%d") diff --git a/packages/discovery-provider/integration_tests/queries/test_get_user_listening_history.py b/packages/discovery-provider/integration_tests/queries/test_get_user_listening_history.py deleted file mode 100644 index 796b0b103f5..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_user_listening_history.py +++ /dev/null @@ -1,428 +0,0 @@ -from datetime import datetime, timedelta - -from integration_tests.utils import populate_mock_db -from src.queries import response_name_constants -from src.queries.get_top_listeners_for_track import _get_top_listeners_for_track -from src.queries.get_user_listening_history import ( - GetUserListeningHistoryArgs, - _get_user_listening_history, -) -from src.queries.query_helpers import SortDirection, SortMethod -from src.tasks.user_listening_history.index_user_listening_history import ( - _index_user_listening_history, -) -from src.utils.db_session import get_db - -TIMESTAMP = datetime(2011, 1, 1) - -test_entities = { - "user_listening_history": [ - { - "user_id": 1, - "listening_history": [ - {"timestamp": str(TIMESTAMP), "track_id": 3}, - {"timestamp": str(TIMESTAMP), "track_id": 4}, - ], - } - ], - "plays": [ - {"user_id": 1, "item_id": 1, "created_at": TIMESTAMP + timedelta(minutes=1)}, - {"user_id": 1, "item_id": 1, "created_at": TIMESTAMP - timedelta(minutes=1)}, - {"user_id": 1, "item_id": 1, "created_at": TIMESTAMP - timedelta(minutes=1)}, - {"user_id": 1, "item_id": 2, "created_at": TIMESTAMP + timedelta(minutes=3)}, - { - "user_id": 1, - "item_id": 1, - "created_at": TIMESTAMP + timedelta(minutes=2), - }, # duplicate play - {"user_id": 1, "item_id": 3, "created_at": TIMESTAMP + timedelta(minutes=4)}, - {"user_id": 2, "item_id": 2, "created_at": TIMESTAMP}, - ], - "tracks": [ - {"track_id": 1, "title": "track 1", "owner_id": 1}, - {"track_id": 2, "title": "track 2", "owner_id": 2}, - {"track_id": 3, "title": "track 3", "owner_id": 3}, - {"track_id": 4, "title": "track 4", "owner_id": 3}, - ], - "users": [ - {"user_id": 1, "handle": "user-1"}, - {"user_id": 2, "handle": "user-2"}, - {"user_id": 3, "handle": "user-3"}, - ], -} - - -def test_get_user_listening_history_multiple_plays(app): - """Tests listening history from user with multiple plays""" - with app.app_context(): - db = get_db() - - populate_mock_db(db, test_entities) - - with db.scoped_session() as session: - _index_user_listening_history(session) - - track_history = _get_user_listening_history( - session, - GetUserListeningHistoryArgs( - user_id=1, - limit=10, - offset=0, - query=None, - sort_method=None, - sort_direction=None, - ), - ) - - assert len(track_history) == 4 - assert ( - track_history[0][response_name_constants.user][response_name_constants.balance] - is not None - ) - assert track_history[0][response_name_constants.track_id] == 3 - assert track_history[0][response_name_constants.activity_timestamp] == str( - TIMESTAMP + timedelta(minutes=4) - ) - assert ( - track_history[1][response_name_constants.user][response_name_constants.balance] - is not None - ) - assert track_history[1][response_name_constants.track_id] == 2 - assert track_history[1][response_name_constants.activity_timestamp] == str( - TIMESTAMP + timedelta(minutes=3) - ) - assert ( - track_history[2][response_name_constants.user][response_name_constants.balance] - is not None - ) - assert track_history[2][response_name_constants.track_id] == 1 - assert track_history[2][response_name_constants.activity_timestamp] == str( - TIMESTAMP + timedelta(minutes=2) - ) - - -def test_get_user_listening_history_no_plays(app): - """Tests a listening history with no plays""" - with app.app_context(): - db = get_db() - - populate_mock_db(db, test_entities) - - with db.scoped_session() as session: - _index_user_listening_history(session) - - track_history = _get_user_listening_history( - session, - GetUserListeningHistoryArgs( - user_id=3, - limit=10, - offset=0, - query=None, - sort_method=None, - sort_direction=None, - ), - ) - - assert len(track_history) == 0 - - -def test_get_user_listening_history_single_play(app): - """Tests a listening history with a single play""" - with app.app_context(): - db = get_db() - - populate_mock_db(db, test_entities) - - with db.scoped_session() as session: - _index_user_listening_history(session) - - track_history = _get_user_listening_history( - session, - GetUserListeningHistoryArgs( - user_id=2, - limit=10, - offset=0, - query=None, - sort_method=None, - sort_direction=None, - ), - ) - - assert len(track_history) == 1 - assert ( - track_history[0][response_name_constants.user][response_name_constants.balance] - is not None - ) - assert track_history[0][response_name_constants.track_id] == 2 - assert track_history[0][response_name_constants.activity_timestamp] == str( - TIMESTAMP - ) - - -def test_get_user_listening_history_pagination(app): - """Tests a track history that's limit bounded""" - with app.app_context(): - db = get_db() - - populate_mock_db(db, test_entities) - - with db.scoped_session() as session: - _index_user_listening_history(session) - - track_history = _get_user_listening_history( - session, - GetUserListeningHistoryArgs( - user_id=1, - limit=1, - offset=1, - query=None, - sort_method=None, - sort_direction=None, - ), - ) - - assert len(track_history) == 1 - assert ( - track_history[0][response_name_constants.user][response_name_constants.balance] - is not None - ) - assert track_history[0][response_name_constants.track_id] == 2 - assert track_history[0][response_name_constants.activity_timestamp] == str( - TIMESTAMP + timedelta(minutes=3) - ) - - -def test_get_user_listening_history_with_query(app): - """Tests listening history from user with a query""" - with app.app_context(): - db = get_db() - - populate_mock_db(db, test_entities) - - with db.scoped_session() as session: - _index_user_listening_history(session) - - track_history = _get_user_listening_history( - session, - GetUserListeningHistoryArgs( - user_id=1, - limit=10, - offset=0, - query="track 2", - sort_method=None, - sort_direction=None, - ), - ) - - # We should only get one history item back - assert len(track_history) == 1 - - assert track_history[0][response_name_constants.track_id] == 2 - assert track_history[0][response_name_constants.activity_timestamp] == str( - TIMESTAMP + timedelta(minutes=3) - ) - - -def test_get_user_listening_history_custom_sort(app): - """Tests listening history from user with multiple plays""" - with app.app_context(): - db = get_db() - - populate_mock_db(db, test_entities) - - with db.scoped_session() as session: - _index_user_listening_history(session) - - track_history = _get_user_listening_history( - session, - GetUserListeningHistoryArgs( - user_id=1, - limit=10, - offset=0, - query=None, - sort_method=SortMethod.title, - sort_direction=SortDirection.asc, - ), - ) - - assert len(track_history) == 4 - assert ( - track_history[2][response_name_constants.user][response_name_constants.balance] - is not None - ) - assert track_history[2][response_name_constants.track_id] == 3 - assert track_history[2][response_name_constants.activity_timestamp] == str( - TIMESTAMP + timedelta(minutes=4) - ) - assert ( - track_history[1][response_name_constants.user][response_name_constants.balance] - is not None - ) - assert track_history[1][response_name_constants.track_id] == 2 - assert track_history[1][response_name_constants.activity_timestamp] == str( - TIMESTAMP + timedelta(minutes=3) - ) - assert ( - track_history[0][response_name_constants.user][response_name_constants.balance] - is not None - ) - assert track_history[0][response_name_constants.track_id] == 1 - assert track_history[0][response_name_constants.activity_timestamp] == str( - TIMESTAMP + timedelta(minutes=2) - ) - - -def test_get_user_listening_history_sort_by_most_listens(app): - """Tests listening history from user with multiple plays""" - with app.app_context(): - db = get_db() - - populate_mock_db(db, test_entities) - - with db.scoped_session() as session: - _index_user_listening_history(session) - - track_history = _get_user_listening_history( - session, - GetUserListeningHistoryArgs( - user_id=1, - limit=10, - offset=0, - query=None, - sort_method=SortMethod.most_listens_by_user, - sort_direction=SortDirection.asc, - ), - ) - - assert len(track_history) == 4 - assert_track_history(track_history[0], 1, TIMESTAMP + timedelta(minutes=2)) - assert_track_history(track_history[1], 3, TIMESTAMP + timedelta(minutes=4)) - assert_track_history(track_history[2], 2, TIMESTAMP + timedelta(minutes=3)) - assert_track_history(track_history[3], 4, TIMESTAMP) - - -def assert_track_history(track_history, track_id, activity_timestamp): - assert ( - track_history[response_name_constants.user][response_name_constants.balance] - is not None - ) - assert track_history[response_name_constants.track_id] == track_id - assert track_history[response_name_constants.activity_timestamp] == str( - activity_timestamp - ) - - -def test_track_top_listeners(app): - """Tests a listening history with no plays""" - with app.app_context(): - db = get_db() - - populate_mock_db(db, test_entities) - - with db.scoped_session() as session: - top_listeners = _get_top_listeners_for_track(session, {"track_id": 2}) - assert len(top_listeners) == 2 - assert top_listeners[0]["user"]["user_id"] == 1 - assert top_listeners[0]["count"] == 1 - assert top_listeners[1]["user"]["user_id"] == 2 - assert top_listeners[1]["count"] == 1 - - top_listeners = _get_top_listeners_for_track(session, {"track_id": 1}) - assert len(top_listeners) == 1 - assert top_listeners[0]["user"]["user_id"] == 1 - assert top_listeners[0]["count"] == 2 - - -def test_get_user_listening_history_filter_deleted_owners(app): - """Tests that tracks with deactivated owners are filtered out of listening history""" - with app.app_context(): - db = get_db() - - test_entities_with_deleted = { - "user_listening_history": [ - { - "user_id": 1, - "listening_history": [ - {"timestamp": str(TIMESTAMP), "track_id": 1}, - {"timestamp": str(TIMESTAMP), "track_id": 2}, - ], - } - ], - "tracks": [ - {"track_id": 1, "title": "track 1", "owner_id": 1}, - {"track_id": 2, "title": "track 2", "owner_id": 2}, - ], - "users": [ - {"user_id": 1, "handle": "user-1", "is_deactivated": False}, - { - "user_id": 2, - "handle": "user-2", - "is_deactivated": True, - }, # Deactivated user - ], - } - - populate_mock_db(db, test_entities_with_deleted) - - with db.scoped_session() as session: - _index_user_listening_history(session) - - track_history = _get_user_listening_history( - session, - GetUserListeningHistoryArgs( - user_id=1, - limit=10, - offset=0, - query=None, - sort_method=None, - sort_direction=None, - ), - ) - - # Should only return track 1 since track 2's owner is deactivated - assert len(track_history) == 1 - assert track_history[0][response_name_constants.track_id] == 1 - assert track_history[0][response_name_constants.activity_timestamp] == str( - TIMESTAMP - ) - - -def test_get_user_listening_history_filter_deleted_tracks(app): - """Tests that deleted tracks are filtered out of listening history""" - with app.app_context(): - db = get_db() - - # Use main test_entities but modify track 1 to be deleted - modified_test_entities = test_entities.copy() - modified_test_entities["tracks"] = [ - {"track_id": 1, "title": "track 1", "owner_id": 1, "is_delete": True}, - *test_entities["tracks"][1:], - ] - - populate_mock_db(db, modified_test_entities) - - with db.scoped_session() as session: - _index_user_listening_history(session) - - track_history = _get_user_listening_history( - session, - GetUserListeningHistoryArgs( - user_id=1, - limit=10, - offset=0, - query=None, - sort_method=None, - sort_direction=None, - ), - ) - - # Should not return track 1 since it is deleted - assert len(track_history) == 3 - assert track_history[0][response_name_constants.track_id] == 3 - assert track_history[0][response_name_constants.activity_timestamp] == str( - TIMESTAMP + timedelta(minutes=4) - ) - assert track_history[1][response_name_constants.track_id] == 2 - assert track_history[1][response_name_constants.activity_timestamp] == str( - TIMESTAMP + timedelta(minutes=3) - ) diff --git a/packages/discovery-provider/integration_tests/queries/test_get_user_signals.py b/packages/discovery-provider/integration_tests/queries/test_get_user_signals.py deleted file mode 100644 index d806dca40c6..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_user_signals.py +++ /dev/null @@ -1,129 +0,0 @@ -from integration_tests.utils import populate_mock_db -from src.queries.get_user_signals import _get_user_signals -from src.utils.db_session import get_db - - -def make_user( - user_id, - handle, - wallet, - profile_picture=None, - profile_picture_sizes=None, - cover_photo=None, - cover_photo_sizes=None, -): - return { - "user_id": user_id, - "handle": handle, - "wallet": wallet, - "profile_picture": profile_picture, - "profile_picture_sizes": profile_picture_sizes, - "cover_photo": cover_photo, - "cover_photo_sizes": cover_photo_sizes, - } - - -def make_follow(follower_user_id, followee_user_id): - return {"follower_user_id": follower_user_id, "followee_user_id": followee_user_id} - - -def test_get_user_signals(app): - with app.app_context(): - db = get_db() - - test_entities = { - "users": [ - make_user(1, "user1", "wallet1"), - make_user(2, "user2", "wallet2"), - make_user(3, "user3", "wallet3"), - make_user(4, "user4", "wallet4"), - make_user(5, "user5", "wallet5"), - make_user( - 6, - "user6", - "wallet6", - profile_picture="Qm0123456789abcdef0123456789abcdef0123456789ab", - ), - make_user( - 7, - "user7", - "wallet7", - profile_picture_sizes="Qm0123456789abcdef0123456789abcdef0123456789ab", - ), - make_user( - 8, - "user8", - "wallet8", - cover_photo="Qm0123456789abcdef0123456789abcdef0123456789ab", - ), - make_user( - 9, - "user9", - "wallet9", - cover_photo_sizes="Qm0123456789abcdef0123456789abcdef0123456789ab", - ), - make_user( - 10, - "user10", - "wallet10", - profile_picture="Qm0123456789abcdef0123456789abcdef0123456789ab", - cover_photo="Qm0123456789abcdef0123456789abcdef0123456789cd", - ), - ], - "follows": [ - make_follow(2, 1), - make_follow(3, 1), - make_follow(5, 1), - make_follow(1, 5), - make_follow(2, 6), - make_follow(3, 7), - make_follow(4, 8), - make_follow(5, 9), - make_follow(10, 4), - ], - } - - populate_mock_db(db, test_entities) - - with db.scoped_session() as session: - user_signals = _get_user_signals(session, "user1") - assert user_signals["num_followers"] == 3 - assert user_signals["num_following"] == 1 - assert user_signals["has_profile_picture"] == False - assert user_signals["has_cover_photo"] == False - assert user_signals["wallet"] == "wallet1" - - user_signals = _get_user_signals(session, "user6") - assert user_signals["num_followers"] == 1 - assert user_signals["num_following"] == 0 - assert user_signals["has_profile_picture"] == True - assert user_signals["has_cover_photo"] == False - assert user_signals["wallet"] == "wallet6" - - user_signals = _get_user_signals(session, "user7") - assert user_signals["num_followers"] == 1 - assert user_signals["num_following"] == 0 - assert user_signals["has_profile_picture"] == True - assert user_signals["has_cover_photo"] == False - assert user_signals["wallet"] == "wallet7" - - user_signals = _get_user_signals(session, "user8") - assert user_signals["num_followers"] == 1 - assert user_signals["num_following"] == 0 - assert user_signals["has_profile_picture"] == False - assert user_signals["has_cover_photo"] == True - assert user_signals["wallet"] == "wallet8" - - user_signals = _get_user_signals(session, "user9") - assert user_signals["num_followers"] == 1 - assert user_signals["num_following"] == 0 - assert user_signals["has_profile_picture"] == False - assert user_signals["has_cover_photo"] == True - assert user_signals["wallet"] == "wallet9" - - user_signals = _get_user_signals(session, "user10") - assert user_signals["num_followers"] == 0 - assert user_signals["num_following"] == 1 - assert user_signals["has_profile_picture"] == True - assert user_signals["has_cover_photo"] == True - assert user_signals["wallet"] == "wallet10" diff --git a/packages/discovery-provider/integration_tests/queries/test_get_users_account.py b/packages/discovery-provider/integration_tests/queries/test_get_users_account.py deleted file mode 100644 index d964a4fa094..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_get_users_account.py +++ /dev/null @@ -1,118 +0,0 @@ -import pytest - -from integration_tests.utils import populate_mock_db -from src import exceptions -from src.queries.get_users_account import GetAccountArgs, get_account -from src.utils.db_session import get_db - -USER_1_WALLET = "0x0000000000000000000000000000000000000001" -USER_2_WALLET = "0x0000000000000000000000000000000000000002" -USER_3_WALLET = "0x0000000000000000000000000000000000000003" - - -def populate_db(db): - test_entities = { - "users": [ - {"user_id": 1, "wallet": USER_1_WALLET, "handle": "user_1"}, - {"user_id": 2, "wallet": USER_2_WALLET, "handle": "user_2"}, - {"user_id": 3, "wallet": USER_3_WALLET, "handle": "user_3"}, - ], - "playlists": [ - { - "playlist_id": 1, - "playlist_owner_id": 1, - "playlist_name": "playlist 1", - }, - { - "playlist_id": 2, - "playlist_owner_id": 1, - "playlist_name": "album 1", - "is_album": True, - }, - { - "playlist_id": 3, - "playlist_owner_id": 1, - "playlist_name": "playlist 1", - "is_private": True, - }, - ], - "grants": [ - # Grant for user2 to manage user1 - { - "user_id": 1, - "grantee_address": USER_2_WALLET, - "is_approved": True, - "is_revoked": False, - } - ], - } - populate_mock_db(db, test_entities) - - -def test_get_account(app): - """Test getting account with a valid wallet""" - - with app.app_context(): - db = get_db() - - populate_db(db) - - account = get_account(GetAccountArgs(wallet=USER_1_WALLET, authed_user_id=1)) - user = account["user"] - playlists = account["playlists"] - - assert user["wallet"] == USER_1_WALLET - assert user["user_id"] == 1 - assert len(playlists) == 3 - - -def test_get_account_invalid_wallet_length(app): - with app.app_context(): - db = get_db() - - populate_db(db) - with pytest.raises(exceptions.ArgumentError): - get_account(GetAccountArgs(wallet="0x123", authed_user_id=1)) - - -def test_get_account_not_found(app): - """Test attempting to fetch nonexistent user account""" - - with app.app_context(): - db = get_db() - - populate_db(db) - account = get_account( - GetAccountArgs( - wallet="0x0000000000000000000000000000000000000009", authed_user_id=1 - ) - ) - assert account is None - - -def test_get_account_as_manager(app): - """Test getting account as a different user with manager access""" - - with app.app_context(): - db = get_db() - - populate_db(db) - - account = get_account(GetAccountArgs(wallet=USER_1_WALLET, authed_user_id=2)) - user = account["user"] - playlists = account["playlists"] - - assert user["wallet"] == USER_1_WALLET - assert user["user_id"] == 1 - assert len(playlists) == 3 - - -def test_get_account_forbidden(app): - """Test getting account as a different user without manager access""" - - with app.app_context(): - db = get_db() - - populate_db(db) - with pytest.raises(exceptions.PermissionError): - get_account(GetAccountArgs(wallet=USER_1_WALLET, authed_user_id=3)) diff --git a/packages/discovery-provider/integration_tests/queries/test_mutual_follows.py b/packages/discovery-provider/integration_tests/queries/test_mutual_follows.py deleted file mode 100644 index 360d6571ad1..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_mutual_follows.py +++ /dev/null @@ -1,43 +0,0 @@ -import logging - -import pytest - -from integration_tests.utils import populate_mock_db -from src.queries.get_follow_intersection_users import get_follow_intersection_users -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - - -@pytest.fixture -def test_entities(): - return { - "users": [ - {"user_id": 51, "handle": "audius"}, - {"user_id": 1, "handle": "ray"}, - {"user_id": 2, "handle": "dave"}, - {"user_id": 3, "handle": "rando"}, - ], - "follows": [ - {"follower_user_id": 1, "followee_user_id": 51}, - {"follower_user_id": 1, "followee_user_id": 2}, - {"follower_user_id": 2, "followee_user_id": 51}, - {"follower_user_id": 3, "followee_user_id": 51}, - ], - } - - -def test_mutual_followers(app, test_entities): - # Add pagination variables to only retrieve first tip result - with app.test_request_context("?limit=10&offset=0"): - db = get_db() - populate_mock_db(db, test_entities) - with db.scoped_session(): - # Test first without filtering, should get the most recent tip - users = get_follow_intersection_users( - {"my_id": 1, "other_user_id": 51, "limit": 10, "offset": 0} - ) - - assert len(users) == 1 - assert users[0]["user_id"] == 2 - assert users[0]["does_current_user_follow"] == True diff --git a/packages/discovery-provider/integration_tests/queries/test_notifications/test_announcement_notification.py b/packages/discovery-provider/integration_tests/queries/test_notifications/test_announcement_notification.py deleted file mode 100644 index b40779d5e86..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_notifications/test_announcement_notification.py +++ /dev/null @@ -1,78 +0,0 @@ -import logging -from datetime import datetime, timedelta - -from integration_tests.utils import populate_mock_db -from src.queries.get_notifications import ( - get_notifications, - get_unread_notification_count, -) -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - - -def test_get_announcement_notifications(app): - with app.app_context(): - db_mock = get_db() - - test_entities = { - "users": [{"user_id": i + 1} for i in range(20)], - "notification": [ - { - "type": "announcement", - "group_id": "fake_group_id", - "specifier": "1", - "data": {"title": "my title", "short_description": "my desc"}, - "user_ids": [], - }, - { - "type": "repost", - "group_id": "fake_group_id_repost", - "specifier": "3", - "data": {"title": "my title", "short_description": "my desc"}, - "user_ids": [3], - }, - ], - } - populate_mock_db(db_mock, test_entities) - - with db_mock.scoped_session() as session: - unread_count = get_unread_notification_count(session, {"user_id": 1}) - assert unread_count == 1 - args = {"limit": 10, "user_id": 1} - u1_notifications = get_notifications(session, args) - assert len(u1_notifications) == 1 - assert u1_notifications[0]["type"] == "announcement" - assert u1_notifications[0]["group_id"] == "fake_group_id" - assert len(u1_notifications[0]["actions"]) == 1 - assert u1_notifications[0]["actions"][0]["data"] == { - "title": "my title", - "short_description": "my desc", - } - - -def test_get_announcement_before_account_created(app): - with app.app_context(): - db_mock = get_db() - - test_entities = { - "users": [{"user_id": i + 1} for i in range(20)], - "notification": [ - { - "type": "announcement", - "group_id": "fake_group_id", - "specifier": "1", - "data": {"title": "my title", "short_description": "my desc"}, - "user_ids": [], - "timestamp": datetime.now() - timedelta(hours=1), - }, - ], - } - populate_mock_db(db_mock, test_entities) - - with db_mock.scoped_session() as session: - unread_count = get_unread_notification_count(session, {"user_id": 1}) - assert unread_count == 0 - args = {"limit": 10, "user_id": 1} - u1_notifications = get_notifications(session, args) - assert len(u1_notifications) == 0 diff --git a/packages/discovery-provider/integration_tests/queries/test_notifications/test_follow_notification.py b/packages/discovery-provider/integration_tests/queries/test_notifications/test_follow_notification.py deleted file mode 100644 index ada247a5a76..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_notifications/test_follow_notification.py +++ /dev/null @@ -1,123 +0,0 @@ -from datetime import datetime, timedelta - -from integration_tests.utils import populate_mock_db -from src.queries.get_notifications import get_notification_groups, get_notifications -from src.utils.db_session import get_db - -t1 = datetime(2020, 10, 10, 10, 35, 0) -t2 = t1 - timedelta(hours=1) -t3 = t1 - timedelta(hours=2) -t4 = t1 - timedelta(hours=3) - - -def test_get_notifications(app): - with app.app_context(): - db_mock = get_db() - - test_entities = { - "users": [{"user_id": i + 1} for i in range(4)], - "follows": [ - {"follower_user_id": i + 2, "followee_user_id": i + 1, "created_at": t2} - for i in range(3) - ], - "notification_seens": [ - {"user_id": 1, "seen_at": t3}, - {"user_id": 1, "seen_at": t1}, - {"user_id": 3, "seen_at": t3}, - ], - } - - populate_mock_db(db_mock, test_entities) - - with db_mock.scoped_session() as session: - args = {"limit": 10, "user_id": 1} - u1_notification_groups = get_notification_groups(session, args) - assert len(u1_notification_groups) == 1 - assert u1_notification_groups[0]["group_id"] == "follow:1" - assert u1_notification_groups[0]["is_seen"] == True - assert u1_notification_groups[0]["seen_at"] == t1 - assert u1_notification_groups[0]["prev_seen_at"] == t3 - assert u1_notification_groups[0]["count"] == 1 - - # Test fetching a notification when a view is before and after - u1_notifications = get_notifications(session, args) - assert u1_notifications[0]["group_id"] == "follow:1" - assert u1_notifications[0]["is_seen"] == True - assert u1_notifications[0]["seen_at"] == t1 - assert u1_notifications[0]["actions"] == [ - { - "specifier": "2", - "type": "follow", - "group_id": "follow:1", - "timestamp": t2, - "data": {"follower_user_id": 2, "followee_user_id": 1}, - } - ] - - # Test fetching a notification when there are no views - u2_args = {"limit": 10, "user_id": 2} - u2_notification_groups = get_notification_groups(session, u2_args) - assert len(u2_notification_groups) == 1 - assert u2_notification_groups[0]["group_id"] == "follow:2" - assert u2_notification_groups[0]["is_seen"] == False - assert u2_notification_groups[0]["seen_at"] == None - assert u2_notification_groups[0]["prev_seen_at"] == None - assert u2_notification_groups[0]["count"] == 1 - - # Test fetching a notification when there is only a view after - u3_args = {"limit": 10, "user_id": 3} - u3_notifiations = get_notifications(session, u3_args) - assert u3_notifiations[0]["group_id"] == "follow:3" - assert u3_notifiations[0]["is_seen"] == False - assert u3_notifiations[0]["actions"] == [ - { - "specifier": "4", - "timestamp": t2, - "type": "follow", - "group_id": "follow:3", - "data": {"follower_user_id": 4, "followee_user_id": 3}, - } - ] - - -def test_get_many_notifications(app): - with app.app_context(): - db_mock = get_db() - - test_entities = { - "users": [{"user_id": i + 1} for i in range(20)], - "follows": [ - {"follower_user_id": i + 2, "followee_user_id": 1, "created_at": t1} - for i in range(4) - ] - + [ - {"follower_user_id": i + 2, "followee_user_id": 1, "created_at": t3} - for i in range(4, 8) - ], - "notification_seens": [{"user_id": 1, "seen_at": t2}], - } - - populate_mock_db(db_mock, test_entities) - - with db_mock.scoped_session() as session: - args = {"limit": 10, "user_id": 1} - u1_notifications = get_notifications(session, args) - assert len(u1_notifications) == 2 - assert u1_notifications[0]["group_id"] == "follow:1" - assert u1_notifications[0]["is_seen"] == False - assert len(u1_notifications[0]["actions"]) == 4 - for user_id_follower in range(2, 6): - assert any( - act["data"]["follower_user_id"] == user_id_follower - for act in u1_notifications[0]["actions"] - ) - - assert u1_notifications[1]["group_id"] == "follow:1" - assert u1_notifications[1]["is_seen"] == True - assert u1_notifications[1]["seen_at"] == t2 - assert len(u1_notifications[1]["actions"]) == 4 - for user_id_follower in range(6, 10): - assert any( - act["data"]["follower_user_id"] == user_id_follower - for act in u1_notifications[1]["actions"] - ) diff --git a/packages/discovery-provider/integration_tests/queries/test_notifications/test_get_unread_notification_count.py b/packages/discovery-provider/integration_tests/queries/test_notifications/test_get_unread_notification_count.py deleted file mode 100644 index 933ce322400..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_notifications/test_get_unread_notification_count.py +++ /dev/null @@ -1,64 +0,0 @@ -from datetime import datetime, timedelta - -from integration_tests.utils import populate_mock_db -from src.queries.get_notifications import get_unread_notification_count -from src.utils.db_session import get_db - -t1 = datetime(2020, 10, 10, 10, 35, 0) -t2 = t1 - timedelta(hours=1) -t3 = t1 - timedelta(hours=2) -t4 = t1 - timedelta(hours=3) - - -def test_get_unread_notification_count(app): - with app.app_context(): - db_mock = get_db() - - test_entities = { - "users": [{"user_id": i + 1} for i in range(5)], - "tracks": [{"track_id": 1, "owner_id": 1}], - "playlists": [{"playlist_id": 1, "playlist_owner_id": 1}], - "notification_seens": [ - {"user_id": 1, "seen_at": t3}, - ], - } - - populate_mock_db(db_mock, test_entities) - - test_actions = { - "follows": [ - {"follower_user_id": 2, "followee_user_id": 1, "created_at": t1} - ], - "reposts": [ - { - "user_id": 3, - "repost_item_id": 1, - "repost_type": "track", - "created_at": t2, - }, - { - "user_id": 3, - "repost_item_id": 1, - "repost_type": "playlist", - "created_at": t3, - }, - ], - "saves": [ - { - "user_id": 4, - "save_item_id": 1, - "save_type": "track", - "created_at": t4, - } - ], - } - populate_mock_db(db_mock, test_actions) - - with db_mock.scoped_session() as session: - args = {"user_id": 1} - unread_count = get_unread_notification_count(session, args) - assert unread_count == 2 - - args = {"user_id": 3} - unread_count = get_unread_notification_count(session, args) - assert unread_count == 0 diff --git a/packages/discovery-provider/integration_tests/queries/test_notifications/test_paginate_notification.py b/packages/discovery-provider/integration_tests/queries/test_notifications/test_paginate_notification.py deleted file mode 100644 index 12581ca4955..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_notifications/test_paginate_notification.py +++ /dev/null @@ -1,65 +0,0 @@ -import logging -from datetime import datetime, timedelta - -from integration_tests.utils import populate_mock_db -from src.queries.get_notifications import get_notifications -from src.utils.db_session import get_db - -t1 = datetime(2020, 10, 10, 10, 35, 0) -times = [t1 - timedelta(hours=i) for i in range(200)] - -logger = logging.getLogger(__name__) - - -def test_get_notifications(app): - with app.app_context(): - db_mock = get_db() - - test_entities = { - "users": [{"user_id": i + 1} for i in range(200)], - "tracks": [{"track_id": i, "owner_id": 1} for i in range(60)], - "notification_seens": [ - {"user_id": 1, "seen_at": times[i]} for i in range(0, 100, 3) - ], - } - - populate_mock_db(db_mock, test_entities) - test_actions = { - "follows": [ - { - "follower_user_id": i + 2, - "followee_user_id": 1, - "created_at": times[i], - } - for i in range(0, 80, 3) - ], - "saves": [ - { - "user_id": i, - "save_item_id": 1, - "save_type": "track", - "created_at": times[i], - } - for i in range(0, 60, 2) - ], - } - populate_mock_db(db_mock, test_actions) - - with db_mock.scoped_session() as session: - args = {"limit": 10, "user_id": 1} - u1_notification_1 = get_notifications(session, args) - assert len(u1_notification_1) == 10 - sec_last_notification = u1_notification_1[8] - timestamp = sec_last_notification["seen_at"] - group_id_offset = sec_last_notification["group_id"] - args_2 = { - "limit": 4, - "user_id": 1, - "group_id": group_id_offset, - "timestamp": timestamp, - } - u1_notification_2 = get_notifications(session, args_2) - logger.info(u1_notification_2) - logger.info(u1_notification_1) - assert len(u1_notification_2) == 4 - assert u1_notification_2[0] == u1_notification_1[9] diff --git a/packages/discovery-provider/integration_tests/queries/test_notifications/test_remix_track_notification.py b/packages/discovery-provider/integration_tests/queries/test_notifications/test_remix_track_notification.py deleted file mode 100644 index a4dcf4a34d2..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_notifications/test_remix_track_notification.py +++ /dev/null @@ -1,89 +0,0 @@ -import logging -from datetime import datetime, timedelta - -from integration_tests.utils import populate_mock_db -from src.models.notifications.notification import Notification -from src.queries.get_notifications import get_notifications -from src.utils.db_session import get_db - -t1 = datetime(2020, 10, 10, 10, 35, 0) -t2 = t1 - timedelta(hours=1) -t3 = t1 - timedelta(hours=2) -t4 = t1 - timedelta(hours=3) - -logger = logging.getLogger(__name__) - - -def test_remix_track_notifications(app): - with app.app_context(): - db_mock = get_db() - - test_entities = { - "users": [{"user_id": i + 1} for i in range(4)], - "plays": [{"user_id": i + 1} for i in range(4)], - "tracks": [{"track_id": 1, "owner_id": 1}], - } - - populate_mock_db(db_mock, test_entities) - - remix_entities = { - "tracks": [ - { - "track_id": 2, - "owner_id": 2, - "created_at": t1, - "updated_at": t1, - "remix_of": {"tracks": [{"parent_track_id": 1}]}, - } - ], - } - - populate_mock_db(db_mock, remix_entities) - - cosign_entities = { - "reposts": [ - { - "user_id": 1, - "repost_type": "track", - "repost_item_id": 2, - "created_at": t1, - } - ], - } - - populate_mock_db(db_mock, cosign_entities) - - with db_mock.scoped_session() as session: - nts = session.query(Notification).all() - logger.info(nts) - args_1 = {"limit": 10, "user_id": 1} - u1_notifs = get_notifications(session, args_1) - assert len(u1_notifs) == 1 - assert u1_notifs[0]["group_id"] == "remix:track:2:parent_track:1" - assert u1_notifs[0]["is_seen"] == False - assert u1_notifs[0]["actions"] == [ - { - "specifier": "2", - "type": "remix", - "timestamp": t1, - "group_id": "remix:track:2:parent_track:1", - "data": {"parent_track_id": 1, "track_id": 2}, - } - ] - - args_2 = {"limit": 10, "user_id": 2} - u2_notifs = get_notifications(session, args_2) - - assert len(u2_notifs) == 2 - assert u2_notifs[0]["group_id"] == "repost:2:type:track" - - assert u2_notifs[1]["group_id"] == "cosign:parent_track1:original_track:2" - assert u2_notifs[1]["actions"] == [ - { - "specifier": "1", - "type": "cosign", - "timestamp": t1, - "group_id": "cosign:parent_track1:original_track:2", - "data": {"track_id": 2, "track_owner_id": 2, "parent_track_id": 1}, - } - ] diff --git a/packages/discovery-provider/integration_tests/queries/test_notifications/test_repost_notification.py b/packages/discovery-provider/integration_tests/queries/test_notifications/test_repost_notification.py deleted file mode 100644 index 449068e624a..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_notifications/test_repost_notification.py +++ /dev/null @@ -1,82 +0,0 @@ -import logging -from datetime import datetime, timedelta - -from integration_tests.utils import populate_mock_db -from src.queries.get_notifications import get_notifications -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - -t1 = datetime(2020, 10, 10, 10, 35, 0) -t2 = t1 - timedelta(hours=1) -t3 = t1 - timedelta(hours=2) -t4 = t1 - timedelta(hours=3) - - -def test_get_repost_notifications(app): - with app.app_context(): - db_mock = get_db() - - test_entities = { - "users": [{"user_id": i + 1} for i in range(20)], - "tracks": [{"track_id": 1, "owner_id": 1}], - "playlists": [{"playlist_id": 1, "playlist_owner_id": 1}], - } - populate_mock_db(db_mock, test_entities) - - test_actions = { - "reposts": [ - { - "user_id": i + 2, - "repost_item_id": 1, - "repost_type": "track", - "created_at": t3, - } - for i in range(4) - ] - + [ - { - "user_id": i + 2, - "repost_item_id": 1, - "repost_type": "playlist", - "created_at": t4, - } - for i in range(11) - ] - } - populate_mock_db(db_mock, test_actions) - - with db_mock.scoped_session() as session: - args = {"limit": 10, "user_id": 1} - u1_notifications = get_notifications(session, args) - assert len(u1_notifications) == 3 - assert u1_notifications[0]["group_id"] == "repost:1:type:track" - assert u1_notifications[0]["is_seen"] == False - assert len(u1_notifications[0]["actions"]) == 4 - for reposter_user_id in range(2, 6): - assert any( - act["data"]["user_id"] == reposter_user_id - for act in u1_notifications[0]["actions"] - ) - - assert u1_notifications[1]["group_id"] == "repost:1:type:playlist" - assert u1_notifications[1]["is_seen"] == False - assert len(u1_notifications[1]["actions"]) == 11 - for reposter_user_id in range(2, 13): - assert any( - act["data"]["user_id"] == reposter_user_id - for act in u1_notifications[1]["actions"] - ) - - assert ( - u1_notifications[2]["group_id"] - == "milestone:PLAYLIST_REPOST_COUNT:id:1:threshold:10" - ) - assert u1_notifications[2]["is_seen"] == False - assert u1_notifications[2]["actions"][0]["type"] == "milestone" - assert ( - u1_notifications[2]["actions"][0]["data"]["type"] - == "PLAYLIST_REPOST_COUNT" - ) - assert u1_notifications[2]["actions"][0]["data"]["playlist_id"] == 1 - assert u1_notifications[2]["actions"][0]["data"]["threshold"] == 10 diff --git a/packages/discovery-provider/integration_tests/queries/test_notifications/test_repost_repost_notifications.py b/packages/discovery-provider/integration_tests/queries/test_notifications/test_repost_repost_notifications.py deleted file mode 100644 index 81698e05e94..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_notifications/test_repost_repost_notifications.py +++ /dev/null @@ -1,120 +0,0 @@ -import logging - -from integration_tests.utils import populate_mock_db -from src.queries.get_notifications import NotificationType, get_notifications -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - - -def assert_notification( - notification, group_id, is_seen, actions_length, reposter_user_ids -): - assert notification["group_id"] == group_id - assert notification["is_seen"] == is_seen - assert len(notification["actions"]) == actions_length - for reposter_user_id in reposter_user_ids: - assert any( - action["data"]["user_id"] == reposter_user_id - for action in notification["actions"] - ) - - -def test_get_repost_of_repost_notifications(app): - with app.app_context(): - db_mock = get_db() - - test_entities = { - "users": [{"user_id": i + 1} for i in range(6)], - "tracks": [{"track_id": 1, "owner_id": 1}], - "playlists": [{"playlist_id": 1, "playlist_owner_id": 1}], - "follows": [ - {"follower_user_id": 4, "followee_user_id": 1}, - {"follower_user_id": 2, "followee_user_id": 4}, - {"follower_user_id": 3, "followee_user_id": 4}, - {"follower_user_id": 3, "followee_user_id": 5}, - {"follower_user_id": 5, "followee_user_id": 3}, - ], - } - populate_mock_db(db_mock, test_entities) - - test_actions = { - "reposts": [ - {"user_id": 4, "repost_item_id": 1, "repost_type": "track"}, - {"user_id": 4, "repost_item_id": 1, "repost_type": "playlist"}, - { - "user_id": 2, - "repost_item_id": 1, - "repost_type": "track", - "is_repost_of_repost": True, - }, - { - "user_id": 3, - "repost_item_id": 1, - "repost_type": "track", - "is_repost_of_repost": True, - }, - { - "user_id": 3, - "repost_item_id": 1, - "repost_type": "playlist", - "is_repost_of_repost": True, - }, - { - "user_id": 5, - "repost_item_id": 1, - "repost_type": "playlist", - "is_repost_of_repost": True, - }, - ] - } - populate_mock_db(db_mock, test_actions) - - with db_mock.scoped_session() as session: - args = { - "limit": 10, - "user_id": 4, - "valid_types": [NotificationType.REPOST_OF_REPOST], - } - user4_notifications = get_notifications(session, args) - assert len(user4_notifications) == 3 - assert_notification( - notification=user4_notifications[0], - group_id="repost_of_repost:1:type:playlist", - is_seen=False, - actions_length=1, - reposter_user_ids=[3], - ) - assert_notification( - notification=user4_notifications[1], - group_id="repost_of_repost:1:type:track", - is_seen=False, - actions_length=2, - reposter_user_ids=[2, 3], - ) - assert "repost_of_repost" not in user4_notifications[2]["group_id"] - - args = { - "limit": 10, - "user_id": 3, - "valid_types": [NotificationType.REPOST_OF_REPOST], - } - user3_notifications = get_notifications(session, args) - assert len(user3_notifications) == 2 - assert_notification( - notification=user3_notifications[0], - group_id="repost_of_repost:1:type:playlist", - is_seen=False, - actions_length=1, - reposter_user_ids=[5], - ) - assert "repost_of_repost" not in user3_notifications[1]["group_id"] - - args = { - "limit": 10, - "user_id": 1, - "valid_types": [NotificationType.REPOST_OF_REPOST], - } - user1_notifications = get_notifications(session, args) - for notif in user1_notifications: - assert "repost_of_repost" not in notif["group_id"] diff --git a/packages/discovery-provider/integration_tests/queries/test_notifications/test_save_notification.py b/packages/discovery-provider/integration_tests/queries/test_notifications/test_save_notification.py deleted file mode 100644 index 59486f64aed..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_notifications/test_save_notification.py +++ /dev/null @@ -1,70 +0,0 @@ -from datetime import datetime, timedelta - -from integration_tests.utils import populate_mock_db -from src.queries.get_notifications import get_notifications -from src.utils.db_session import get_db - -t1 = datetime(2020, 10, 10, 10, 35, 0) -t2 = t1 - timedelta(hours=1) -t3 = t1 - timedelta(hours=2) -t4 = t1 - timedelta(hours=3) - - -def test_get_save_notifications(app): - with app.app_context(): - db_mock = get_db() - - test_entities = { - "users": [{"user_id": i + 1} for i in range(10)], - "tracks": [{"track_id": 1, "owner_id": 1}], - "playlists": [{"playlist_id": 1, "playlist_owner_id": 1}], - } - populate_mock_db(db_mock, test_entities) - - test_actions = { - "saves": [ - {"user_id": i + 2, "save_item_id": 1, "save_type": "track"} - for i in range(4) - ] - + [ - {"user_id": i + 2, "save_item_id": 1, "save_type": "playlist"} - for i in range(12) - ] - } - populate_mock_db(db_mock, test_actions) - - with db_mock.scoped_session() as session: - args = {"limit": 10, "user_id": 1} - u1_notifications = get_notifications(session, args) - assert len(u1_notifications) == 3 - - assert u1_notifications[0]["group_id"] == "save:1:type:playlist" - assert u1_notifications[0]["is_seen"] == False - assert len(u1_notifications[0]["actions"]) == 12 - for saver_user_id in range(2, 14): - assert any( - act["data"]["user_id"] == saver_user_id - for act in u1_notifications[0]["actions"] - ) - - assert ( - u1_notifications[1]["group_id"] - == "milestone:PLAYLIST_SAVE_COUNT:id:1:threshold:10" - ) - assert u1_notifications[1]["is_seen"] == False - assert u1_notifications[1]["actions"][0]["type"] == "milestone" - assert ( - u1_notifications[1]["actions"][0]["data"]["type"] - == "PLAYLIST_SAVE_COUNT" - ) - assert u1_notifications[1]["actions"][0]["data"]["playlist_id"] == 1 - assert u1_notifications[1]["actions"][0]["data"]["threshold"] == 10 - - assert u1_notifications[2]["group_id"] == "save:1:type:track" - assert u1_notifications[2]["is_seen"] == False - assert len(u1_notifications[2]["actions"]) == 4 - for saver_user_id in range(2, 6): - assert any( - act["data"]["user_id"] == saver_user_id - for act in u1_notifications[2]["actions"] - ) diff --git a/packages/discovery-provider/integration_tests/queries/test_notifications/test_save_of_repost_notification.py b/packages/discovery-provider/integration_tests/queries/test_notifications/test_save_of_repost_notification.py deleted file mode 100644 index 0c0bdf785a6..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_notifications/test_save_of_repost_notification.py +++ /dev/null @@ -1,118 +0,0 @@ -import logging - -from integration_tests.utils import populate_mock_db -from src.queries.get_notifications import NotificationType, get_notifications -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - - -def assert_notification( - notification, group_id, is_seen, actions_length, reposter_user_ids -): - assert notification["group_id"] == group_id - assert notification["is_seen"] == is_seen - assert len(notification["actions"]) == actions_length - for reposter_user_id in reposter_user_ids: - assert any( - action["data"]["user_id"] == reposter_user_id - for action in notification["actions"] - ) - - -def test_get_save_of_repost_notifications(app): - with app.app_context(): - db_mock = get_db() - - test_entities = { - "users": [{"user_id": i + 1} for i in range(6)], - "tracks": [{"track_id": 1, "owner_id": 1}], - "playlists": [{"playlist_id": 1, "playlist_owner_id": 1}], - "follows": [ - {"follower_user_id": 4, "followee_user_id": 1}, - {"follower_user_id": 2, "followee_user_id": 4}, - {"follower_user_id": 3, "followee_user_id": 4}, - {"follower_user_id": 3, "followee_user_id": 5}, - {"follower_user_id": 5, "followee_user_id": 3}, - ], - } - populate_mock_db(db_mock, test_entities) - - test_reposts = { - "reposts": [ - {"user_id": 4, "repost_item_id": 1, "repost_type": "track"}, - {"user_id": 4, "repost_item_id": 1, "repost_type": "playlist"}, - ] - } - test_saves = { - "saves": [ - { - "user_id": 2, - "save_item_id": 1, - "save_type": "track", - "is_save_of_repost": True, - }, - { - "user_id": 3, - "save_item_id": 1, - "save_type": "track", - "is_save_of_repost": True, - }, - { - "user_id": 3, - "save_item_id": 1, - "save_type": "playlist", - "is_save_of_repost": True, - }, - { - "user_id": 5, - "save_item_id": 1, - "save_type": "playlist", - "is_save_of_repost": True, - }, - ], - } - populate_mock_db(db_mock, test_reposts) - populate_mock_db(db_mock, test_saves) - - with db_mock.scoped_session() as session: - args = { - "limit": 10, - "user_id": 4, - "valid_types": [NotificationType.SAVE_OF_REPOST], - } - user4_notifications = get_notifications(session, args) - assert len(user4_notifications) == 3 - assert_notification( - notification=user4_notifications[0], - group_id="save_of_repost:1:type:playlist", - is_seen=False, - actions_length=1, - reposter_user_ids=[3], - ) - assert_notification( - notification=user4_notifications[1], - group_id="save_of_repost:1:type:track", - is_seen=False, - actions_length=2, - reposter_user_ids=[2, 3], - ) - assert "save_of_repost" not in user4_notifications[2]["group_id"] - - args = { - "limit": 10, - "user_id": 3, - "valid_types": [NotificationType.SAVE_OF_REPOST], - } - user3_notifications = get_notifications(session, args) - for notif in user3_notifications: - assert "save_of_repost" not in notif["group_id"] - - args = { - "limit": 10, - "user_id": 1, - "valid_types": [NotificationType.SAVE_OF_REPOST], - } - user1_notifications = get_notifications(session, args) - for notif in user1_notifications: - assert "save_of_repost" not in notif["group_id"] diff --git a/packages/discovery-provider/integration_tests/queries/test_notifications/test_tastemaker_notification.py b/packages/discovery-provider/integration_tests/queries/test_notifications/test_tastemaker_notification.py deleted file mode 100644 index 7d534e520d1..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_notifications/test_tastemaker_notification.py +++ /dev/null @@ -1,76 +0,0 @@ -import logging - -from integration_tests.utils import populate_mock_db -from src.queries.get_notifications import NotificationType, get_notifications -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - - -def assert_notification( - notification, group_id, is_seen, actions_length, user_ids_to_notify -): - assert notification["group_id"] == group_id - assert notification["is_seen"] == is_seen - assert len(notification["actions"]) == actions_length - for user_id in user_ids_to_notify: - assert any( - action["data"]["tastemaker_user_id"] == user_id - for action in notification["actions"] - ) - - -def test_get_tastemaker_notifications(app): - with app.app_context(): - db_mock = get_db() - - test_entities = { - "users": [{"user_id": i + 1} for i in range(6)], - "tracks": [{"track_id": 1, "owner_id": 1}], - "playlists": [{"playlist_id": 1, "playlist_owner_id": 1}], - } - populate_mock_db(db_mock, test_entities) - - test_actions = { - "notification": [ - { - "user_ids": [2], - "type": "tastemaker", - "group_id": "fake-group-id", - "specifier": 1, - "data": { - "tastemaker_item_id": 1, - "tastemaker_item_owner_id": 1, - "action": "repost", - "tastemaker_item_type": "track", - "tastemaker_user_id": 2, - }, - }, - ] - } - populate_mock_db(db_mock, test_actions) - - with db_mock.scoped_session() as session: - args = { - "limit": 10, - "user_id": 2, - "valid_types": [], - } - user2_notifications = get_notifications(session, args) - for notification in user2_notifications: - assert notification["type"] != "tastemaker" - - args = { - "limit": 10, - "user_id": 2, - "valid_types": [NotificationType.TASTEMAKER], - } - user2_notifications = get_notifications(session, args) - assert len(user2_notifications) == 1 - assert_notification( - notification=user2_notifications[0], - group_id="fake-group-id", - is_seen=False, - actions_length=1, - user_ids_to_notify=[2], - ) diff --git a/packages/discovery-provider/integration_tests/queries/test_notifications/test_track_added_to_playlist_notification.py b/packages/discovery-provider/integration_tests/queries/test_notifications/test_track_added_to_playlist_notification.py deleted file mode 100644 index ddc5e0d3652..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_notifications/test_track_added_to_playlist_notification.py +++ /dev/null @@ -1,47 +0,0 @@ -import logging - -from integration_tests.utils import populate_mock_db -from src.queries.get_notifications import ( - get_notifications, - get_unread_notification_count, -) -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - - -def test_get_announcement_notifications(app): - with app.app_context(): - db_mock = get_db() - - test_entities = { - "users": [{"user_id": i + 1} for i in range(20)], - "notification": [ - { - "type": "track_added_to_playlist", - "group_id": "track_added_to_playlist:playlist_id:1230335492:track_id:1553038297:blocknumber:41260831", - "specifier": "1", - "data": {"track_id": 23, "playlist_id": 32, "playlist_owner_id": 3}, - "user_ids": [1], - }, - ], - } - populate_mock_db(db_mock, test_entities) - - with db_mock.scoped_session() as session: - unread_count = get_unread_notification_count(session, {"user_id": 1}) - assert unread_count == 1 - args = {"limit": 10, "user_id": 1} - u1_notifications = get_notifications(session, args) - assert len(u1_notifications) == 1 - assert u1_notifications[0]["type"] == "track_added_to_playlist" - assert ( - u1_notifications[0]["group_id"] - == "track_added_to_playlist:playlist_id:1230335492:track_id:1553038297:blocknumber:41260831" - ) - assert len(u1_notifications[0]["actions"]) == 1 - assert u1_notifications[0]["actions"][0]["data"] == { - "track_id": 23, - "playlist_id": 32, - "playlist_owner_id": 3, - } diff --git a/packages/discovery-provider/integration_tests/queries/test_notifications/test_trending_playlist_notification.py b/packages/discovery-provider/integration_tests/queries/test_notifications/test_trending_playlist_notification.py deleted file mode 100644 index e532f84e46a..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_notifications/test_trending_playlist_notification.py +++ /dev/null @@ -1,91 +0,0 @@ -from integration_tests.utils import populate_mock_db -from src.queries.get_notifications import NotificationType, get_notifications -from src.utils.db_session import get_db - - -def test_get_trending_playlist_notifications(app): - with app.app_context(): - db_mock = get_db() - - entities = { - "users": [{"user_id": i + 1} for i in range(6)], - "playlists": [ - {"playlist_id": 1, "playlist_owner_id": 1}, - {"playlist_id": 2, "playlist_owner_id": 2}, - ], - "notification": [ - { - "user_ids": [1], - "type": "trending_playlist", - "group_id": "group-id-1", - "specifier": 1, - "data": { - "time_range": "week", - "genre": "all", - "rank": "1", - "playlist_id": 1, - }, - }, - { - "user_ids": [2], - "type": "trending_playlist", - "group_id": "group-id-2", - "specifier": 1, - "data": { - "time_range": "week", - "genre": "all", - "rank": "2", - "playlist_id": 2, - }, - }, - ], - } - populate_mock_db(db_mock, entities) - - # When valid_types is [] we don't select any trending_playlist notifications - with db_mock.scoped_session() as session: - args = { - "limit": 10, - "user_id": 1, - "valid_types": [], - } - user1_notifications = get_notifications(session, args) - assert len(user1_notifications) == 0 - - # When valid_types is ["trending_playlist"] and user_id is 1, - # select notifs for user1 - with db_mock.scoped_session() as session: - args = { - "limit": 10, - "user_id": 1, - "valid_types": [NotificationType.TRENDING_PLAYLIST], - } - - user1_notifications = get_notifications(session, args) - assert len(user1_notifications) == 1 - - user1_notification = user1_notifications[0] - assert user1_notification["type"] == "trending_playlist" - - notification_action = user1_notification["actions"][0] - assert notification_action["data"]["rank"] == "1" - assert notification_action["data"]["playlist_id"] == 1 - - # When valid_types is ["trending_playlist"] and user_id is 2, - # select notifs for user2 - with db_mock.scoped_session() as session: - args = { - "limit": 10, - "user_id": 2, - "valid_types": [NotificationType.TRENDING_PLAYLIST], - } - - user2_notifications = get_notifications(session, args) - assert len(user2_notifications) == 1 - - user2_notification = user2_notifications[0] - assert user2_notification["type"] == "trending_playlist" - - notification_action = user2_notification["actions"][0] - assert notification_action["data"]["rank"] == "2" - assert notification_action["data"]["playlist_id"] == 2 diff --git a/packages/discovery-provider/integration_tests/queries/test_notifications/test_trending_underground_notification.py b/packages/discovery-provider/integration_tests/queries/test_notifications/test_trending_underground_notification.py deleted file mode 100644 index 39bc9ca685e..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_notifications/test_trending_underground_notification.py +++ /dev/null @@ -1,88 +0,0 @@ -from integration_tests.utils import populate_mock_db -from src.queries.get_notifications import NotificationType, get_notifications -from src.utils.db_session import get_db - - -def test_get_trending_underground_notifications(app): - with app.app_context(): - db_mock = get_db() - - entities = { - "users": [{"user_id": i + 1} for i in range(6)], - "tracks": [{"track_id": 1, "owner_id": 1}, {"track_id": 2, "owner_id": 2}], - "notification": [ - { - "user_ids": [1], - "type": "trending_underground", - "group_id": "group-id-1", - "specifier": 1, - "data": { - "time_range": "week", - "genre": "all", - "rank": "1", - "track_id": 1, - }, - }, - { - "user_ids": [2], - "type": "trending_underground", - "group_id": "group-id-2", - "specifier": 1, - "data": { - "time_range": "week", - "genre": "all", - "rank": "2", - "track_id": 2, - }, - }, - ], - } - populate_mock_db(db_mock, entities) - - # When valid_types is [] we don't select any trending_underground notifications - with db_mock.scoped_session() as session: - args = { - "limit": 10, - "user_id": 1, - "valid_types": [], - } - user1_notifications = get_notifications(session, args) - assert len(user1_notifications) == 0 - - # When valid_types is ["trending_underground"] and user_id is 1, - # select notifs for user1 - with db_mock.scoped_session() as session: - args = { - "limit": 10, - "user_id": 1, - "valid_types": [NotificationType.TRENDING_UNDERGROUND], - } - - user2_notifications = get_notifications(session, args) - assert len(user2_notifications) == 1 - - user1_notification = user2_notifications[0] - assert user1_notification["type"] == "trending_underground" - - notification_action = user1_notification["actions"][0] - assert notification_action["data"]["rank"] == "1" - assert notification_action["data"]["track_id"] == 1 - - # When valid_types is ["trending_underground"] and user_id is 2, - # select notifs for user2 - with db_mock.scoped_session() as session: - args = { - "limit": 10, - "user_id": 2, - "valid_types": [NotificationType.TRENDING_UNDERGROUND], - } - - user2_notifications = get_notifications(session, args) - assert len(user2_notifications) == 1 - - user2_notification = user2_notifications[0] - assert user2_notification["type"] == "trending_underground" - - notification_action = user2_notification["actions"][0] - assert notification_action["data"]["rank"] == "2" - assert notification_action["data"]["track_id"] == 2 diff --git a/packages/discovery-provider/integration_tests/queries/test_notifications/test_user_tier_change_notification.py b/packages/discovery-provider/integration_tests/queries/test_notifications/test_user_tier_change_notification.py deleted file mode 100644 index ac4e29ea715..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_notifications/test_user_tier_change_notification.py +++ /dev/null @@ -1,45 +0,0 @@ -from datetime import datetime, timedelta - -from integration_tests.utils import populate_mock_db -from src.queries.get_notifications import get_notifications -from src.utils.db_session import get_db - -t1 = datetime(2020, 10, 10, 10, 35, 0) -t2 = t1 - timedelta(hours=1) -t3 = t1 - timedelta(hours=2) -t4 = t1 - timedelta(hours=3) - - -def test_get_save_notifications(app): - with app.app_context(): - db_mock = get_db() - - test_entities = { - "user_balance_changes": [ - { - "user_id": 1, - "blocknumber": 10, - # No tier change, none -> none - "previous_balance": 0, - "current_balance": 20000000000000000000, - }, - ], - "users": [{"user_id": i + 1} for i in range(2)], - } - populate_mock_db(db_mock, test_entities) - - with db_mock.scoped_session() as session: - args = {"limit": 10, "user_id": 1} - u1_notifications = get_notifications(session, args) - assert len(u1_notifications) == 1 - assert ( - u1_notifications[0]["group_id"] - == "tier_change:user_id:1:tier:bronze:blocknumber:10" - ) - assert u1_notifications[0]["is_seen"] == False - assert len(u1_notifications[0]["actions"]) == 1 - u1_notifications[0]["actions"][0]["data"] = { - "new_tier": "bronze", - "new_tier_value": 10, - "current_value": "20000000000000000000", - } diff --git a/packages/discovery-provider/integration_tests/queries/test_populate_user_metadata.py b/packages/discovery-provider/integration_tests/queries/test_populate_user_metadata.py deleted file mode 100644 index 9b0f68b169a..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_populate_user_metadata.py +++ /dev/null @@ -1,164 +0,0 @@ -import logging - -from integration_tests.utils import populate_mock_db -from src.models.playlists.playlist import Playlist -from src.queries import response_name_constants -from src.queries.get_top_users import _get_top_users -from src.queries.query_helpers import populate_user_metadata -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - - -def test_populate_user_metadata(app): - """Tests that populate_user_metadata works after aggregate_user update""" - with app.app_context(): - db = get_db() - - test_entities = { - "tracks": [ - {"track_id": 1, "owner_id": 1}, - {"track_id": 2, "owner_id": 1}, - {"track_id": 3, "owner_id": 2}, - {"track_id": 4, "owner_id": 2}, - {"track_id": 5, "owner_id": 2}, - {"track_id": 6, "owner_id": 2}, - {"track_id": 7, "owner_id": 3}, - {"track_id": 8, "owner_id": 3}, - {"track_id": 9, "is_unlisted": True, "owner_id": 3}, - ], - "playlists": [ - {"playlist_id": 1, "playlist_owner_id": 1}, - {"playlist_id": 2, "playlist_owner_id": 1}, - {"playlist_id": 3, "is_album": True, "playlist_owner_id": 1}, - {"playlist_id": 4, "playlist_owner_id": 2}, - { - "playlist_id": 5, - "is_delete": False, - "playlist_owner_id": 2, - }, - {"playlist_id": 6, "is_album": True, "playlist_owner_id": 3}, - {"playlist_id": 6, "is_private": True, "playlist_owner_id": 3}, - ], - "users": [ - {"user_id": 1, "handle": "user1", "wallet": "0x111"}, - {"user_id": 2, "handle": "user2", "wallet": "0x222"}, - {"user_id": 3, "handle": "user3", "wallet": "0x333"}, - {"user_id": 4, "handle": "user4", "wallet": "0x444"}, - ], - "follows": [ - {"follower_user_id": 1, "followee_user_id": 2}, - {"follower_user_id": 1, "followee_user_id": 3}, - {"follower_user_id": 2, "followee_user_id": 3}, - ], - "reposts": [ - {"repost_item_id": 1, "repost_type": "track", "user_id": 2}, - {"repost_item_id": 1, "repost_type": "playlist", "user_id": 2}, - {"repost_item_id": 1, "repost_type": "track", "user_id": 3}, - {"repost_item_id": 1, "repost_type": "playlist", "user_id": 3}, - {"repost_item_id": 4, "repost_type": "track", "user_id": 1}, - {"repost_item_id": 5, "repost_type": "track", "user_id": 1}, - {"repost_item_id": 6, "repost_type": "track", "user_id": 1}, - ], - "saves": [ - {"save_item_id": 1, "save_type": "track", "user_id": 2}, - {"save_item_id": 1, "save_type": "playlist", "user_id": 2}, - {"save_item_id": 1, "save_type": "track", "user_id": 3}, - {"save_item_id": 1, "save_type": "playlist", "user_id": 3}, - {"save_item_id": 4, "save_type": "track", "user_id": 1}, - {"save_item_id": 5, "save_type": "track", "user_id": 1}, - {"save_item_id": 6, "save_type": "track", "user_id": 1}, - ], - } - - populate_mock_db(db, test_entities) - with db.scoped_session() as session: - playlist_to_delete = ( - session.query(Playlist).filter(Playlist.playlist_id == 5).first() - ) - playlist_to_delete.is_delete = True - - with db.scoped_session() as session: - user_ids = [1, 2, 3, 4, 5] - users = [ - {"user_id": 1, "wallet": "0x111", "is_verified": False}, - {"user_id": 2, "wallet": "0x222", "is_verified": False}, - {"user_id": 3, "wallet": "0x333", "is_verified": False}, - {"user_id": 4, "wallet": "0x444", "is_verified": False}, - {"user_id": 5, "wallet": "0x555", "is_verified": False}, - ] - - users = populate_user_metadata(session, user_ids, users, 3) - assert len(users) == 5 - - assert users[0]["user_id"] == 1 - assert users[0][response_name_constants.track_count] == 2 - assert users[0][response_name_constants.playlist_count] == 2 - assert users[0][response_name_constants.album_count] == 1 - assert users[0][response_name_constants.follower_count] == 0 - assert users[0][response_name_constants.followee_count] == 2 - assert users[0][response_name_constants.repost_count] == 3 - - assert users[1]["user_id"] == 2 - assert users[1][response_name_constants.track_count] == 4 - assert users[1][response_name_constants.playlist_count] == 2 - assert users[1][response_name_constants.album_count] == 0 - assert users[1][response_name_constants.follower_count] == 1 - assert users[1][response_name_constants.followee_count] == 1 - assert users[1][response_name_constants.repost_count] == 2 - - assert users[2]["user_id"] == 3 - assert users[2][response_name_constants.track_count] == 2 - assert users[2][response_name_constants.playlist_count] == 0 - assert users[2][response_name_constants.album_count] == 1 - assert users[2][response_name_constants.follower_count] == 2 - assert users[2][response_name_constants.followee_count] == 0 - assert users[2][response_name_constants.repost_count] == 2 - - assert users[3]["user_id"] == 4 - assert users[3][response_name_constants.track_count] == 0 - assert users[3][response_name_constants.playlist_count] == 0 - assert users[3][response_name_constants.album_count] == 0 - assert users[3][response_name_constants.follower_count] == 0 - assert users[3][response_name_constants.followee_count] == 0 - assert users[3][response_name_constants.repost_count] == 0 - - assert users[4]["user_id"] == 5 - assert users[4][response_name_constants.track_count] == 0 - assert users[4][response_name_constants.playlist_count] == 0 - assert users[4][response_name_constants.album_count] == 0 - assert users[4][response_name_constants.follower_count] == 0 - assert users[4][response_name_constants.followee_count] == 0 - assert users[4][response_name_constants.repost_count] == 0 - - curr_user_ids = [1, 2, 3] - curr_users = [ - {"user_id": 1, "wallet": "0x111", "is_verified": False}, - {"user_id": 2, "wallet": "0x222", "is_verified": False}, - {"user_id": 3, "wallet": "0x333", "is_verified": False}, - ] - - users = populate_user_metadata(session, curr_user_ids, curr_users, 1) - assert len(users) == 3 - - assert users[0]["user_id"] == 1 - assert users[0][response_name_constants.does_current_user_follow] == False - assert users[0][response_name_constants.current_user_followee_follow_count] == 0 - assert users[0][response_name_constants.balance] == "0" - assert users[0][response_name_constants.associated_wallets_balance] == "0" - - assert users[1]["user_id"] == 2 - assert users[1][response_name_constants.does_current_user_follow] == True - assert users[1][response_name_constants.current_user_followee_follow_count] == 0 - assert users[1][response_name_constants.balance] == "0" - assert users[1][response_name_constants.associated_wallets_balance] == "0" - - assert users[2]["user_id"] == 3 - assert users[2][response_name_constants.does_current_user_follow] == True - assert users[2][response_name_constants.current_user_followee_follow_count] == 1 - assert users[2][response_name_constants.balance] == "0" - assert users[2][response_name_constants.associated_wallets_balance] == "0" - - # get_top_users: should return only artists, most followers first - top_user_ids = [u["user_id"] for u in _get_top_users(session, 1, 100, 0)] - assert top_user_ids == [3, 2, 1] diff --git a/packages/discovery-provider/integration_tests/queries/test_undisbursed_challenges.py b/packages/discovery-provider/integration_tests/queries/test_undisbursed_challenges.py deleted file mode 100644 index ac1918c9c73..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_undisbursed_challenges.py +++ /dev/null @@ -1,239 +0,0 @@ -from datetime import datetime - -from integration_tests.utils import populate_mock_db_blocks -from src.models.rewards.challenge import Challenge, ChallengeType -from src.models.rewards.user_challenge import UserChallenge -from src.models.users.user import User -from src.models.users.user_bank import UserBankAccount -from src.queries.get_undisbursed_challenges import get_undisbursed_challenges -from src.utils.db_session import get_db - - -def setup_challenges(app): - with app.app_context(): - db = get_db() - populate_mock_db_blocks(db, 99, 110) - challenges = [ - Challenge( - id="test_challenge_1", - type=ChallengeType.numeric, - amount="5", - step_count=3, - active=False, - starting_block=100, - ), - Challenge( - id="test_challenge_2", - type=ChallengeType.boolean, - amount="5", - active=True, - starting_block=100, - ), - Challenge( - id="test_challenge_3", - type=ChallengeType.aggregate, - amount="5", - active=True, - starting_block=100, - ), - ] - - users = [ - User( - blockhash=hex(99), - blocknumber=99, - txhash=f"xyz{i}", - user_id=i, - is_current=True, - handle=f"TestHandle{i}", - handle_lc=f"testhandle{i}", - wallet=f"0x{i}", - is_verified=False, - name=f"test_name{i}", - created_at=datetime.now(), - updated_at=datetime.now(), - ) - for i in range(7) - ] - user_bank_accounts = [ - UserBankAccount( - signature=f"0x{i}", - ethereum_address=users[i].wallet, - bank_account=f"0x{i}", - created_at=datetime.now(), - ) - for i in range(7) - ] - - user_challenges = [ - UserChallenge( - challenge_id="test_challenge_1", - user_id=1, - specifier="1", - is_complete=False, - current_step_count=1, - amount=5, - created_at="2023-10-16 17:51:31.105065+00", - completed_at="2023-10-16 17:51:31.105065+00", - ), - UserChallenge( - challenge_id="test_challenge_1", - user_id=2, - specifier="2", - is_complete=True, - current_step_count=3, - completed_blocknumber=100, - amount=5, - created_at="2023-10-16 17:51:31.105065+00", - completed_at="2023-10-16 17:51:31.105065+00", - ), - UserChallenge( - challenge_id="test_challenge_2", - user_id=3, - specifier="3", - is_complete=False, - amount=5, - created_at="2023-10-16 17:51:31.105065+00", - completed_at="2023-10-16 17:51:31.105065+00", - ), - UserChallenge( - challenge_id="test_challenge_2", - user_id=4, - specifier="4", - is_complete=True, - completed_blocknumber=102, - amount=5, - created_at="2023-10-16 17:51:31.105065+00", - completed_at="2023-10-16 17:51:31.105065+00", - ), - UserChallenge( - challenge_id="test_challenge_2", - user_id=5, - specifier="5", - is_complete=True, - completed_blocknumber=102, - amount=5, - created_at="2023-10-16 17:51:31.105065+00", - completed_at="2023-10-16 17:51:31.105065+00", - ), - UserChallenge( - challenge_id="test_challenge_3", - user_id=6, - specifier="6", - is_complete=True, - completed_blocknumber=100, - amount=5, - created_at="2023-10-16 17:51:31.105065+00", - completed_at="2023-10-16 17:51:31.105065+00", - ), - ] - - with db.scoped_session() as session: - session.add_all(challenges) - session.flush() - session.add_all(users) - session.add_all(user_bank_accounts) - session.add_all(user_challenges) - - -def test_undisbursed_challenges(app): - setup_challenges(app) - - with app.app_context(): - db = get_db() - - with db.scoped_session() as session: - # Test that all undisbursed challenges are returned in order - undisbursed = get_undisbursed_challenges( - session, - { - "user_id": None, - "limit": 10, - "offset": 0, - "completed_blocknumber": 99, - "challenge_id": None, - }, - ) - - expected = [ - { - "challenge_id": "test_challenge_3", - "user_id": 6, - "specifier": "6", - "amount": "5", - "completed_blocknumber": 100, - "handle": "TestHandle6", - "wallet": "0x6", - "created_at": "2023-10-16 17:51:31.105065+00:00", - "completed_at": "2023-10-16 17:51:31.105065", - "cooldown_days": None, - }, - { - "challenge_id": "test_challenge_2", - "user_id": 4, - "specifier": "4", - "amount": "5", - "completed_blocknumber": 102, - "handle": "TestHandle4", - "wallet": "0x4", - "created_at": "2023-10-16 17:51:31.105065+00:00", - "completed_at": "2023-10-16 17:51:31.105065", - "cooldown_days": None, - }, - { - "challenge_id": "test_challenge_2", - "user_id": 5, - "specifier": "5", - "amount": "5", - "completed_blocknumber": 102, - "handle": "TestHandle5", - "wallet": "0x5", - "created_at": "2023-10-16 17:51:31.105065+00:00", - "completed_at": "2023-10-16 17:51:31.105065", - "cooldown_days": None, - }, - ] - assert expected == undisbursed - - # Test that it filters correctly by user_id - undisbursed = get_undisbursed_challenges( - session, - { - "user_id": 6, - "limit": 10, - "offset": 0, - "completed_blocknumber": 99, - "challenge_id": None, - }, - ) - - expected = [ - { - "challenge_id": "test_challenge_3", - "user_id": 6, - "specifier": "6", - "amount": "5", - "completed_blocknumber": 100, - "handle": "TestHandle6", - "wallet": "0x6", - "created_at": "2023-10-16 17:51:31.105065+00:00", - "completed_at": "2023-10-16 17:51:31.105065", - "cooldown_days": None, - }, - ] - assert expected == undisbursed - - # Test that it filters correctly by user_id & completed blocknumber - undisbursed = get_undisbursed_challenges( - session, - { - "user_id": 6, - "limit": 10, - "offset": 0, - "completed_blocknumber": 101, - "challenge_id": None, - }, - ) - - expected = [] - assert expected == undisbursed diff --git a/packages/discovery-provider/integration_tests/queries/test_user_playlist_update.py b/packages/discovery-provider/integration_tests/queries/test_user_playlist_update.py deleted file mode 100644 index c1690b28248..00000000000 --- a/packages/discovery-provider/integration_tests/queries/test_user_playlist_update.py +++ /dev/null @@ -1,103 +0,0 @@ -import logging -from datetime import datetime - -from integration_tests.utils import populate_mock_db -from src.queries.get_user_playlist_update import get_user_playlist_update -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - - -t1 = datetime.fromtimestamp(10000000) -t2 = datetime.fromtimestamp(10000001) -t3 = datetime.fromtimestamp(10000002) -t4 = datetime.fromtimestamp(10000003) - - -def test_user_playlist_update(app): - """Tests that fetching updated playlists for users works""" - with app.app_context(): - db = get_db() - - test_entities = { - "playlists": [ - {"playlist_id": playlist_id, "updated_at": t2} - for playlist_id in range(1, 20) - ], - "users": [{"user_id": user_id} for user_id in range(1, 20)], - "saves": [ - { - "user_id": 1, - "save_item_id": 1, - "save_type": "playlist", - "created_at": t1, - }, - { - "user_id": 1, - "save_item_id": 2, - "save_type": "playlist", - "created_at": t1, - }, - { - "user_id": 1, - "save_item_id": 3, - "save_type": "playlist", - "created_at": t1, - }, - { - "user_id": 1, - "save_item_id": 4, - "save_type": "playlist", - "created_at": t1, - }, - { - "user_id": 2, - "save_item_id": 2, - "save_type": "playlist", - "created_at": t1, - }, - { - "user_id": 3, - "save_item_id": 2, - "save_type": "playlist", - "created_at": t3, - }, - ], - "playlist_seens": [ - {"is_current": True, "user_id": 1, "playlist_id": 1, "seen_at": t1}, - {"is_current": True, "user_id": 1, "playlist_id": 2, "seen_at": t2}, - {"is_current": True, "user_id": 1, "playlist_id": 3, "seen_at": t3}, - {"is_current": True, "user_id": 1, "playlist_id": 4, "seen_at": t1}, - ], - } - - populate_mock_db(db, test_entities) - - user_id = 1 - playlist_updates = get_user_playlist_update(user_id) - assert playlist_updates == [ - { - "playlist_id": 1, - "updated_at": t2, - "last_seen_at": t1, - }, - { - "playlist_id": 4, - "updated_at": t2, - "last_seen_at": t1, - }, - ] - - user_id = 2 - playlist_updates = get_user_playlist_update(user_id) - assert playlist_updates == [ - { - "playlist_id": 2, - "updated_at": t2, - "last_seen_at": None, - } - ] - - user_id = 3 - playlist_updates = get_user_playlist_update(user_id) - assert playlist_updates == [] diff --git a/packages/discovery-provider/integration_tests/tasks/test_artist_remix_contest_ending_soon_notification.py b/packages/discovery-provider/integration_tests/tasks/test_artist_remix_contest_ending_soon_notification.py deleted file mode 100644 index 2ffce034bfe..00000000000 --- a/packages/discovery-provider/integration_tests/tasks/test_artist_remix_contest_ending_soon_notification.py +++ /dev/null @@ -1,166 +0,0 @@ -import logging -from datetime import datetime, timedelta - -from integration_tests.utils import populate_mock_db -from src.models.notifications.notification import Notification -from src.queries.get_notifications import NotificationType -from src.tasks.remix_contest_notifications.artist_remix_contest_ending_soon import ( - create_artist_remix_contest_ending_soon_notifications, -) -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - -TEST_EVENT_CREATOR_ID = 1 -TEST_TRACK_ID = 100 -TEST_FOLLOWER_ID = 2 -TEST_FAVORITER_ID = 3 - - -def test_artist_remix_contest_ending_soon_notification_for_artist(app): - """Test that artist remix contest ending soon notification is created for the event creator only""" - with app.app_context(): - db = get_db() - - now = datetime.now() - end_date = now + timedelta(hours=24) - - entities = { - "users": [ - { - "user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": TEST_FOLLOWER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": TEST_FAVORITER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": TEST_TRACK_ID, - "owner_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - "updated_at": now, - }, - ], - "follows": [ - { - "follower_user_id": TEST_FOLLOWER_ID, - "followee_user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - }, - ], - "saves": [ - { - "user_id": TEST_FAVORITER_ID, - "save_item_id": TEST_TRACK_ID, - "save_type": "track", - "is_current": True, - "is_delete": False, - "created_at": now, - }, - ], - "events": [ - { - "event_type": "remix_contest", - "user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "created_at": now, - "updated_at": now, - "end_date": end_date, - }, - ], - } - populate_mock_db(db, entities) - - with db.scoped_session() as session: - create_artist_remix_contest_ending_soon_notifications(session) - notifications = ( - session.query(Notification) - .filter( - Notification.type == NotificationType.ARTIST_REMIX_CONTEST_ENDING_SOON - ) - .all() - ) - assert len(notifications) == 1 - notification = notifications[0] - assert notification.user_ids == [TEST_EVENT_CREATOR_ID] - assert notification.data["entity_user_id"] == TEST_EVENT_CREATOR_ID - assert notification.data["entity_id"] == TEST_TRACK_ID - assert notification.type == NotificationType.ARTIST_REMIX_CONTEST_ENDING_SOON - assert notification.group_id.startswith("artist_remix_contest_ending_soon:") - - -def test_artist_remix_contest_ending_soon_notification_private_track(app): - """Test that no notification is created for a remix contest on a private (unlisted) track for the artist""" - with app.app_context(): - db = get_db() - - now = datetime.now() - end_date = now + timedelta(hours=24) - PRIVATE_TRACK_ID = 200 - PRIVATE_TRACK_OWNER_ID = 5 - - entities = { - "users": [ - { - "user_id": PRIVATE_TRACK_OWNER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": PRIVATE_TRACK_ID, - "owner_id": PRIVATE_TRACK_OWNER_ID, - "is_current": True, - "is_delete": False, - "is_unlisted": True, # Mark as private - "created_at": now, - "updated_at": now, - }, - ], - "events": [ - { - "event_type": "remix_contest", - "user_id": PRIVATE_TRACK_OWNER_ID, - "entity_id": PRIVATE_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "created_at": now, - "updated_at": now, - "end_date": end_date, - }, - ], - } - populate_mock_db(db, entities) - - with db.scoped_session() as session: - create_artist_remix_contest_ending_soon_notifications(session) - notifications = ( - session.query(Notification) - .filter( - Notification.type == NotificationType.ARTIST_REMIX_CONTEST_ENDING_SOON - ) - .all() - ) - # Should not notify the artist for private tracks - assert len(notifications) == 0 diff --git a/packages/discovery-provider/integration_tests/tasks/test_artist_remix_contest_submissions_notification.py b/packages/discovery-provider/integration_tests/tasks/test_artist_remix_contest_submissions_notification.py deleted file mode 100644 index 3bcbe07fd91..00000000000 --- a/packages/discovery-provider/integration_tests/tasks/test_artist_remix_contest_submissions_notification.py +++ /dev/null @@ -1,131 +0,0 @@ -import logging -from datetime import datetime, timedelta - -from integration_tests.utils import populate_mock_db -from src.models.notifications.notification import Notification -from src.queries.get_notifications import NotificationType -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - -TEST_CONTEST_CREATOR_ID = 1 -TEST_PARENT_TRACK_ID = 100 -TEST_EVENT_ID = 200 - - -def make_remix(track_id, owner_id, created_at): - return { - "track_id": track_id, - "owner_id": owner_id, - "is_current": True, - "is_delete": False, - "created_at": created_at, - "updated_at": created_at, - "remix_of": {"tracks": [{"parent_track_id": TEST_PARENT_TRACK_ID}]}, - } - - -def get_milestone_notifications(session): - return ( - session.query(Notification) - .filter(Notification.type == NotificationType.ARTIST_REMIX_CONTEST_SUBMISSIONS) - .all() - ) - - -def test_artist_remix_contest_submissions_milestones(app): - """Test that milestone notifications are created for 1, 10, 50 submissions after contest start""" - with app.app_context(): - db = get_db() - - now = datetime.now() - before = now - timedelta(days=1) - - # Insert contest creator and parent track - entities = { - "users": [ - { - "user_id": TEST_CONTEST_CREATOR_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - } - ], - "tracks": [ - { - "track_id": TEST_PARENT_TRACK_ID, - "owner_id": TEST_CONTEST_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - "updated_at": now, - } - ], - } - populate_mock_db(db, entities) - - # Insert contest event - event_entities = { - "events": [ - { - "event_id": TEST_EVENT_ID, - "event_type": "remix_contest", - "user_id": TEST_CONTEST_CREATOR_ID, - "entity_id": TEST_PARENT_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "created_at": now, - "updated_at": now, - "end_date": now + timedelta(days=7), - } - ] - } - populate_mock_db(db, event_entities) - - with db.scoped_session() as session: - # Insert 1 remix before contest start (should not count) - remix_entities = {"tracks": [make_remix(1000, 2, before)]} - populate_mock_db(db, remix_entities) - notifications = get_milestone_notifications(session) - assert len(notifications) == 0 - - # Insert 1st remix after contest start (should trigger 1 milestone) - remix_entities = {"tracks": [make_remix(1001, 3, now + timedelta(minutes=1))]} - populate_mock_db(db, remix_entities) - notifications = get_milestone_notifications(session) - milestones = {n.data["milestone"] for n in notifications} - assert milestones == {1} - assert len(notifications) == 1 - - # Insert 9 more remixes (total 10 after contest start) - remix_entities = { - "tracks": [ - make_remix(1002 + i, 4 + i, now + timedelta(minutes=2 + i)) - for i in range(9) - ] - } - populate_mock_db(db, remix_entities) - notifications = get_milestone_notifications(session) - milestones = {n.data["milestone"] for n in notifications} - assert milestones == {1, 10} - assert len([n for n in notifications if n.data["milestone"] == 10]) == 1 - - # Insert 40 more remixes (total 50 after contest start) - remix_entities = { - "tracks": [ - make_remix(1011 + i, 13 + i, now + timedelta(minutes=11 + i)) - for i in range(40) - ] - } - populate_mock_db(db, remix_entities) - notifications = get_milestone_notifications(session) - milestones = {n.data["milestone"] for n in notifications} - assert milestones == {1, 10, 50} - assert len([n for n in notifications if n.data["milestone"] == 50]) == 1 - - # Final check: only 3 notifications, and all are for the contest creator and correct event/track - assert len(notifications) == 3 - for n in notifications: - assert n.user_ids == [TEST_CONTEST_CREATOR_ID] - assert n.data["event_id"] == TEST_EVENT_ID - assert n.data["entity_id"] == TEST_PARENT_TRACK_ID diff --git a/packages/discovery-provider/integration_tests/tasks/test_fan_remix_contest_ended_notification.py b/packages/discovery-provider/integration_tests/tasks/test_fan_remix_contest_ended_notification.py deleted file mode 100644 index d50dcbf99d3..00000000000 --- a/packages/discovery-provider/integration_tests/tasks/test_fan_remix_contest_ended_notification.py +++ /dev/null @@ -1,274 +0,0 @@ -import logging -from datetime import datetime, timedelta - -from integration_tests.utils import populate_mock_db -from src.models.notifications.notification import Notification -from src.queries.get_notifications import NotificationType -from src.tasks.remix_contest_notifications.fan_remix_contest_ended import ( - create_fan_remix_contest_ended_notifications, -) -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - -TEST_EVENT_CREATOR_ID = 1 -TEST_TRACK_ID = 100 -TEST_REMIXER_ID_1 = 2 -TEST_REMIXER_ID_2 = 3 - - -def test_fan_remix_contest_ended_notification_for_remixers(app): - """Test that remix contest ended notification is created for all remixers of the contest track""" - with app.app_context(): - db = get_db() - - now = datetime.now() - event_time = now - timedelta(hours=1) - remix_time = now - timedelta(minutes=30) - - entities = { - "users": [ - { - "user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": TEST_REMIXER_ID_1, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": TEST_REMIXER_ID_2, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": TEST_TRACK_ID, - "owner_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - "updated_at": now, - }, - # Remix 1 by REMIXER_ID_1 - { - "track_id": 200, - "owner_id": TEST_REMIXER_ID_1, - "is_current": True, - "is_delete": False, - "created_at": remix_time, - "updated_at": remix_time, - "remix_of": {"tracks": [{"parent_track_id": TEST_TRACK_ID}]}, - }, - # Remix 2 by REMIXER_ID_2 - { - "track_id": 201, - "owner_id": TEST_REMIXER_ID_2, - "is_current": True, - "is_delete": False, - "created_at": remix_time, - "updated_at": remix_time, - "remix_of": {"tracks": [{"parent_track_id": TEST_TRACK_ID}]}, - }, - ], - "events": [ - { - "event_type": "remix_contest", - "user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "end_date": event_time, - "created_at": event_time, - "updated_at": event_time, - } - ], - } - populate_mock_db(db, entities) - - with db.scoped_session() as session: - create_fan_remix_contest_ended_notifications(session) - notifications = ( - session.query(Notification) - .filter(Notification.type == NotificationType.FAN_REMIX_CONTEST_ENDED) - .all() - ) - notified_user_ids = set() - for notification in notifications: - notified_user_ids.update(notification.user_ids) - # entity_user_id should be the original artist - assert notification.data["entity_user_id"] == TEST_EVENT_CREATOR_ID - assert notification.data["entity_id"] == TEST_TRACK_ID - assert notification.type == NotificationType.FAN_REMIX_CONTEST_ENDED - assert notification.group_id.startswith("fan_remix_contest_ended:") - # Should notify both remixers - assert TEST_REMIXER_ID_1 in notified_user_ids - assert TEST_REMIXER_ID_2 in notified_user_ids - assert len(notified_user_ids) == 2 - - -def test_fan_remix_contest_ended_notification_no_duplicate_for_multiple_remixes(app): - """Test that a user who submitted multiple remixes only gets one notification""" - with app.app_context(): - db = get_db() - - now = datetime.now() - event_time = now - timedelta(hours=1) - remix_time = now - timedelta(minutes=30) - REMIXER_ID = 4 - - entities = { - "users": [ - { - "user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": REMIXER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": TEST_TRACK_ID, - "owner_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - "updated_at": now, - }, - # Two remixes by the same user - { - "track_id": 300, - "owner_id": REMIXER_ID, - "is_current": True, - "is_delete": False, - "created_at": remix_time, - "updated_at": remix_time, - "remix_of": {"tracks": [{"parent_track_id": TEST_TRACK_ID}]}, - }, - { - "track_id": 301, - "owner_id": REMIXER_ID, - "is_current": True, - "is_delete": False, - "created_at": remix_time, - "updated_at": remix_time, - "remix_of": {"tracks": [{"parent_track_id": TEST_TRACK_ID}]}, - }, - ], - "events": [ - { - "event_type": "remix_contest", - "user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "end_date": event_time, - "created_at": event_time, - "updated_at": event_time, - } - ], - } - populate_mock_db(db, entities) - - with db.scoped_session() as session: - create_fan_remix_contest_ended_notifications(session) - notifications = ( - session.query(Notification) - .filter(Notification.type == NotificationType.FAN_REMIX_CONTEST_ENDED) - .all() - ) - notif_count = 0 - for notification in notifications: - if REMIXER_ID in notification.user_ids: - notif_count += 1 - assert notification.data["entity_user_id"] == TEST_EVENT_CREATOR_ID - assert notification.data["entity_id"] == TEST_TRACK_ID - assert notification.type == NotificationType.FAN_REMIX_CONTEST_ENDED - assert notification.group_id.startswith("fan_remix_contest_ended:") - assert notif_count == 1 - - -def test_fan_remix_contest_ended_notification_private_parent_track(app): - """Test that no notification is created for remixers if the parent track is private (unlisted)""" - with app.app_context(): - db = get_db() - - now = datetime.now() - event_time = now - timedelta(hours=1) - remix_time = now - timedelta(minutes=30) - PRIVATE_TRACK_ID = 400 - PRIVATE_TRACK_OWNER_ID = 10 - REMIXER_ID = 11 - - entities = { - "users": [ - { - "user_id": PRIVATE_TRACK_OWNER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": REMIXER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": PRIVATE_TRACK_ID, - "owner_id": PRIVATE_TRACK_OWNER_ID, - "is_current": True, - "is_delete": False, - "is_unlisted": True, # Mark as private - "created_at": now, - "updated_at": now, - }, - # Remix by REMIXER_ID - { - "track_id": 401, - "owner_id": REMIXER_ID, - "is_current": True, - "is_delete": False, - "created_at": remix_time, - "updated_at": remix_time, - "remix_of": {"tracks": [{"parent_track_id": PRIVATE_TRACK_ID}]}, - }, - ], - "events": [ - { - "event_type": "remix_contest", - "user_id": PRIVATE_TRACK_OWNER_ID, - "entity_id": PRIVATE_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "end_date": event_time, - "created_at": event_time, - "updated_at": event_time, - } - ], - } - populate_mock_db(db, entities) - - with db.scoped_session() as session: - create_fan_remix_contest_ended_notifications(session) - notifications = ( - session.query(Notification) - .filter(Notification.type == NotificationType.FAN_REMIX_CONTEST_ENDED) - .all() - ) - # Should not notify any remixers for private parent tracks - assert len(notifications) == 0 diff --git a/packages/discovery-provider/integration_tests/tasks/test_fan_remix_contest_ending_soon_notification.py b/packages/discovery-provider/integration_tests/tasks/test_fan_remix_contest_ending_soon_notification.py deleted file mode 100644 index 8b904faa3f9..00000000000 --- a/packages/discovery-provider/integration_tests/tasks/test_fan_remix_contest_ending_soon_notification.py +++ /dev/null @@ -1,274 +0,0 @@ -import logging -from datetime import datetime, timedelta - -from integration_tests.utils import populate_mock_db -from src.models.notifications.notification import Notification -from src.queries.get_notifications import NotificationType -from src.tasks.remix_contest_notifications.fan_remix_contest_ending_soon import ( - create_fan_remix_contest_ending_soon_notifications, -) -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - -TEST_EVENT_CREATOR_ID = 1 -TEST_TRACK_ID = 100 -TEST_FOLLOWER_ID = 2 -TEST_FAVORITER_ID = 3 - - -def test_fan_remix_contest_ending_soon_notification_for_followers_and_favoriters(app): - """Test that remix contest ending soon notification is created for followers and users who favorited the track""" - with app.app_context(): - db = get_db() - - now = datetime.now() - end_date = now + timedelta(hours=48) - - entities = { - "users": [ - { - "user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": TEST_FOLLOWER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": TEST_FAVORITER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": TEST_TRACK_ID, - "owner_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - "updated_at": now, - }, - ], - "follows": [ - { - "follower_user_id": TEST_FOLLOWER_ID, - "followee_user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - }, - ], - "saves": [ - { - "user_id": TEST_FAVORITER_ID, - "save_item_id": TEST_TRACK_ID, - "save_type": "track", - "is_current": True, - "is_delete": False, - "created_at": now, - }, - ], - "events": [ - { - "event_type": "remix_contest", - "user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "created_at": now, - "updated_at": now, - "end_date": end_date, - }, - ], - } - populate_mock_db(db, entities) - - with db.scoped_session() as session: - create_fan_remix_contest_ending_soon_notifications(session) - notifications = ( - session.query(Notification) - .filter(Notification.type == NotificationType.FAN_REMIX_CONTEST_ENDING_SOON) - .all() - ) - notified_user_ids = set() - for notification in notifications: - notified_user_ids.update(notification.user_ids) - assert notification.data["entity_user_id"] == TEST_EVENT_CREATOR_ID - assert notification.data["entity_id"] == TEST_TRACK_ID - assert notification.type == NotificationType.FAN_REMIX_CONTEST_ENDING_SOON - assert notification.group_id.startswith("fan_remix_contest_ending_soon:") - # Should notify both the follower and the favoriter - assert TEST_FOLLOWER_ID in notified_user_ids - assert TEST_FAVORITER_ID in notified_user_ids - assert len(notified_user_ids) == 2 - - -def test_fan_remix_contest_ending_soon_notification_no_duplicate_for_follower_and_favoriter( - app, -): - """Test that a user who both follows the creator and saved the track only gets one notification""" - with app.app_context(): - db = get_db() - - now = datetime.now() - BOTH_ID = 4 - end_date = now + timedelta(hours=48) - - entities = { - "users": [ - { - "user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": BOTH_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": TEST_TRACK_ID, - "owner_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - "updated_at": now, - }, - ], - "follows": [ - { - "follower_user_id": BOTH_ID, - "followee_user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - }, - ], - "saves": [ - { - "user_id": BOTH_ID, - "save_item_id": TEST_TRACK_ID, - "save_type": "track", - "is_current": True, - "is_delete": False, - "created_at": now, - }, - ], - "events": [ - { - "event_type": "remix_contest", - "user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "created_at": now, - "updated_at": now, - "end_date": end_date, - }, - ], - } - populate_mock_db(db, entities) - - with db.scoped_session() as session: - create_fan_remix_contest_ending_soon_notifications(session) - notifications = ( - session.query(Notification) - .filter(Notification.type == NotificationType.FAN_REMIX_CONTEST_ENDING_SOON) - .all() - ) - notif_count = 0 - for notification in notifications: - if BOTH_ID in notification.user_ids: - notif_count += 1 - assert notification.data["entity_user_id"] == TEST_EVENT_CREATOR_ID - assert notification.data["entity_id"] == TEST_TRACK_ID - assert ( - notification.type == NotificationType.FAN_REMIX_CONTEST_ENDING_SOON - ) - assert notification.group_id.startswith( - "fan_remix_contest_ending_soon:" - ) - assert notif_count == 1 - - -def test_fan_remix_contest_ending_soon_notification_private_track(app): - """Test that no notification is created for a remix contest on a private (unlisted) track""" - with app.app_context(): - db = get_db() - - now = datetime.now() - end_date = now + timedelta(hours=48) - PRIVATE_TRACK_ID = 200 - PRIVATE_TRACK_OWNER_ID = 5 - PRIVATE_TRACK_FOLLOWER_ID = 6 - - entities = { - "users": [ - { - "user_id": PRIVATE_TRACK_OWNER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": PRIVATE_TRACK_FOLLOWER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": PRIVATE_TRACK_ID, - "owner_id": PRIVATE_TRACK_OWNER_ID, - "is_current": True, - "is_delete": False, - "is_unlisted": True, # Mark as private - "created_at": now, - "updated_at": now, - }, - ], - "follows": [ - { - "follower_user_id": PRIVATE_TRACK_FOLLOWER_ID, - "followee_user_id": PRIVATE_TRACK_OWNER_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - }, - ], - "events": [ - { - "event_type": "remix_contest", - "user_id": PRIVATE_TRACK_OWNER_ID, - "entity_id": PRIVATE_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "created_at": now, - "updated_at": now, - "end_date": end_date, - }, - ], - } - populate_mock_db(db, entities) - - with db.scoped_session() as session: - create_fan_remix_contest_ending_soon_notifications(session) - notifications = ( - session.query(Notification) - .filter(Notification.type == NotificationType.FAN_REMIX_CONTEST_ENDING_SOON) - .all() - ) - # Should not notify anyone for private tracks - assert len(notifications) == 0 diff --git a/packages/discovery-provider/integration_tests/tasks/test_fan_remix_contest_started_notification.py b/packages/discovery-provider/integration_tests/tasks/test_fan_remix_contest_started_notification.py deleted file mode 100644 index 9110e385df0..00000000000 --- a/packages/discovery-provider/integration_tests/tasks/test_fan_remix_contest_started_notification.py +++ /dev/null @@ -1,1068 +0,0 @@ -import json -import logging -from datetime import datetime, timedelta - -from web3 import Web3 -from web3.datastructures import AttributeDict - -from integration_tests.challenges.index_helpers import UpdateTask -from integration_tests.utils import populate_mock_db -from src.challenges.challenge_event_bus import ChallengeEventBus, setup_challenge_bus -from src.models.notifications.notification import Notification -from src.models.tracks.track import Track -from src.queries.get_notifications import NotificationType -from src.tasks.entity_manager.entity_manager import entity_manager_update -from src.tasks.publish_scheduled_releases import _publish_scheduled_releases -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - -TEST_EVENT_CREATOR_ID = 1 -TEST_TRACK_ID = 100 -TEST_PRIVATE_TRACK_ID = 101 -TEST_FOLLOWER_ID = 2 -TEST_FAVORITER_ID = 3 - -default_metadata = { - "cover_art": None, - "cover_art_sizes": "QmdxhDiRUC3zQEKqwnqksaSsSSeHiRghjwKzwoRvm77yaZ", - "tags": "realmagic,theroom", - "genre": "R&B/Soul", - "mood": "Empowering", - "credits_splits": None, - "created_at": "2020-07-11 08:22:15", - "create_date": None, - "updated_at": "2020-07-11 08:22:15", - "release_date": "2020-07-11 08:22:15", - "file_type": None, - "is_playlist_upload": True, - "track_segments": [ - { - "duration": 6.016, - "multihash": "QmabM5svgDgcRdQZaEKSMBCpSZrrYy2y87L8Dx8EQ3T2jp", - } - ], - "has_current_user_reposted": False, - "is_current": True, - "field_visibility": { - "mood": True, - "tags": True, - "genre": True, - "share": True, - "play_count": True, - "remixes": True, - }, - "remix_of": None, - "repost_count": 12, - "save_count": 21, - "description": "some description", - "license": "All rights reserved", - "isrc": None, - "iswc": None, - "stem_of": None, - "ai_attribution_user_id": None, - "orig_file_cid": "original-file-cid", - "orig_filename": "original-filename", - "is_original_available": False, -} - - -def test_fan_remix_contest_started_notification_for_followers_and_favoriters(app): - """Test that remix contest started notification is created for followers and users who favorited the track, - but not for private tracks even if they have followers/favoriters. See handle_event.sql for the logic. - """ - with app.app_context(): - db = get_db() - - now = datetime.now() - - # First: insert all entities except events - entities = { - "users": [ - { - "user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": TEST_FOLLOWER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": TEST_FAVORITER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": TEST_TRACK_ID, - "owner_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "is_unlisted": False, - "created_at": now, - "updated_at": now, - }, - { - "track_id": TEST_PRIVATE_TRACK_ID, - "owner_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "is_unlisted": True, # Private track - "created_at": now, - "updated_at": now, - }, - ], - "follows": [ - { - "follower_user_id": TEST_FOLLOWER_ID, - "followee_user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - } - ], - "saves": [ - { - "user_id": TEST_FAVORITER_ID, - "save_item_id": TEST_TRACK_ID, - "save_type": "track", - "is_current": True, - "is_delete": False, - "created_at": now, - }, - { - "user_id": TEST_FAVORITER_ID, - "save_item_id": TEST_PRIVATE_TRACK_ID, - "save_type": "track", - "is_current": True, - "is_delete": False, - "created_at": now, - }, - ], - # no "events" - } - populate_mock_db(db, entities) - - # Second: insert events for both tracks - event_entities = { - "events": [ - { - "event_type": "remix_contest", - "user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "created_at": now, - "updated_at": now, - }, - { - "event_type": "remix_contest", - "user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_PRIVATE_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "created_at": now, - "updated_at": now, - }, - ] - } - populate_mock_db(db, event_entities) - - with db.scoped_session() as session: - notifications = ( - session.query(Notification) - .filter(Notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED) - .all() - ) - notified_user_ids = set() - for notification in notifications: - notified_user_ids.update(notification.user_ids) - assert notification.data["entity_user_id"] == TEST_EVENT_CREATOR_ID - assert notification.data["entity_id"] == TEST_TRACK_ID - assert notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED - assert notification.group_id.startswith("fan_remix_contest_started:") - # Ensure no notifications were created for the private track - assert notification.data["entity_id"] != TEST_PRIVATE_TRACK_ID - # Should notify both the follower and the favoriter - assert TEST_FOLLOWER_ID in notified_user_ids - assert TEST_FAVORITER_ID in notified_user_ids - assert len(notified_user_ids) == 2 - - -def test_fan_remix_contest_started_notification_no_duplicate_for_follower_and_favoriter( - app, -): - """Test that a user who both follows the creator and saved the track only gets one notification""" - with app.app_context(): - db = get_db() - - now = datetime.now() - BOTH_ID = 4 - - # First: insert all entities except events - entities = { - "users": [ - { - "user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": BOTH_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": TEST_TRACK_ID, - "owner_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - "updated_at": now, - } - ], - "follows": [ - { - "follower_user_id": BOTH_ID, - "followee_user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - } - ], - "saves": [ - { - "user_id": BOTH_ID, - "save_item_id": TEST_TRACK_ID, - "save_type": "track", - "is_current": True, - "is_delete": False, - "created_at": now, - } - ], - # no "events" - } - populate_mock_db(db, entities) - - # Second: insert just the event - event_entities = { - "events": [ - { - "event_type": "remix_contest", - "user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "created_at": now, - "updated_at": now, - } - ] - } - populate_mock_db(db, event_entities) - - with db.scoped_session() as session: - notifications = ( - session.query(Notification) - .filter(Notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED) - .all() - ) - # There should be only one notification for BOTH_ID - notif_count = 0 - for notification in notifications: - if BOTH_ID in notification.user_ids: - notif_count += 1 - assert notification.data["entity_user_id"] == TEST_EVENT_CREATOR_ID - assert notification.data["entity_id"] == TEST_TRACK_ID - assert notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED - assert notification.specifier == str(BOTH_ID) - assert notification.group_id.startswith("fan_remix_contest_started:") - assert notif_count == 1 - - -def test_fan_remix_contest_started_notification_on_track_update(app, mocker): - """Test that notifications are created when a private track with a remix contest becomes public via update""" - with app.app_context(): - db = get_db() - web3 = Web3() - challenge_event_bus: ChallengeEventBus = setup_challenge_bus() - update_task = UpdateTask(web3, challenge_event_bus) - - now = datetime.now() - - # First: insert all entities including a private track - entities = { - "users": [ - { - "user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - "handle": "creator", - "wallet": "creator_wallet", - }, - { - "user_id": TEST_FOLLOWER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": TEST_FAVORITER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": TEST_TRACK_ID, - "owner_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "is_unlisted": True, # Track starts as private - "created_at": now, - "updated_at": now, - } - ], - "follows": [ - { - "follower_user_id": TEST_FOLLOWER_ID, - "followee_user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - } - ], - "saves": [ - { - "user_id": TEST_FAVORITER_ID, - "save_item_id": TEST_TRACK_ID, - "save_type": "track", - "is_current": True, - "is_delete": False, - "created_at": now, - } - ], - "events": [ - { - "event_type": "remix_contest", - "user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "created_at": now, - "updated_at": now, - } - ], - } - populate_mock_db(db, entities) - - # Create update metadata to make track public - update_track_metadata = { - **default_metadata, - "owner_id": TEST_EVENT_CREATOR_ID, - "track_id": TEST_TRACK_ID, - "title": "Public Track NEW NEW NEW", - "is_unlisted": False, # Make track public - } - - update_track_json = json.dumps(update_track_metadata) - update_track_receipt = [ - { - "args": AttributeDict( - { - "_entityId": TEST_TRACK_ID, - "_entityType": "Track", - "_userId": TEST_EVENT_CREATOR_ID, - "_action": "Update", - "_metadata": f'{{"cid": "QmUpdateTrack", "data": {update_track_json}}}', - "_signer": "creator_wallet", - } - ) - } - ] - - def get_events_side_effect(_, tx_receipt): - return update_track_receipt - - mocker.patch( - "src.tasks.entity_manager.entity_manager.get_entity_manager_events_tx", - side_effect=get_events_side_effect, - autospec=True, - ) - - # Verify no notifications exist before update - with db.scoped_session() as session: - notifications = ( - session.query(Notification) - .filter(Notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED) - .all() - ) - assert len(notifications) == 0 - - # Process the update that makes the track public - with db.scoped_session() as session: - entity_manager_update( - update_task, - session, - entity_manager_txs=[ - AttributeDict({"transactionHash": web3.to_bytes(text="UpdateTrack")}) - ], - block_number=1, - block_timestamp=int(now.timestamp()), - block_hash=hex(1), - ) - - # Verify notifications were created after update - notifications = ( - session.query(Notification) - .filter(Notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED) - .all() - ) - notified_user_ids = set() - for notification in notifications: - notified_user_ids.update(notification.user_ids) - assert notification.data["entity_user_id"] == TEST_EVENT_CREATOR_ID - assert notification.data["entity_id"] == TEST_TRACK_ID - assert notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED - assert notification.group_id.startswith("fan_remix_contest_started:") - - # Should notify both the follower and the favoriter - assert TEST_FOLLOWER_ID in notified_user_ids - assert TEST_FAVORITER_ID in notified_user_ids - assert len(notified_user_ids) == 2 - - -def test_fan_remix_contest_started_notification_on_scheduled_release(app): - """Test that notifications are created when a scheduled track with a remix contest becomes public""" - with app.app_context(): - db = get_db() - - now = datetime.now() - past_release_date = now - timedelta( - days=1 - ) # Release date in the past to trigger publish - - # First: insert all entities including a scheduled private track - entities = { - "users": [ - { - "user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": TEST_FOLLOWER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": TEST_FAVORITER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": TEST_TRACK_ID, - "owner_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "is_unlisted": True, # Track starts as private - "is_scheduled_release": True, # This is a scheduled release - "release_date": past_release_date, # Release date in the past - "created_at": now, - "updated_at": now, - "blocknumber": 1, - } - ], - "follows": [ - { - "follower_user_id": TEST_FOLLOWER_ID, - "followee_user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - } - ], - "saves": [ - { - "user_id": TEST_FAVORITER_ID, - "save_item_id": TEST_TRACK_ID, - "save_type": "track", - "is_current": True, - "is_delete": False, - "created_at": now, - } - ], - "events": [ - { - "event_type": "remix_contest", - "user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "created_at": now, - "updated_at": now, - } - ], - } - populate_mock_db(db, entities) - - # Verify no notifications exist before publishing - with db.scoped_session() as session: - notifications = ( - session.query(Notification) - .filter(Notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED) - .all() - ) - assert len(notifications) == 0 - - # Run the scheduled release publish - _publish_scheduled_releases(session) - - # Verify track was made public - track = session.query(Track).filter_by(track_id=TEST_TRACK_ID).first() - assert track.is_unlisted == False - - # Verify notifications were created - notifications = ( - session.query(Notification) - .filter(Notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED) - .all() - ) - notified_user_ids = set() - for notification in notifications: - notified_user_ids.update(notification.user_ids) - assert notification.data["entity_user_id"] == TEST_EVENT_CREATOR_ID - assert notification.data["entity_id"] == TEST_TRACK_ID - assert notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED - assert notification.group_id.startswith("fan_remix_contest_started:") - - # Should notify both the follower and the favoriter - assert TEST_FOLLOWER_ID in notified_user_ids - assert TEST_FAVORITER_ID in notified_user_ids - assert len(notified_user_ids) == 2 - - -def test_fan_remix_contest_started_notification_no_duplicate_on_track_update( - app, mocker -): - """Test that a user who both follows the creator and saved the track only gets one notification - when a private track becomes public via update""" - with app.app_context(): - db = get_db() - web3 = Web3() - challenge_event_bus: ChallengeEventBus = setup_challenge_bus() - update_task = UpdateTask(web3, challenge_event_bus) - - now = datetime.now() - BOTH_ID = 4 - - # First: insert all entities including a private track - entities = { - "users": [ - { - "user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - "handle": "creator", - "wallet": "creator_wallet", - }, - { - "user_id": BOTH_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": TEST_TRACK_ID, - "owner_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "is_unlisted": True, # Track starts as private - "created_at": now, - "updated_at": now, - } - ], - "follows": [ - { - "follower_user_id": BOTH_ID, - "followee_user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - } - ], - "saves": [ - { - "user_id": BOTH_ID, - "save_item_id": TEST_TRACK_ID, - "save_type": "track", - "is_current": True, - "is_delete": False, - "created_at": now, - } - ], - "events": [ - { - "event_type": "remix_contest", - "user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "created_at": now, - "updated_at": now, - } - ], - } - populate_mock_db(db, entities) - - # Create update metadata to make track public - update_track_metadata = { - **default_metadata, - "owner_id": TEST_EVENT_CREATOR_ID, - "track_id": TEST_TRACK_ID, - "title": "Public Track NEW NEW NEW", - "is_unlisted": False, # Make track public - } - - update_track_json = json.dumps(update_track_metadata) - update_track_receipt = [ - { - "args": AttributeDict( - { - "_entityId": TEST_TRACK_ID, - "_entityType": "Track", - "_userId": TEST_EVENT_CREATOR_ID, - "_action": "Update", - "_metadata": f'{{"cid": "QmUpdateTrack", "data": {update_track_json}}}', - "_signer": "creator_wallet", - } - ) - } - ] - - def get_events_side_effect(_, tx_receipt): - return update_track_receipt - - mocker.patch( - "src.tasks.entity_manager.entity_manager.get_entity_manager_events_tx", - side_effect=get_events_side_effect, - autospec=True, - ) - - # Process the update that makes the track public - with db.scoped_session() as session: - entity_manager_update( - update_task, - session, - entity_manager_txs=[ - AttributeDict({"transactionHash": web3.to_bytes(text="UpdateTrack")}) - ], - block_number=1, - block_timestamp=int(now.timestamp()), - block_hash=hex(1), - ) - - # Verify notifications after update - notifications = ( - session.query(Notification) - .filter(Notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED) - .all() - ) - # There should be only one notification for BOTH_ID - notif_count = 0 - for notification in notifications: - if BOTH_ID in notification.user_ids: - notif_count += 1 - assert notification.data["entity_user_id"] == TEST_EVENT_CREATOR_ID - assert notification.data["entity_id"] == TEST_TRACK_ID - assert notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED - assert notification.specifier == str(BOTH_ID) - assert notification.group_id.startswith("fan_remix_contest_started:") - assert notif_count == 1 - - -def test_fan_remix_contest_started_notification_no_duplicate_on_scheduled_release(app): - """Test that a user who both follows the creator and saved the track only gets one notification - when a scheduled private track becomes public""" - with app.app_context(): - db = get_db() - - now = datetime.now() - past_release_date = now - timedelta( - days=1 - ) # Release date in the past to trigger publish - BOTH_ID = 4 - - # First: insert all entities including a scheduled private track - entities = { - "users": [ - { - "user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": BOTH_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": TEST_TRACK_ID, - "owner_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "is_unlisted": True, # Track starts as private - "is_scheduled_release": True, # This is a scheduled release - "release_date": past_release_date, # Release date in the past - "created_at": now, - "updated_at": now, - "blocknumber": 1, - } - ], - "follows": [ - { - "follower_user_id": BOTH_ID, - "followee_user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - } - ], - "saves": [ - { - "user_id": BOTH_ID, - "save_item_id": TEST_TRACK_ID, - "save_type": "track", - "is_current": True, - "is_delete": False, - "created_at": now, - } - ], - "events": [ - { - "event_type": "remix_contest", - "user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "created_at": now, - "updated_at": now, - } - ], - } - populate_mock_db(db, entities) - - with db.scoped_session() as session: - # Run the scheduled release publish - _publish_scheduled_releases(session) - - # Verify track was made public - track = session.query(Track).filter_by(track_id=TEST_TRACK_ID).first() - assert track.is_unlisted == False - - # Verify notifications - notifications = ( - session.query(Notification) - .filter(Notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED) - .all() - ) - # There should be only one notification for BOTH_ID - notif_count = 0 - for notification in notifications: - if BOTH_ID in notification.user_ids: - notif_count += 1 - assert notification.data["entity_user_id"] == TEST_EVENT_CREATOR_ID - assert notification.data["entity_id"] == TEST_TRACK_ID - assert notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED - assert notification.specifier == str(BOTH_ID) - assert notification.group_id.startswith("fan_remix_contest_started:") - assert notif_count == 1 - - -def test_fan_remix_contest_started_notification_no_duplicate_with_existing_on_track_update( - app, mocker -): - """Test that we don't create duplicate notifications when making a track public via update - if an identical notification already exists""" - with app.app_context(): - db = get_db() - web3 = Web3() - challenge_event_bus: ChallengeEventBus = setup_challenge_bus() - update_task = UpdateTask(web3, challenge_event_bus) - - now = datetime.now() - FOLLOWER_ID = 4 - - # First: insert all entities including a private track - entities = { - "users": [ - { - "user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - "handle": "creator", - "wallet": "creator_wallet", - }, - { - "user_id": FOLLOWER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": TEST_TRACK_ID, - "owner_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "is_unlisted": True, # Track starts as private - "created_at": now, - "updated_at": now, - "blocknumber": 1, - } - ], - "follows": [ - { - "follower_user_id": FOLLOWER_ID, - "followee_user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - } - ], - "events": [ - { - "event_type": "remix_contest", - "user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "created_at": now, - "updated_at": now, - } - ], - # Add an existing notification - "notification": [ - { - "blocknumber": 1, - "user_ids": [FOLLOWER_ID], - "timestamp": now, - "type": NotificationType.FAN_REMIX_CONTEST_STARTED, - "specifier": str(FOLLOWER_ID), - "group_id": f"fan_remix_contest_started:{TEST_TRACK_ID}:user:{TEST_EVENT_CREATOR_ID}", - "data": { - "entity_user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_TRACK_ID, - }, - } - ], - } - populate_mock_db(db, entities) - - # Create update metadata to make track public - update_track_metadata = { - **default_metadata, - "owner_id": TEST_EVENT_CREATOR_ID, - "track_id": TEST_TRACK_ID, - "title": "Public Track NEW NEW NEW", - "is_unlisted": False, # Make track public - } - - update_track_json = json.dumps(update_track_metadata) - update_track_receipt = [ - { - "args": AttributeDict( - { - "_entityId": TEST_TRACK_ID, - "_entityType": "Track", - "_userId": TEST_EVENT_CREATOR_ID, - "_action": "Update", - "_metadata": f'{{"cid": "QmUpdateTrack", "data": {update_track_json}}}', - "_signer": "creator_wallet", - } - ) - } - ] - - def get_events_side_effect(_, tx_receipt): - return update_track_receipt - - mocker.patch( - "src.tasks.entity_manager.entity_manager.get_entity_manager_events_tx", - side_effect=get_events_side_effect, - autospec=True, - ) - - # Process the update that makes the track public - with db.scoped_session() as session: - # Count notifications before update - notifications_before = ( - session.query(Notification) - .filter( - Notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED, - ) - .all() - ) - assert len(notifications_before) == 1 - assert notifications_before[0].user_ids == [FOLLOWER_ID] - - entity_manager_update( - update_task, - session, - entity_manager_txs=[ - AttributeDict({"transactionHash": web3.to_bytes(text="UpdateTrack")}) - ], - block_number=1, - block_timestamp=int(now.timestamp()), - block_hash=hex(1), - ) - - # Verify no new notifications were created - notifications_after = ( - session.query(Notification) - .filter( - Notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED, - ) - .all() - ) - assert len(notifications_after) == 1 - assert notifications_after[0].user_ids == [FOLLOWER_ID] - - -def test_fan_remix_contest_started_notification_no_duplicate_with_existing_on_scheduled_release( - app, -): - """Test that we don't create duplicate notifications when making a track public via scheduled release - if an identical notification already exists""" - with app.app_context(): - db = get_db() - - now = datetime.now() - past_release_date = now - timedelta( - days=1 - ) # Release date in the past to trigger publish - FOLLOWER_ID = 4 - - # First: insert all entities including a scheduled private track - entities = { - "users": [ - { - "user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - { - "user_id": FOLLOWER_ID, - "is_current": True, - "created_at": now, - "updated_at": now, - }, - ], - "tracks": [ - { - "track_id": TEST_TRACK_ID, - "owner_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "is_unlisted": True, # Track starts as private - "is_scheduled_release": True, # This is a scheduled release - "release_date": past_release_date, # Release date in the past - "created_at": now, - "updated_at": now, - "blocknumber": 1, - } - ], - "follows": [ - { - "follower_user_id": FOLLOWER_ID, - "followee_user_id": TEST_EVENT_CREATOR_ID, - "is_current": True, - "is_delete": False, - "created_at": now, - } - ], - "events": [ - { - "event_type": "remix_contest", - "user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_TRACK_ID, - "entity_type": "track", - "is_deleted": False, - "created_at": now, - "updated_at": now, - } - ], - # Add an existing notification - "notification": [ - { - "blocknumber": 1, - "user_ids": [FOLLOWER_ID], - "timestamp": now, - "type": NotificationType.FAN_REMIX_CONTEST_STARTED, - "specifier": str(FOLLOWER_ID), - "group_id": f"fan_remix_contest_started:{TEST_TRACK_ID}:user:{TEST_EVENT_CREATOR_ID}", - "data": { - "entity_user_id": TEST_EVENT_CREATOR_ID, - "entity_id": TEST_TRACK_ID, - }, - } - ], - } - populate_mock_db(db, entities) - - with db.scoped_session() as session: - # Count notifications before publish - notifications_before = ( - session.query(Notification) - .filter( - Notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED, - ) - .all() - ) - assert len(notifications_before) == 1 - assert notifications_before[0].user_ids == [FOLLOWER_ID] - - # Run the scheduled release publish - _publish_scheduled_releases(session) - - # Verify track was made public - track = session.query(Track).filter_by(track_id=TEST_TRACK_ID).first() - assert track.is_unlisted == False - - # Verify no new notifications were created - notifications_after = ( - session.query(Notification) - .filter( - Notification.type == NotificationType.FAN_REMIX_CONTEST_STARTED, - ) - .all() - ) - logger.info(f"notifications_after: {notifications_after}") - assert len(notifications_after) == 1 - assert notifications_after[0].user_ids == [FOLLOWER_ID] diff --git a/packages/discovery-provider/src/queries/get_aggregate_route_metrics.py b/packages/discovery-provider/src/queries/get_aggregate_route_metrics.py deleted file mode 100644 index 9d28f60f736..00000000000 --- a/packages/discovery-provider/src/queries/get_aggregate_route_metrics.py +++ /dev/null @@ -1,379 +0,0 @@ -import functools as ft -import logging -from datetime import date, timedelta - -from sqlalchemy import asc, func - -from src import exceptions -from src.models.metrics.aggregate_daily_total_users_metrics import ( - AggregateDailyTotalUsersMetrics, -) -from src.models.metrics.aggregate_daily_unique_users_metrics import ( - AggregateDailyUniqueUsersMetrics, -) -from src.models.metrics.aggregate_monthly_total_users_metrics import ( - AggregateMonthlyTotalUsersMetric, -) -from src.models.metrics.aggregate_monthly_unique_users_metrics import ( - AggregateMonthlyUniqueUsersMetric, -) -from src.utils import db_session - -logger = logging.getLogger(__name__) - - -def get_aggregate_route_metrics(time_range, bucket_size): - """ - Returns a list of timestamps with unique count and total count for all routes - based on given time range and grouped by bucket size - - Returns: - [{ timestamp, unique_count, total_count }] - """ - db = db_session.get_db_read_replica() - with db.scoped_session() as session: - return _get_aggregate_route_metrics(session, time_range, bucket_size) - - -def _get_aggregate_route_metrics(session, time_range, bucket_size): - today = date.today() - seven_days_ago = today - timedelta(days=7) - thirty_days_ago = today - timedelta(days=30) - one_year_ago = today - timedelta(days=365) - first_day_of_month = today.replace(day=1) - - if time_range == "week": - if bucket_size == "day": - unique_counts = ( - session.query( - AggregateDailyUniqueUsersMetrics.timestamp, - AggregateDailyUniqueUsersMetrics.count, - AggregateDailyUniqueUsersMetrics.summed_count, - ) - .filter(seven_days_ago <= AggregateDailyUniqueUsersMetrics.timestamp) - .filter(AggregateDailyUniqueUsersMetrics.timestamp < today) - .order_by(asc("timestamp")) - .all() - ) - unique_count_records = ft.reduce( - lambda acc, curr: acc.update( - {str(curr[0]): {"unique": curr[1], "summed_unique": curr[2] or 0}} - ) - or acc, - unique_counts, - {}, - ) - - total_counts = ( - session.query( - AggregateDailyTotalUsersMetrics.timestamp, - AggregateDailyTotalUsersMetrics.count, - ) - .filter(seven_days_ago <= AggregateDailyTotalUsersMetrics.timestamp) - .filter(AggregateDailyTotalUsersMetrics.timestamp < today) - .order_by(asc("timestamp")) - .all() - ) - total_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - total_counts, - {}, - ) - - metrics = [] - for timestamp, counts in unique_count_records.items(): - if timestamp in total_count_records: - metrics.append( - { - "timestamp": timestamp, - "unique_count": counts["unique"], - "summed_unique_count": counts["summed_unique"], - "total_count": total_count_records[timestamp], - } - ) - return metrics - raise exceptions.ArgumentError("Invalid bucket_size for time_range") - if time_range == "month": - if bucket_size == "day": - unique_counts = ( - session.query( - AggregateDailyUniqueUsersMetrics.timestamp, - AggregateDailyUniqueUsersMetrics.count, - AggregateDailyUniqueUsersMetrics.summed_count, - ) - .filter(thirty_days_ago <= AggregateDailyUniqueUsersMetrics.timestamp) - .filter(AggregateDailyUniqueUsersMetrics.timestamp < today) - .order_by(asc("timestamp")) - .all() - ) - unique_count_records = ft.reduce( - lambda acc, curr: acc.update( - {str(curr[0]): {"unique": curr[1], "summed_unique": curr[2] or 0}} - ) - or acc, - unique_counts, - {}, - ) - - total_counts = ( - session.query( - AggregateDailyTotalUsersMetrics.timestamp, - AggregateDailyTotalUsersMetrics.count, - ) - .filter(thirty_days_ago <= AggregateDailyTotalUsersMetrics.timestamp) - .filter(AggregateDailyTotalUsersMetrics.timestamp < today) - .order_by(asc("timestamp")) - .all() - ) - total_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - total_counts, - {}, - ) - - metrics = [] - for timestamp, counts in unique_count_records.items(): - if timestamp in total_count_records: - metrics.append( - { - "timestamp": timestamp, - "unique_count": counts["unique"], - "summed_unique_count": counts["summed_unique"], - "total_count": total_count_records[timestamp], - } - ) - return metrics - if bucket_size == "week": - unique_counts = ( - session.query( - func.date_trunc( - bucket_size, AggregateDailyUniqueUsersMetrics.timestamp - ).label("timestamp"), - func.sum(AggregateDailyUniqueUsersMetrics.count).label("count"), - func.sum(AggregateDailyUniqueUsersMetrics.summed_count).label( - "summed_count" - ), - ) - .filter(thirty_days_ago <= AggregateDailyUniqueUsersMetrics.timestamp) - .filter(AggregateDailyUniqueUsersMetrics.timestamp < today) - .group_by( - func.date_trunc( - bucket_size, AggregateDailyUniqueUsersMetrics.timestamp - ) - ) - .order_by(asc("timestamp")) - .all() - ) - unique_count_records = ft.reduce( - lambda acc, curr: acc.update( - {str(curr[0]): {"unique": curr[1], "summed_unique": curr[2] or 0}} - ) - or acc, - unique_counts, - {}, - ) - - total_counts = ( - session.query( - func.date_trunc( - bucket_size, AggregateDailyTotalUsersMetrics.timestamp - ).label("timestamp"), - func.sum(AggregateDailyTotalUsersMetrics.count).label("count"), - ) - .filter(thirty_days_ago <= AggregateDailyTotalUsersMetrics.timestamp) - .filter(AggregateDailyTotalUsersMetrics.timestamp < today) - .group_by( - func.date_trunc( - bucket_size, AggregateDailyTotalUsersMetrics.timestamp - ) - ) - .order_by(asc("timestamp")) - .all() - ) - total_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - total_counts, - {}, - ) - - metrics = [] - for timestamp, counts in unique_count_records.items(): - if timestamp in total_count_records: - metrics.append( - { - "timestamp": timestamp, - "unique_count": counts["unique"], - "summed_unique_count": counts["summed_unique"], - "total_count": total_count_records[timestamp], - } - ) - return metrics - raise exceptions.ArgumentError("Invalid bucket_size for time_range") - if time_range == "year": - if bucket_size == "month": - unique_counts = ( - session.query( - AggregateMonthlyUniqueUsersMetric.timestamp, - AggregateMonthlyUniqueUsersMetric.count, - AggregateMonthlyUniqueUsersMetric.summed_count, - ) - .filter( - AggregateMonthlyUniqueUsersMetric.timestamp < first_day_of_month - ) - .filter(one_year_ago <= AggregateMonthlyUniqueUsersMetric.timestamp) - .order_by(asc("timestamp")) - .all() - ) - unique_count_records = ft.reduce( - lambda acc, curr: acc.update( - {str(curr[0]): {"unique": curr[1], "summed_unique": curr[2] or 0}} - ) - or acc, - unique_counts, - {}, - ) - - total_counts = ( - session.query( - AggregateMonthlyTotalUsersMetric.timestamp, - AggregateMonthlyTotalUsersMetric.count, - ) - .filter(AggregateMonthlyTotalUsersMetric.timestamp < first_day_of_month) - .filter(one_year_ago <= AggregateMonthlyTotalUsersMetric.timestamp) - .order_by(asc("timestamp")) - .all() - ) - total_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - total_counts, - {}, - ) - - metrics = [] - for timestamp, counts in unique_count_records.items(): - if timestamp in total_count_records: - metrics.append( - { - "timestamp": timestamp, - "unique_count": counts["unique"], - "summed_unique_count": counts["summed_unique"], - "total_count": total_count_records[timestamp], - } - ) - return metrics - raise exceptions.ArgumentError("Invalid bucket_size for time_range") - if time_range == "all_time": - if bucket_size == "month": - unique_counts = ( - session.query( - AggregateMonthlyUniqueUsersMetric.timestamp, - AggregateMonthlyUniqueUsersMetric.count, - AggregateMonthlyUniqueUsersMetric.summed_count, - ) - .filter( - AggregateMonthlyUniqueUsersMetric.timestamp < first_day_of_month - ) - .order_by(asc("timestamp")) - .all() - ) - unique_count_records = ft.reduce( - lambda acc, curr: acc.update( - {str(curr[0]): {"unique": curr[1], "summed_unique": curr[2] or 0}} - ) - or acc, - unique_counts, - {}, - ) - - total_counts = ( - session.query( - AggregateMonthlyTotalUsersMetric.timestamp, - AggregateMonthlyTotalUsersMetric.count, - ) - .filter(AggregateMonthlyTotalUsersMetric.timestamp < first_day_of_month) - .order_by(asc("timestamp")) - .all() - ) - total_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - total_counts, - {}, - ) - - metrics = [] - for timestamp, counts in unique_count_records.items(): - if timestamp in total_count_records: - metrics.append( - { - "timestamp": timestamp, - "unique_count": counts["unique"], - "summed_unique_count": counts["summed_unique"], - "total_count": total_count_records[timestamp], - } - ) - return metrics - if bucket_size == "week": - unique_counts = ( - session.query( - func.date_trunc( - bucket_size, AggregateDailyUniqueUsersMetrics.timestamp - ).label("timestamp"), - func.sum(AggregateDailyUniqueUsersMetrics.count).label("count"), - func.sum(AggregateDailyUniqueUsersMetrics.summed_count).label( - "summed_count" - ), - ) - .filter(AggregateDailyUniqueUsersMetrics.timestamp < today) - .group_by( - func.date_trunc( - bucket_size, AggregateDailyUniqueUsersMetrics.timestamp - ) - ) - .order_by(asc("timestamp")) - .all() - ) - unique_count_records = ft.reduce( - lambda acc, curr: acc.update( - {str(curr[0]): {"unique": curr[1], "summed_unique": curr[2] or 0}} - ) - or acc, - unique_counts, - {}, - ) - - total_counts = ( - session.query( - func.date_trunc( - bucket_size, AggregateDailyTotalUsersMetrics.timestamp - ).label("timestamp"), - func.sum(AggregateDailyTotalUsersMetrics.count).label("count"), - ) - .filter(AggregateDailyTotalUsersMetrics.timestamp < today) - .group_by( - func.date_trunc( - bucket_size, AggregateDailyTotalUsersMetrics.timestamp - ) - ) - .order_by(asc("timestamp")) - .all() - ) - total_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - total_counts, - {}, - ) - - metrics = [] - for timestamp, counts in unique_count_records.items(): - if timestamp in total_count_records: - metrics.append( - { - "timestamp": timestamp, - "unique_count": counts["unique"], - "summed_unique_count": counts["summed_unique"], - "total_count": total_count_records[timestamp], - } - ) - return metrics - raise exceptions.ArgumentError("Invalid bucket_size for time_range") - raise exceptions.ArgumentError("Invalid time_range") diff --git a/packages/discovery-provider/src/queries/get_app_name_metrics.py b/packages/discovery-provider/src/queries/get_app_name_metrics.py deleted file mode 100644 index b33a9252369..00000000000 --- a/packages/discovery-provider/src/queries/get_app_name_metrics.py +++ /dev/null @@ -1,171 +0,0 @@ -import logging -from datetime import date, timedelta - -from sqlalchemy import asc, desc, func - -from src import exceptions -from src.models.metrics.aggregate_daily_app_name_metrics import ( - AggregateDailyAppNameMetric, -) -from src.models.metrics.aggregate_monthly_app_name_metrics import ( - AggregateMonthlyAppNameMetric, -) -from src.utils import db_session - -logger = logging.getLogger(__name__) - - -def get_historical_app_metrics(): - """ - Returns daily metrics for the last thirty days and all time monthly metrics - - Returns: - { - daily: { - 2021/01/15: {app1: ..., app2: ...} - ... - }, - monthly: { - 2021/01/01: {app1: ..., app2: ...} - ... - } - } - """ - - db = db_session.get_db_read_replica() - with db.scoped_session() as session: - return _get_historical_app_metrics(session) - - -def _get_historical_app_metrics(session, min_count=100): - """ - gets historical app metrics monthly and daily request counts. - - Args: - session: Database session - min_count: Minimum count an app must have in order to be returned - """ - today = date.today() - thirty_days_ago = today - timedelta(days=30) - first_day_of_month = today.replace(day=1) - - daily_query = ( - session.query( - AggregateDailyAppNameMetric.timestamp, - AggregateDailyAppNameMetric.application_name, - AggregateDailyAppNameMetric.count, - ) - .filter(min_count <= AggregateDailyAppNameMetric.count) - .filter(thirty_days_ago <= AggregateDailyAppNameMetric.timestamp) - .filter(AggregateDailyAppNameMetric.timestamp < today) - .all() - ) - daily_metrics = {} - for attribute in daily_query: - day = str(attribute[0]) - if day not in daily_metrics: - daily_metrics[day] = {attribute[1]: attribute[2]} - else: - daily_metrics[day][attribute[1]] = attribute[2] - - monthly_query = ( - session.query( - AggregateMonthlyAppNameMetric.timestamp, - AggregateMonthlyAppNameMetric.application_name, - AggregateMonthlyAppNameMetric.count, - ) - .filter(min_count <= AggregateMonthlyAppNameMetric.count) - .filter(AggregateMonthlyAppNameMetric.timestamp < first_day_of_month) - .all() - ) - monthly_metrics = {} - for attribute in monthly_query: - month = str(attribute[0]) - if month not in monthly_metrics: - monthly_metrics[month] = {attribute[1]: attribute[2]} - else: - monthly_metrics[month][attribute[1]] = attribute[2] - - return {"daily": daily_metrics, "monthly": monthly_metrics} - - -def get_aggregate_app_metrics(time_range, limit): - """ - Returns app name metrics for a given time range - - Args: - time_range: one of "week", "month", "year", or "all_time" - limit: number The max number of apps to return - Returns: - [{ name: string, count: number }, ...] - """ - db = db_session.get_db_read_replica() - with db.scoped_session() as session: - return _get_aggregate_app_metrics(session, time_range, limit) - - -def _get_aggregate_app_metrics(session, time_range, limit): - today = date.today() - seven_days_ago = today - timedelta(days=7) - thirty_days_ago = today - timedelta(days=30) - one_year_ago = today - timedelta(days=365) - - if time_range == "week": - query = ( - session.query( - AggregateDailyAppNameMetric.application_name, - func.sum(AggregateDailyAppNameMetric.count).label("count"), - ) - .filter(seven_days_ago <= AggregateDailyAppNameMetric.timestamp) - .filter(AggregateDailyAppNameMetric.timestamp < today) - .group_by(AggregateDailyAppNameMetric.application_name) - .order_by(desc("count"), asc(AggregateDailyAppNameMetric.application_name)) - .limit(limit) - .all() - ) - elif time_range == "month": - query = ( - session.query( - AggregateDailyAppNameMetric.application_name, - func.sum(AggregateDailyAppNameMetric.count).label("count"), - ) - .filter(thirty_days_ago <= AggregateDailyAppNameMetric.timestamp) - .filter(AggregateDailyAppNameMetric.timestamp < today) - .group_by(AggregateDailyAppNameMetric.application_name) - .order_by(desc("count"), asc(AggregateDailyAppNameMetric.application_name)) - .limit(limit) - .all() - ) - elif time_range == "year": - query = ( - session.query( - AggregateMonthlyAppNameMetric.application_name, - func.sum(AggregateMonthlyAppNameMetric.count).label("count"), - ) - .filter(one_year_ago <= AggregateDailyAppNameMetric.timestamp) - .filter(AggregateMonthlyAppNameMetric.timestamp < today) - .group_by(AggregateMonthlyAppNameMetric.application_name) - .order_by( - desc("count"), asc(AggregateMonthlyAppNameMetric.application_name) - ) - .limit(limit) - .all() - ) - elif time_range == "all_time": - query = ( - session.query( - AggregateMonthlyAppNameMetric.application_name, - func.sum(AggregateMonthlyAppNameMetric.count).label("count"), - ) - .filter(AggregateMonthlyAppNameMetric.timestamp < today) - .group_by(AggregateMonthlyAppNameMetric.application_name) - .order_by( - desc("count"), asc(AggregateMonthlyAppNameMetric.application_name) - ) - .limit(limit) - .all() - ) - else: - raise exceptions.ArgumentError("Invalid time_range") - - return [{"name": item[0], "count": item[1]} for item in query] diff --git a/packages/discovery-provider/src/queries/get_attestation.py b/packages/discovery-provider/src/queries/get_attestation.py deleted file mode 100644 index 3156b825959..00000000000 --- a/packages/discovery-provider/src/queries/get_attestation.py +++ /dev/null @@ -1,257 +0,0 @@ -from datetime import datetime -from typing import Tuple - -import pytz -from eth_keys import keys -from eth_utils.conversions import to_bytes -from hexbytes import HexBytes -from solders.pubkey import Pubkey -from sqlalchemy.orm.session import Session -from sqlalchemy.sql.elements import and_ -from web3 import Web3 - -from src.models.rewards.challenge import Challenge -from src.models.rewards.challenge_disbursement import ChallengeDisbursement -from src.models.rewards.user_challenge import UserChallenge -from src.models.users.user import User -from src.queries.get_disbursed_challenges_amount import ( - get_disbursed_challenges_amount, - get_weekly_pool_window_start, -) -from src.solana.constants import WAUDIO_DECIMALS -from src.tasks.index_oracles import ( - get_oracle_addresses_from_chain, - oracle_addresses_key, -) -from src.utils.config import shared_config -from src.utils.get_all_nodes import ( - get_all_content_nodes_cached, - get_all_discovery_nodes_cached, -) -from src.utils.redis_connection import get_redis - -REWARDS_MANAGER_ACCOUNT = shared_config["solana"]["rewards_manager_account"] -REWARDS_MANAGER_ACCOUNT_PUBLIC_KEY = None -if REWARDS_MANAGER_ACCOUNT: - REWARDS_MANAGER_ACCOUNT_PUBLIC_KEY = Pubkey.from_string(REWARDS_MANAGER_ACCOUNT) -DATETIME_FORMAT_STRING = "%Y-%m-%d %H:%M:%S.%f+00" - - -class Attestation: - """Represents DN attesting to a user completing a given challenge""" - - def __init__( - self, - *, - amount: str, - oracle_address: str, - user_address: str, - challenge_id: str, - challenge_specifier: str, - ): - self.amount = amount - self.oracle_address = oracle_address - self.challenge_id = challenge_id - self.user_address = user_address - self.challenge_specifier = challenge_specifier - - def __repr__(self): - # Format: - # recipient + "_" + amount + "_" + ID (challengeId + specifier) + "_" + oracle_address - return "_".join( - [ - self.user_address, - self.amount, - self._get_combined_id(), - self.oracle_address, - ] - ) - - def _get_combined_id(self): - return f"{self.challenge_id}:{self.challenge_specifier}" - - def _get_encoded_amount(self): - amt = int(self.amount) * 10**WAUDIO_DECIMALS - return amt.to_bytes(8, byteorder="little") - - def get_attestation_bytes(self): - user_bytes = to_bytes(hexstr=self.user_address) - oracle_bytes = to_bytes(hexstr=self.oracle_address) - combined_id_bytes = to_bytes(text=self._get_combined_id()) - amount_bytes = self._get_encoded_amount() - items = [user_bytes, amount_bytes, combined_id_bytes, oracle_bytes] - joined = to_bytes(text="_").join(items) - return joined - - -# Define custom error strings -CHALLENGE_INCOMPLETE = "CHALLENGE_INCOMPLETE" -ALREADY_DISBURSED = "ALREADY_DISBURSED" -INVALID_ORACLE = "INVALID_ORACLE" -MISSING_CHALLENGES = "MISSING_CHALLENGES" -INVALID_INPUT = "INVALID_INPUT" -USER_NOT_FOUND = "USER_NOT_FOUND" -POOL_EXHAUSTED = "POOL_EXHAUSTED" -WAIT_FOR_COOLDOWN = "WAIT_FOR_COOLDOWN" - - -class AttestationError(Exception): - pass - - -def is_valid_oracle(address: str) -> bool: - redis = get_redis() - oracle_addresses = redis.get(oracle_addresses_key) - if oracle_addresses: - oracle_addresses = oracle_addresses.decode().split(",") - else: - oracle_addresses = get_oracle_addresses_from_chain(redis) - return address in oracle_addresses - - -def sign_attestation(attestation_bytes: bytes, private_key: str): - k = keys.PrivateKey(HexBytes(private_key)) - to_sign_hash = Web3.keccak(attestation_bytes) - sig = k.sign_msg_hash(to_sign_hash) - return sig.to_hex() - - -def get_attestation( - session: Session, - *, - challenge_id: str, - user_id: int, - oracle_address: str, - specifier: str, -): - """ - Returns a delegate_owner_wallet, signed_attestation tuple, - or throws an error explaining why the attestation was - not able to be created. - """ - if not user_id or not challenge_id or not oracle_address: - raise AttestationError(INVALID_INPUT) - - # First, validate the oracle adddress - if not is_valid_oracle(oracle_address): - raise AttestationError(INVALID_ORACLE) - - challenges_and_disbursements: Tuple[ - UserChallenge, Challenge, ChallengeDisbursement - ] = ( - session.query(UserChallenge, Challenge, ChallengeDisbursement) - .join(Challenge, Challenge.id == UserChallenge.challenge_id) - # Need to do outerjoin because some challenges - # may not have disbursements - .outerjoin( - ChallengeDisbursement, - and_( - ChallengeDisbursement.specifier == UserChallenge.specifier, - ChallengeDisbursement.challenge_id == UserChallenge.challenge_id, - ), - ) - .filter( - UserChallenge.user_id == user_id, - UserChallenge.challenge_id == challenge_id, - UserChallenge.specifier == specifier, - ) - ).one_or_none() - - if not challenges_and_disbursements: - raise AttestationError(MISSING_CHALLENGES) - user_challenge, challenge, disbursement = ( - challenges_and_disbursements[0], - challenges_and_disbursements[1], - challenges_and_disbursements[2], - ) - if not user_challenge.is_complete: - raise AttestationError(CHALLENGE_INCOMPLETE) - if disbursement: - raise AttestationError(ALREADY_DISBURSED) - now_utc = datetime.now(pytz.UTC) - if challenge.cooldown_days and user_challenge.completed_at: - time_passed = now_utc - pytz.utc.localize(user_challenge.completed_at) - if time_passed.days < challenge.cooldown_days: - raise AttestationError(WAIT_FOR_COOLDOWN) - if challenge.weekly_pool: - disbursed_amount = get_disbursed_challenges_amount( - session, challenge.id, get_weekly_pool_window_start(now_utc) - ) - if disbursed_amount + user_challenge.amount > challenge.weekly_pool: - raise AttestationError(POOL_EXHAUSTED) - - # Get the users's eth address - user_eth_address = ( - session.query(User.wallet) - .filter( - User.is_current == True, - User.user_id == user_id, - User.is_deactivated == False, - ) - .one_or_none() - ) - if not user_eth_address: - raise AttestationError(USER_NOT_FOUND) - user_address = str(user_eth_address[0]) - - attestation = Attestation( - amount=str(user_challenge.amount), - oracle_address=oracle_address, - user_address=user_address, - challenge_id=challenge.id, - challenge_specifier=user_challenge.specifier, - ) - - attestation_bytes = attestation.get_attestation_bytes() - signed_attestation: str = sign_attestation( - attestation_bytes, shared_config["delegate"]["private_key"] - ) - return ( - shared_config["delegate"]["owner_wallet"], - signed_attestation, - ) - - -ADD_SENDER_MESSAGE_PREFIX = "add" - - -def verify_discovery_node_exists_on_chain(new_sender_address: str) -> bool: - redis = get_redis() - nodes = get_all_discovery_nodes_cached(redis) - wallets = set([d["delegateOwnerWallet"] for d in nodes] if nodes else []) - return new_sender_address in wallets - - -def verify_content_node_exists_on_chain(new_sender_address: str) -> bool: - redis = get_redis() - nodes = get_all_content_nodes_cached(redis) - wallets = set([d["delegateOwnerWallet"] for d in nodes] if nodes else []) - return new_sender_address in wallets - - -def get_create_sender_attestation(new_sender_address: str) -> Tuple[str, str]: - """ - Returns a owner_wallet, signed_attestation tuple, - or throws an error explaining why the sender attestation was - not able to be created. - """ - if not REWARDS_MANAGER_ACCOUNT_PUBLIC_KEY: - raise Exception("No Rewards Manager Account initialized") - - is_valid_dn = verify_discovery_node_exists_on_chain(new_sender_address) - is_valid_cn = verify_content_node_exists_on_chain(new_sender_address) - if not is_valid_dn and not is_valid_cn: - raise Exception(f"Expected {new_sender_address} to be registered on chain") - - items = [ - to_bytes(text=ADD_SENDER_MESSAGE_PREFIX), - # Solana PubicKey should be coerced to bytes using the pythonic bytes - bytes(REWARDS_MANAGER_ACCOUNT_PUBLIC_KEY), - to_bytes(hexstr=new_sender_address), - ] - attestation_bytes = to_bytes(text="").join(items) - signed_attestation: str = sign_attestation( - attestation_bytes, shared_config["delegate"]["private_key"] - ) - owner_wallet = shared_config["delegate"]["owner_wallet"] - return owner_wallet, signed_attestation diff --git a/packages/discovery-provider/src/queries/get_challenges.py b/packages/discovery-provider/src/queries/get_challenges.py deleted file mode 100644 index df9df897b2e..00000000000 --- a/packages/discovery-provider/src/queries/get_challenges.py +++ /dev/null @@ -1,314 +0,0 @@ -from collections import defaultdict -from functools import reduce -from typing import DefaultDict, Dict, List, Optional, Tuple, TypedDict, cast - -from sqlalchemy import and_ -from sqlalchemy.orm import Session - -from src.challenges.challenge_event_bus import ChallengeEventBus -from src.challenges.listen_streak_endless_challenge import NUM_DAYS_IN_STREAK -from src.models.rewards.challenge import Challenge, ChallengeType -from src.models.rewards.challenge_disbursement import ChallengeDisbursement -from src.models.rewards.user_challenge import UserChallenge -from src.utils.spl_audio import from_lamports - - -class ChallengeResponse(TypedDict): - challenge_id: str - user_id: int - specifier: str - is_complete: bool - is_active: bool # need this - is_disbursed: bool # need this - current_step_count: Optional[int] - max_steps: Optional[int] - challenge_type: str - amount: str - disbursed_amount: int - cooldown_days: Optional[int] - metadata: Dict - - -def get_disbursements_by_challenge_id( - disbursements: List[ChallengeDisbursement], challenge_id: str -) -> List[ChallengeDisbursement]: - return list(filter(lambda d: d.challenge_id == challenge_id, disbursements)) - - -def get_disbursed_amount(disbursements: List[ChallengeDisbursement]) -> int: - if disbursements is None: - return 0 - return sum( - from_lamports(disbursement.amount) - for disbursement in filter(lambda x: x is not None, disbursements) - ) - - -def rollup_aggregates( - event_bus: ChallengeEventBus, - session: Session, - user_challenges: List[UserChallenge], - parent_challenge: Challenge, - disbursements: List[ChallengeDisbursement], -) -> ChallengeResponse: - step_count = parent_challenge.step_count - num_complete = reduce( - lambda acc, cur: cast(int, acc) + cur.amount if cur.is_complete else acc, - user_challenges, - 0, - ) - amount = parent_challenge.amount - current_step_count = num_complete - - # The parent challenge should have a step count, otherwise, we can just - # say it's complete. - if parent_challenge.id == "o": - # one shot is a special aggregate that's different for every user - # max steps is unique for user so override the parent challenge step count - # each step has a value of 1 - step_count = sum(challenge.amount for challenge in user_challenges) - amount = "1" - is_complete = True - elif parent_challenge.id == "e": - # `step_count` should only reflect the _current_ in-progress streak, so we - # count only the `step_count` of the most recent "full" 7-day streak user_challenge - # row, and sum that with the 1-amount user_challenge rows after that one (if any). - sorted_challenges = sorted( - user_challenges, key=lambda x: x.created_at, reverse=True - ) - most_recent_challenge = sorted_challenges[0] - if most_recent_challenge.is_complete: - current_step_count = 0 - for challenge in sorted_challenges: - if challenge.current_step_count is None: - continue - current_step_count += challenge.current_step_count - if challenge.current_step_count >= NUM_DAYS_IN_STREAK: - break - else: - current_step_count = most_recent_challenge.current_step_count or 0 - is_complete = num_complete >= NUM_DAYS_IN_STREAK - elif parent_challenge.step_count: - is_complete = num_complete >= parent_challenge.step_count - else: - is_complete = True - override_step_count = event_bus.get_manager( - parent_challenge.id - ).get_override_challenge_step_count(session, user_challenges[0].user_id) - if override_step_count is not None: - current_step_count = override_step_count - - response_dict: ChallengeResponse = { - "challenge_id": parent_challenge.id, - "user_id": user_challenges[0].user_id, - "specifier": "", - "is_complete": is_complete, - "current_step_count": current_step_count, - "max_steps": step_count, - "challenge_type": parent_challenge.type, - "is_active": parent_challenge.active, - "is_disbursed": False, # This doesn't indicate anything for aggregate challenges - "amount": amount, - "disbursed_amount": get_disbursed_amount(disbursements), - "cooldown_days": parent_challenge.cooldown_days or 0, - "metadata": {}, - } - return response_dict - - -def to_challenge_response( - user_challenge: UserChallenge, - challenge: Challenge, - disbursements: List[ChallengeDisbursement], - metadata: Dict, -) -> ChallengeResponse: - return { - "challenge_id": challenge.id, - "user_id": user_challenge.user_id, - "specifier": user_challenge.specifier, - "is_complete": user_challenge.is_complete, - "current_step_count": user_challenge.current_step_count, - "max_steps": challenge.step_count, - "challenge_type": challenge.type, - "is_active": challenge.active, - "is_disbursed": disbursements is not None and len(disbursements) > 0, - "amount": challenge.amount, - "disbursed_amount": get_disbursed_amount(disbursements), - "cooldown_days": challenge.cooldown_days or 0, - "metadata": metadata, - } - - -def create_empty_user_challenges( - user_id: int, challenges: List[Challenge], metadatas: List[Dict] -) -> List[ChallengeResponse]: - user_challenges: List[ChallengeResponse] = [] - for i, challenge in enumerate(challenges): - user_challenge: ChallengeResponse = { - "challenge_id": challenge.id, - "user_id": user_id, - "specifier": "", - "is_complete": False, - "current_step_count": 0, - "max_steps": challenge.step_count, - "challenge_type": challenge.type, - "is_active": challenge.active, - "is_disbursed": False, - "amount": challenge.amount, - "disbursed_amount": 0, - "cooldown_days": challenge.cooldown_days or 0, - "metadata": metadatas[i], - } - user_challenges.append(user_challenge) - return user_challenges - - -def get_challenges_metadata( - session: Session, - event_bus: ChallengeEventBus, - challenges: List[UserChallenge], -) -> List[Dict]: - # Break it up into map per challenge type - challenge_map: Dict[str, List[str]] = defaultdict(lambda: []) - specifier_metadata_map: Dict[str, Dict] = {} - for challenge in challenges: - challenge_map[challenge.challenge_id].append(challenge.specifier) - - for challenge_type, specifiers in challenge_map.items(): - manager = event_bus.get_manager(challenge_type) - metadatas = manager.get_metadata(session, specifiers) - for i, specifier in enumerate(specifiers): - metadata = metadatas[i] - specifier_metadata_map[specifier] = metadata - - # Finally, re-sort the metadata - return [specifier_metadata_map[c.specifier] for c in challenges] - - -def get_empty_metadata(event_bus: ChallengeEventBus, challenges: List[Challenge]): - return [event_bus.get_manager(c.id).get_default_metadata() for c in challenges] - - -# gets challenges, returning: -# - any existing user_challenge, rolling up aggregate ones -# - for active, non-hidden challenges, returns default state -# - ignores inactive + completed, unless show_historical is true -def get_challenges( - user_id: int, - show_historical: bool, - session: Session, - event_bus: ChallengeEventBus, -) -> List[ChallengeResponse]: - challenges_and_disbursements: List[Tuple[UserChallenge, ChallengeDisbursement]] = ( - session.query(UserChallenge, ChallengeDisbursement) - # Need to do outerjoin because some challenges - # may not have disbursements - .outerjoin( - ChallengeDisbursement, - and_( - ChallengeDisbursement.specifier == UserChallenge.specifier, - ChallengeDisbursement.challenge_id == UserChallenge.challenge_id, - ), - ).filter(UserChallenge.user_id == user_id) - ).all() - - # Filter to challenges that have active managers - # (in practice, all challenge should) - challenges_and_disbursements = [ - c - for c in challenges_and_disbursements - if event_bus.does_manager_exist(c[0].challenge_id) - ] - - # Combine aggregates - - all_challenges: List[Challenge] = (session.query(Challenge)).all() - all_challenges_map = {challenge.id: challenge for challenge in all_challenges} - - # grab user challenges - # if not historical, filter only to *active* challenges - existing_user_challenges: List[UserChallenge] = [ - i[0] - for i in challenges_and_disbursements - if show_historical or all_challenges_map[i[0].challenge_id].active - ] - disbursements: List[ChallengeDisbursement] = [ - i[1] for i in challenges_and_disbursements if i[1] is not None - ] - regular_user_challenges: List[ChallengeResponse] = [] - aggregate_user_challenges_map: DefaultDict[str, List[UserChallenge]] = defaultdict( - lambda: [] - ) - # Get extra metadata - existing_metadata = get_challenges_metadata( - session, event_bus, existing_user_challenges - ) - for i, user_challenge in enumerate(existing_user_challenges): - parent_challenge = all_challenges_map[user_challenge.challenge_id] - if parent_challenge.type == ChallengeType.aggregate: - aggregate_user_challenges_map[user_challenge.challenge_id].append( - user_challenge - ) - else: - # If we're a trending challenge, don't add if the user_challenge is incomplete - if ( - parent_challenge.type == ChallengeType.trending - and not user_challenge.is_complete - ): - continue - user_challenge_dict = to_challenge_response( - user_challenge, - parent_challenge, - get_disbursements_by_challenge_id( - disbursements, user_challenge.challenge_id - ), - existing_metadata[i], - ) - override_step_count = event_bus.get_manager( - parent_challenge.id - ).get_override_challenge_step_count(session, user_id) - if override_step_count is not None and not user_challenge.is_complete: - user_challenge_dict["current_step_count"] = override_step_count - regular_user_challenges.append(user_challenge_dict) - - rolled_up: List[ChallengeResponse] = [] - for challenge_id, challenges in aggregate_user_challenges_map.items(): - parent_challenge = all_challenges_map[challenge_id] - rolled_up.append( - rollup_aggregates( - event_bus, - session, - challenges, - parent_challenge, - get_disbursements_by_challenge_id(disbursements, challenge_id), - ) - ) - - # Return empty user challenges for active challenges that are non-hidden - # and visible for the current user - active_non_hidden_challenges: List[Challenge] = [ - challenge - for challenge in all_challenges - if ( - challenge.active - and not challenge.type == ChallengeType.trending - and event_bus.get_manager(challenge.id).should_show_challenge_for_user( - session, user_id - ) - ) - ] - existing_challenge_ids = { - user_challenge.challenge_id for user_challenge in existing_user_challenges - } - needs_user_challenge = [ - challenge - for challenge in active_non_hidden_challenges - if challenge.id not in existing_challenge_ids - ] - empty_metadata = get_empty_metadata(event_bus, needs_user_challenge) - empty_challenges = create_empty_user_challenges( - user_id, needs_user_challenge, empty_metadata - ) - - combined = regular_user_challenges + rolled_up + empty_challenges - return combined diff --git a/packages/discovery-provider/src/queries/get_collection_library.py b/packages/discovery-provider/src/queries/get_collection_library.py deleted file mode 100644 index 437d2605c15..00000000000 --- a/packages/discovery-provider/src/queries/get_collection_library.py +++ /dev/null @@ -1,269 +0,0 @@ -import enum -from typing import Optional, TypedDict - -from sqlalchemy import asc, desc, or_ -from sqlalchemy.orm import contains_eager -from sqlalchemy.sql.functions import max - -from src.models.playlists.aggregate_playlist import AggregatePlaylist -from src.models.playlists.playlist import Playlist -from src.models.social.repost import Repost, RepostType -from src.models.social.save import Save, SaveType -from src.models.users.usdc_purchase import PurchaseType, USDCPurchase -from src.models.users.user import User -from src.queries import response_name_constants -from src.queries.get_playlists import add_users_to_playlists -from src.queries.query_helpers import ( - CollectionLibrarySortMethod, - LibraryFilterType, - SortDirection, - add_query_pagination, - filter_playlists_with_only_hidden_tracks, - populate_playlist_metadata, -) -from src.utils import helpers -from src.utils.db_session import get_db_read_replica - - -class CollectionType(str, enum.Enum): - playlist = "playlist" - album = "album" - - -class GetCollectionLibraryArgs(TypedDict): - # The current user logged in (from route param) - user_id: int - - collection_type: CollectionType - - # The maximum number of listens to return - limit: int - - # The offset for the listen history - offset: int - - # One of: all, favorite, repost, purchase - filter_type: LibraryFilterType - - # Include deleted collections - filter_deleted: Optional[bool] - - # Optional filter for the returned results - query: Optional[str] - - sort_direction: Optional[SortDirection] - sort_method: Optional[CollectionLibrarySortMethod] - - -# Most of the code in this file is copied from/similar to src/queries/get_track_library.py - -# see that file for more in depth comments about query structure etc -def _get_collection_library(args: GetCollectionLibraryArgs, session): - user_id = args["user_id"] - limit = args["limit"] - offset = args["offset"] - collection_type = args["collection_type"] - filter_type = args["filter_type"] - - filter_deleted = args.get("filter_deleted", False) - query = args.get("query") - sort_method = args.get("sort_method", CollectionLibrarySortMethod.added_date) - sort_direction = args.get("sort_direction", SortDirection.desc) - - if not filter_type: - raise ValueError("Invalid filter type") - - sort_fn = desc if sort_direction == SortDirection.desc else asc - - own_playlists_base = session.query( - Playlist.playlist_id.label("item_id"), - Playlist.created_at.label("item_created_at"), - ).filter( - Playlist.is_current == True, - Playlist.is_album == (collection_type == CollectionType.album), - Playlist.playlist_owner_id == user_id, - ) - if filter_deleted: - own_playlists_base = own_playlists_base.filter(Playlist.is_delete == False) - - favorites_base = session.query( - Save.save_item_id.label("item_id"), Save.created_at.label("item_created_at") - ).filter( - Save.user_id == user_id, - Save.is_current == True, - Save.is_delete == False, - # Both albums and playlists have SaveType.playlist at the moment, will filter albums with the join as necessary - or_(Save.save_type == SaveType.playlist, Save.save_type == SaveType.album), - ) - - reposts_base = session.query( - Repost.repost_item_id.label("item_id"), - Repost.created_at.label("item_created_at"), - ).filter( - Repost.user_id == user_id, - Repost.is_current == True, - Repost.is_delete == False, - # Both albums and playlists have RepostType.playlist at the moment, will filter albums with the join as necessary - or_( - Repost.repost_type == RepostType.playlist, - Repost.repost_type == RepostType.album, - ), - ) - - purchase_base = session.query( - USDCPurchase.content_id.label("item_id"), - USDCPurchase.created_at.label("item_created_at"), - ).filter( - USDCPurchase.content_type == PurchaseType.album, - USDCPurchase.buyer_user_id == user_id, - ) - - # Union everything for the "all" query - union_subquery = favorites_base.union_all( - reposts_base, own_playlists_base, purchase_base - ).subquery(name="union_subquery") - # Remove dupes - all_base = ( - session.query( - union_subquery.c.item_id, - max(union_subquery.c.item_created_at).label("item_created_at"), - ) - .select_from(union_subquery) - .group_by(union_subquery.c.item_id) - ) - - # Favorites are unioned with own playlists by design - favorites_base = favorites_base.union_all(own_playlists_base) - - subquery = { - LibraryFilterType.all: all_base.subquery("library"), - LibraryFilterType.favorite: favorites_base.subquery(name="favorites_and_own"), - LibraryFilterType.repost: reposts_base.subquery(name="reposts"), - LibraryFilterType.purchase: purchase_base.subquery(name="purchases"), - }[filter_type] - - base_query = ( - session.query(Playlist, subquery.c.item_created_at) - .select_from(subquery) - .join(Playlist, Playlist.playlist_id == subquery.c.item_id) - .filter( - Playlist.is_current == True, - Playlist.is_album == (collection_type == CollectionType.album), - ) - ) - - if filter_deleted: - base_query = base_query.filter(Playlist.is_delete == False) - - if query: - # Need to disable lazy join for this to work - base_query = ( - base_query.join(Playlist.user.of_type(User)) - .options(contains_eager(Playlist.user)) - .filter( - or_( - Playlist.playlist_name.ilike(f"%{query.lower()}%"), - User.name.ilike(f"%{query.lower()}%"), - ) - ) - ) - - # Set sort methods - if sort_method == CollectionLibrarySortMethod.added_date: - base_query = base_query.order_by( - sort_fn(subquery.c.item_created_at), desc(Playlist.playlist_id) - ) - elif sort_method == CollectionLibrarySortMethod.reposts: - base_query = base_query.join( - AggregatePlaylist, - AggregatePlaylist.playlist_id == Playlist.playlist_id, - ).order_by(sort_fn(AggregatePlaylist.repost_count)) - - elif sort_method == CollectionLibrarySortMethod.saves: - base_query = base_query.join( - AggregatePlaylist, - AggregatePlaylist.playlist_id == Playlist.playlist_id, - ).order_by(sort_fn(AggregatePlaylist.save_count)) - - query_results = add_query_pagination(base_query, limit, offset).all() - - if not query_results: - return [] - - playlist_results, activity_timestamp = zip(*query_results) - playlists = helpers.query_result_to_list(playlist_results) - playlist_ids = list(map(lambda playlist: playlist["playlist_id"], playlists)) - - # Populate playlists - playlists = populate_playlist_metadata( - session, - playlist_ids, - playlists, - [RepostType.playlist, RepostType.album], - [SaveType.playlist, SaveType.album], - user_id, - ) - - # exclude hidden playlists and hidden albums which were not previously purchase by current user - album_purchases = set() - album_ids = ( - [p["playlist_id"] for p in playlists] - if collection_type == CollectionType.album - else [] - ) - if album_ids: - album_purchases = ( - session.query(USDCPurchase.content_id) - .filter( - USDCPurchase.buyer_user_id == user_id, - USDCPurchase.content_id.in_(album_ids), - USDCPurchase.content_type == "album", - ) - .all() - ) - album_purchases = set([p[0] for p in album_purchases]) - - # We don't want to filter any of the user's own playlists in the logic below - own_playlist_ids = { - p["playlist_id"] for p in playlists if p["playlist_owner_id"] == user_id - } - - playlists = list( - filter( - lambda playlist: playlist["playlist_id"] in own_playlist_ids - or not playlist["is_private"] - or playlist["playlist_id"] in album_purchases, - playlists, - ) - ) - - playlist_track_ids = set() - for playlist in playlists: - track_ids = set( - map( - lambda t: t["track"], - playlist.get("playlist_contents", {}).get("track_ids", []), - ) - ) - playlist_track_ids = playlist_track_ids.union(track_ids) - - # exclude playlists with only hidden tracks and empty playlists - playlists = filter_playlists_with_only_hidden_tracks( - session, - playlists, - playlist_track_ids, - ignore_ids=list(album_purchases | own_playlist_ids), - ) - - # attach users - playlists = add_users_to_playlists(playlists, session, user_id) - - for idx, playlist in enumerate(playlists): - playlist[response_name_constants.activity_timestamp] = activity_timestamp[idx] - - return playlists - - -def get_collection_library(args: GetCollectionLibraryArgs): - db = get_db_read_replica() - with db.scoped_session() as session: - return _get_collection_library(args, session) diff --git a/packages/discovery-provider/src/queries/get_follow_intersection_users.py b/packages/discovery-provider/src/queries/get_follow_intersection_users.py deleted file mode 100644 index 4bdb5440220..00000000000 --- a/packages/discovery-provider/src/queries/get_follow_intersection_users.py +++ /dev/null @@ -1,41 +0,0 @@ -from sqlalchemy.sql import text - -from src.queries.get_unpopulated_users import get_unpopulated_users -from src.queries.query_helpers import populate_user_metadata -from src.utils.db_session import get_db_read_replica - -sql = text( - """ -select - x.follower_user_id -from follows x -join aggregate_user au on x.follower_user_id = au.user_id -join follows me - on me.follower_user_id = :my_id - and me.followee_user_id = x.follower_user_id - and me.is_delete = false -where x.followee_user_id = :other_user_id - and x.is_delete = false -order by follower_count desc -limit :limit -offset :offset -""" -) - - -def get_follow_intersection_users(args): - users = [] - my_id = args.get("my_id") - - db = get_db_read_replica() - with db.scoped_session() as session: - rows = session.execute(sql, args) - user_ids = [r[0] for r in rows] - - # get all users for above user_ids - users = get_unpopulated_users(session, user_ids) - - # bundle peripheral info into user results - users = populate_user_metadata(session, user_ids, users, my_id) - - return users diff --git a/packages/discovery-provider/src/queries/get_genre_metrics.py b/packages/discovery-provider/src/queries/get_genre_metrics.py deleted file mode 100644 index 180664baef8..00000000000 --- a/packages/discovery-provider/src/queries/get_genre_metrics.py +++ /dev/null @@ -1,49 +0,0 @@ -import logging - -from sqlalchemy import desc, func - -from src.models.tracks.track import Track -from src.utils import db_session -from src.utils.hardcoded_data import genre_allowlist - -logger = logging.getLogger(__name__) - - -def get_genre_metrics(args): - """ - Returns metrics for track genres over the provided bucket - - Args: - args: dict The parsed args from the request - args.offset: number The offset to start querying from - args.limit: number The max number of queries to return - args.start_time: date The start of the query - - Returns: - Array of dictionaries with the play counts and timestamp - """ - db = db_session.get_db_read_replica() - with db.scoped_session() as session: - return _get_genre_metrics(session, args) - - -def _get_genre_metrics(session, args): - metrics_query = ( - session.query(Track.genre, func.count(Track.track_id).label("count")) - .filter( - Track.genre != None, - Track.genre != "", - Track.genre.in_(genre_allowlist), - Track.is_current == True, - Track.created_at > args.get("start_time"), - ) - .group_by(Track.genre) - .order_by(desc("count")) - .limit(args.get("limit")) - .offset(args.get("offset")) - ) - - metrics = metrics_query.all() - genres = [{"name": m[0], "count": m[1]} for m in metrics] - - return genres diff --git a/packages/discovery-provider/src/queries/get_genre_metrics_unit_test.py b/packages/discovery-provider/src/queries/get_genre_metrics_unit_test.py deleted file mode 100644 index 4948559bc05..00000000000 --- a/packages/discovery-provider/src/queries/get_genre_metrics_unit_test.py +++ /dev/null @@ -1,2 +0,0 @@ -def test(): - """See /integration_tests/test_get_genre_metrics.py""" diff --git a/packages/discovery-provider/src/queries/get_historical_route_metrics.py b/packages/discovery-provider/src/queries/get_historical_route_metrics.py deleted file mode 100644 index b4aca68ee1e..00000000000 --- a/packages/discovery-provider/src/queries/get_historical_route_metrics.py +++ /dev/null @@ -1,133 +0,0 @@ -import functools as ft -import logging -from datetime import date, timedelta - -from src.models.metrics.aggregate_daily_total_users_metrics import ( - AggregateDailyTotalUsersMetrics, -) -from src.models.metrics.aggregate_daily_unique_users_metrics import ( - AggregateDailyUniqueUsersMetrics, -) -from src.models.metrics.aggregate_monthly_total_users_metrics import ( - AggregateMonthlyTotalUsersMetric, -) -from src.models.metrics.aggregate_monthly_unique_users_metrics import ( - AggregateMonthlyUniqueUsersMetric, -) -from src.utils import db_session - -logger = logging.getLogger(__name__) - - -def get_historical_route_metrics(): - """ - Returns daily metrics for the last thirty days and all time monthly metrics - - Returns: - { - daily: { - 2021/01/15: {unique_count: ..., total_count: ...} - ... - }, - monthly: { - 2021/01/01: {unique_count: ..., total_count: ...} - ... - } - } - """ - - db = db_session.get_db_read_replica() - with db.scoped_session() as session: - return _get_historical_route_metrics(session) - - -def _get_historical_route_metrics(session): - today = date.today() - thirty_days_ago = today - timedelta(days=30) - first_day_of_month = today.replace(day=1) - - daily_metrics = {} - unique_daily_counts = ( - session.query( - AggregateDailyUniqueUsersMetrics.timestamp, - AggregateDailyUniqueUsersMetrics.count, - AggregateDailyUniqueUsersMetrics.summed_count, - ) - .filter(thirty_days_ago <= AggregateDailyUniqueUsersMetrics.timestamp) - .filter(AggregateDailyUniqueUsersMetrics.timestamp < today) - .all() - ) - unique_daily_count_records = ft.reduce( - lambda acc, curr: acc.update( - {str(curr[0]): {"unique": curr[1], "summed_unique": curr[2] or 0}} - ) - or acc, - unique_daily_counts, - {}, - ) - - total_daily_counts = ( - session.query( - AggregateDailyTotalUsersMetrics.timestamp, - AggregateDailyTotalUsersMetrics.count, - ) - .filter(thirty_days_ago <= AggregateDailyTotalUsersMetrics.timestamp) - .filter(AggregateDailyTotalUsersMetrics.timestamp < today) - .all() - ) - total_daily_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - total_daily_counts, - {}, - ) - - for timestamp, counts in unique_daily_count_records.items(): - if timestamp in total_daily_count_records: - daily_metrics[timestamp] = { - "unique_count": counts["unique"], - "summed_unique_count": counts["summed_unique"], - "total_count": total_daily_count_records[timestamp], - } - - monthly_metrics = {} - unique_monthly_counts = ( - session.query( - AggregateMonthlyUniqueUsersMetric.timestamp, - AggregateMonthlyUniqueUsersMetric.count, - AggregateMonthlyUniqueUsersMetric.summed_count, - ) - .filter(AggregateMonthlyUniqueUsersMetric.timestamp < first_day_of_month) - .all() - ) - unique_monthly_count_records = ft.reduce( - lambda acc, curr: acc.update( - {str(curr[0]): {"unique": curr[1], "summed_unique": curr[2] or 0}} - ) - or acc, - unique_monthly_counts, - {}, - ) - - total_monthly_counts = ( - session.query( - AggregateMonthlyTotalUsersMetric.timestamp, - AggregateMonthlyTotalUsersMetric.count, - ) - .filter(AggregateMonthlyTotalUsersMetric.timestamp < first_day_of_month) - .all() - ) - total_monthly_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - total_monthly_counts, - {}, - ) - - for timestamp, counts in unique_monthly_count_records.items(): - if timestamp in total_monthly_count_records: - monthly_metrics[timestamp] = { - "unique_count": counts["unique"], - "summed_unique_count": counts["summed_unique"], - "total_count": total_monthly_count_records[timestamp], - } - - return {"daily": daily_metrics, "monthly": monthly_metrics} diff --git a/packages/discovery-provider/src/queries/get_notifications.py b/packages/discovery-provider/src/queries/get_notifications.py deleted file mode 100644 index 11d0360f453..00000000000 --- a/packages/discovery-provider/src/queries/get_notifications.py +++ /dev/null @@ -1,719 +0,0 @@ -import logging -from collections import defaultdict -from datetime import datetime -from enum import Enum -from typing import Dict, List, Optional, TypedDict, Union - -from sqlalchemy import bindparam, text -from sqlalchemy.orm.session import Session - -from src.models.tracks.track import Track - -logger = logging.getLogger(__name__) - -notification_groups_sql = text( - """ ---- Create Intervals of user seen -WITH user_seen as ( - SELECT - LAG(seen_at, 1, now()::timestamp) OVER ( ORDER BY seen_at desc ) AS seen_at, - seen_at as prev_seen_at - FROM - notification_seen - WHERE - user_id = :user_id - ORDER BY - seen_at desc -), user_created_at as ( - SELECT - created_at - FROM - users - WHERE - user_id = :user_id - AND is_current -) -SELECT - n.type, - n.group_id as group_id, - array_agg(n.id), - CASE - WHEN user_seen.seen_at is not NULL THEN now()::timestamp != user_seen.seen_at - ELSE EXISTS(SELECT 1 from notification_seen where user_id = :user_id) - END as is_seen, - CASE - WHEN user_seen.seen_at is not NULL THEN user_seen.seen_at - ELSE ( - SELECT seen_at from notification_seen - WHERE user_id = :user_id - ORDER BY seen_at ASC - limit 1 - ) - END as seen_at, - user_seen.prev_seen_at, - count(n.group_id), - max(n.timestamp) as latest_timestamp -FROM - notification n -LEFT JOIN user_seen on - user_seen.seen_at >= n.timestamp and user_seen.prev_seen_at < n.timestamp -WHERE - ((ARRAY[:user_id] && n.user_ids) OR (n.type = 'announcement' AND n.timestamp > (SELECT created_at FROM user_created_at))) AND - (:valid_types is NOT NULL AND n.type in :valid_types) AND - ( - (:timestamp_offset is NULL AND :group_id_offset is NULL) OR - (:timestamp_offset is NULL AND :group_id_offset is NOT NULL AND n.group_id < :group_id_offset) OR - (:timestamp_offset is NOT NULL AND n.timestamp < :timestamp_offset) OR - ( - :group_id_offset is NOT NULL AND :timestamp_offset is NOT NULL AND - (n.timestamp = :timestamp_offset AND n.group_id < :group_id_offset) - ) - ) -GROUP BY - n.type, n.group_id, user_seen.seen_at, user_seen.prev_seen_at -ORDER BY - user_seen.seen_at desc NULLS LAST, - max(n.timestamp) desc, - n.group_id desc -limit :limit; -""" -) -notification_groups_sql = notification_groups_sql.bindparams( - bindparam("valid_types", expanding=True) -) - -unread_notification_count_sql = text( - """ ---- Create Intervals of user seen -WITH user_created_at as ( - SELECT - created_at - FROM - users - WHERE - user_id = :user_id - AND is_current -) -SELECT - count(*) -FROM ( - select n.type, n.group_id - from - notification n - WHERE - ((ARRAY[:user_id] && n.user_ids) OR (n.type = 'announcement' AND n.timestamp > (SELECT created_at FROM user_created_at))) AND - (:valid_types is NOT NULL AND n.type in :valid_types) AND - n.timestamp > COALESCE(( - SELECT - seen_at - FROM - notification_seen - WHERE - user_id = :user_id - ORDER BY seen_at desc - LIMIT 1 - ), '2016-01-01'::timestamp) - GROUP BY - n.type, n.group_id -) user_notifications; -""" -) -unread_notification_count_sql = unread_notification_count_sql.bindparams( - bindparam("valid_types", expanding=True) -) - - -MAX_LIMIT = 50 -DEFAULT_LIMIT = 20 - - -class NotificationGroup(TypedDict): - type: str - group_id: str - notification_ids: List[int] - is_seen: bool - seen_at: datetime - prev_seen_at: datetime - count: int - - -class NotificationType(str, Enum): - ANNOUNCEMENT = "announcement" - FOLLOW = "follow" - REPOST = "repost" - SAVE = "save" - REMIX = "remix" - COSIGN = "cosign" - CREATE = "create" - TIP_RECEIVE = "tip_receive" - TIP_SEND = "tip_send" - CHALLENGE_REWARD = "challenge_reward" - REPOST_OF_REPOST = "repost_of_repost" - SAVE_OF_REPOST = "save_of_repost" - TASTEMAKER = "tastemaker" - REACTION = "reaction" - SUPPORTER_DETRONED = "supporter_dethroned" - SUPPORTER_RANK_UP = "supporter_rank_up" - SUPPORTING_RANK_UP = "supporting_rank_up" - MILESTONE = "milestone" - TRACK_MILESTONE = "track_milestone" - TRACK_ADDED_TO_PLAYLIST = "track_added_to_playlist" - PLAYLIST_MILSTONE = "playlist_milestone" - TIER_CHANGE = "tier_change" - TRENDING = "trending" - TRENDING_PLAYLIST = "trending_playlist" - TRENDING_UNDERGROUND = "trending_underground" - USDC_PURCHASE_BUYER = "usdc_purchase_buyer" - USDC_PURCHASE_SELLER = "usdc_purchase_seller" - TRACK_ADDED_TO_PURCHASED_ALBUM = "track_added_to_purchased_album" - REQUEST_MANAGER = "request_manager" - APPROVE_MANAGER_REQUEST = "approve_manager_request" - CLAIMABLE_REWARD = "claimable_reward" - COMMENT = "comment" - COMMENT_THREAD = "comment_thread" - COMMENT_MENTION = "comment_mention" - COMMENT_REACTION = "comment_reaction" - LISTEN_STREAK_REMINDER = "listen_streak_reminder" - FAN_REMIX_CONTEST_STARTED = "fan_remix_contest_started" - FAN_REMIX_CONTEST_ENDED = "fan_remix_contest_ended" - FAN_REMIX_CONTEST_ENDING_SOON = "fan_remix_contest_ending_soon" - FAN_REMIX_CONTEST_WINNERS_SELECTED = "fan_remix_contest_winners_selected" - ARTIST_REMIX_CONTEST_ENDED = "artist_remix_contest_ended" - ARTIST_REMIX_CONTEST_ENDING_SOON = "artist_remix_contest_ending_soon" - ARTIST_REMIX_CONTEST_SUBMISSIONS = "artist_remix_contest_submissions" - # Indexer/pedalboard emit these types but they were missing from the - # enum, so the parser's `oneof` validation rejected them as - # `valid_types` args and the default list below couldn't include - # them. Add the missing members so the frontend can opt in/out and - # the default-all path returns them. - FAN_REMIX_CONTEST_SUBMISSION = "fan_remix_contest_submission" - FAN_CLUB_TEXT_POST = "fan_club_text_post" - REMIX_CONTEST_UPDATE = "remix_contest_update" - - def __str__(self) -> str: - return str.__str__(self) - - -# Default-on notification types when the client doesn't enumerate them -# explicitly (or sends an empty list). The set mirrors the Go API's -# `oneof=...` whitelist on /v1/notifications (api.audius.co repo, -# api/v1_notifications.go) which is the canonical source of truth for -# "types the frontend knows how to render". Adding a new user-facing -# notification type means appending it here AND to the Go API -# whitelist; deprecated/broken types are owned by the Go API's -# `unsupportedNotificationTypes` deny-list and intentionally omitted -# here too. -default_valid_notification_types = [ - NotificationType.ANNOUNCEMENT, - NotificationType.FOLLOW, - NotificationType.REPOST, - NotificationType.SAVE, - NotificationType.COSIGN, - NotificationType.CREATE, - NotificationType.TIP_RECEIVE, - NotificationType.TIP_SEND, - NotificationType.CHALLENGE_REWARD, - NotificationType.REPOST_OF_REPOST, - NotificationType.SAVE_OF_REPOST, - NotificationType.TASTEMAKER, - NotificationType.REACTION, - NotificationType.SUPPORTER_DETRONED, - NotificationType.SUPPORTER_RANK_UP, - NotificationType.SUPPORTING_RANK_UP, - NotificationType.MILESTONE, - NotificationType.TRACK_ADDED_TO_PLAYLIST, - NotificationType.TIER_CHANGE, - NotificationType.TRENDING, - NotificationType.TRENDING_PLAYLIST, - NotificationType.TRENDING_UNDERGROUND, - NotificationType.USDC_PURCHASE_BUYER, - NotificationType.USDC_PURCHASE_SELLER, - NotificationType.TRACK_ADDED_TO_PURCHASED_ALBUM, - NotificationType.REQUEST_MANAGER, - NotificationType.APPROVE_MANAGER_REQUEST, - NotificationType.CLAIMABLE_REWARD, - NotificationType.COMMENT, - NotificationType.COMMENT_THREAD, - NotificationType.COMMENT_MENTION, - NotificationType.COMMENT_REACTION, - NotificationType.LISTEN_STREAK_REMINDER, - NotificationType.FAN_REMIX_CONTEST_STARTED, - NotificationType.FAN_REMIX_CONTEST_ENDED, - NotificationType.FAN_REMIX_CONTEST_ENDING_SOON, - NotificationType.FAN_REMIX_CONTEST_WINNERS_SELECTED, - NotificationType.FAN_REMIX_CONTEST_SUBMISSION, - NotificationType.ARTIST_REMIX_CONTEST_ENDED, - NotificationType.ARTIST_REMIX_CONTEST_ENDING_SOON, - NotificationType.ARTIST_REMIX_CONTEST_SUBMISSIONS, - NotificationType.FAN_CLUB_TEXT_POST, - NotificationType.REMIX_CONTEST_UPDATE, -] - - -class GetNotificationArgs(TypedDict): - user_id: int - timestamp: Optional[datetime] - group_id: Optional[str] - limit: Optional[int] - valid_types: Optional[List[NotificationType]] - - -def get_notification_groups(session: Session, args: GetNotificationArgs): - """ - Gets the user's notifications in the database - """ - valid_types = args.get("valid_types", default_valid_notification_types) - limit = args.get("limit") or DEFAULT_LIMIT - limit = min(limit, MAX_LIMIT) # type: ignore - - rows = session.execute( - notification_groups_sql, - { - "user_id": args["user_id"], - "limit": limit, - "timestamp_offset": args.get("timestamp", None), - "group_id_offset": args.get("group_id", None), - "valid_types": valid_types, - }, - ) - - res: List[NotificationGroup] = [ - { - "type": r[0], - "group_id": r[1], - "notification_ids": r[2], - "is_seen": r[3] if r[3] != None else False, - "seen_at": r[4], - "prev_seen_at": r[5], - "count": r[6], - } - for r in rows - ] - - return res - - -class FollowNotification(TypedDict): - follower_user_id: int - followee_user_id: int - - -class RepostNotification(TypedDict): - type: str - user_id: int - repost_item_id: int - - -class RepostOfRepostNotification(TypedDict): - type: str - user_id: int - repost_of_repost_item_id: int - - -class SaveOfRepostNotification(TypedDict): - type: str - user_id: int - save_of_repost_item_id: int - - -class TastemakerNotification(TypedDict): - tastemaker_item_id: int - tastemaker_item_owner_id: int - action: str - tastemaker_item_type: str - tastemaker_user_id: int - - -class SaveNotification(TypedDict): - type: str - user_id: int - save_item_id: int - - -class RemixNotification(TypedDict): - parent_track_id: int - track_id: int - - -class CosignRemixNotification(TypedDict): - parent_track_id: int - track_owner_id: int - track_id: int - - -class CreateTrackNotification(TypedDict): - track_id: int - - -class CreatePlaylistNotification(TypedDict): - is_album: bool - playlist_id: int - - -class TipReceiveNotification(TypedDict): - amount: int - sender_user_id: int - receiver_user_id: int - tx_signature: str - reaction_value: Optional[int] - - -class TipSendNotification(TypedDict): - amount: int - sender_user_id: int - receiver_user_id: int - tx_signature: str - - -class ChallengeRewardNotification(TypedDict): - amount: int - specifier: str - challenge_id: str - listen_streak: Optional[int] - - -class ClaimableRewardNotification(TypedDict): - amount: int - specifier: str - challenge_id: str - - -class ReactionNotification(TypedDict): - reacted_to: str - reaction_type: str - reaction_value: int - sender_wallet: str - receiver_user_id: int - sender_user_id: int - tip_amount: int - - -class SupporterRankUpNotification(TypedDict): - rank: int - sender_user_id: int - receiver_user_id: int - - -class SupportingRankUpNotification(TypedDict): - rank: int - sender_user_id: int - receiver_user_id: int - - -class SupporterDethronedNotification(TypedDict): - sender_user_id: int - receiver_user_id: int - dethroned_user_id: int - - -class FollowerMilestoneNotification(TypedDict): - type: str - user_id: int - threshold: int - - -class TrackMilestoneNotification(TypedDict): - type: str - track_id: int - threshold: int - - -class PlaylistMilestoneNotification(TypedDict): - type: str - playlist_id: int - threshold: int - is_album: bool - - -class TierChangeNotification(TypedDict): - new_tier: str - new_tier_value: int - current_value: str - - -class TrackAddedToPlaylistNotification(TypedDict): - track_id: int - playlist_id: int - playlist_owner_id: int - - -class TrackAddedToPurchasedAlbumNotification(TypedDict): - track_id: int - playlist_id: int - playlist_owner_id: int - - -class TrendingNotification(TypedDict): - rank: int - genre: str - track_id: int - time_range: str - - -class TrendingPlaylistNotification(TypedDict): - rank: int - genre: str - playlist_id: int - time_range: str - - -class TrendingUndergroundNotification(TypedDict): - rank: int - genre: str - track_id: int - time_range: str - - -class UsdcPurchaseSellerNotification(TypedDict): - content_type: str - content_id: int - buyer_user_id: int - seller_user_id: int - amount: int - extra_amount: int - - -class UsdcPurchaseBuyerNotification(TypedDict): - content_type: str - content_id: int - buyer_user_id: int - seller_user_id: int - amount: int - extra_amount: int - - -class RequestManagerNotification(TypedDict): - user_id: int - grantee_user_id: int - grantee_address: str - - -class ApproveManagerNotification(TypedDict): - user_id: int - grantee_user_id: int - grantee_address: str - - -class AnnouncementNotification(TypedDict): - title: str - short_description: str - long_description: Optional[str] - - -class CommentNotification(TypedDict): - type: str - entity_id: int - comment_user_id: int - comment_id: Optional[int] - - -class CommentThreadNotification(TypedDict): - type: str - entity_id: int - entity_user_id: int - comment_user_id: int - comment_id: Optional[int] - - -class CommentMentionNotification(TypedDict): - type: str - entity_id: int - entity_user_id: int - comment_user_id: int - comment_id: Optional[int] - - -class CommentReactionNotification(TypedDict): - type: str - entity_id: int - entity_user_id: int - reacter_user_id: int - comment_id: Optional[int] - - -class ListenStreakReminderNotification(TypedDict): - streak: int - - -class FanRemixContestStartedNotification(TypedDict): - entity_user_id: int - entity_id: int - - -class FanRemixContestEndedNotification(TypedDict): - entity_user_id: int - entity_id: int - - -class FanRemixContestEndingSoonNotification(TypedDict): - entity_user_id: int - entity_id: int - - -class FanRemixContestWinnersSelectedNotification(TypedDict): - entity_user_id: int - entity_id: int - - -class ArtistRemixContestEndedNotification(TypedDict): - entity_id: int - - -class ArtistRemixContestEndingSoonNotification(TypedDict): - entity_user_id: int - entity_id: int - - -class ArtistRemixContestSubmissionsNotification(TypedDict): - event_id: int - milestone: int - entity_id: int - - -NotificationData = Union[ - AnnouncementNotification, - FollowNotification, - RepostNotification, - RepostOfRepostNotification, - SaveOfRepostNotification, - SaveNotification, - RemixNotification, - CosignRemixNotification, - CreateTrackNotification, - CreatePlaylistNotification, - TastemakerNotification, - TipReceiveNotification, - TipSendNotification, - TrackAddedToPlaylistNotification, - ChallengeRewardNotification, - ReactionNotification, - SupporterRankUpNotification, - SupportingRankUpNotification, - SupporterDethronedNotification, - FollowerMilestoneNotification, - TrackMilestoneNotification, - PlaylistMilestoneNotification, - TierChangeNotification, - TrendingNotification, - UsdcPurchaseBuyerNotification, - UsdcPurchaseSellerNotification, - CommentNotification, - CommentThreadNotification, - CommentMentionNotification, - CommentReactionNotification, - ListenStreakReminderNotification, - FanRemixContestStartedNotification, - FanRemixContestEndedNotification, - FanRemixContestEndingSoonNotification, - FanRemixContestWinnersSelectedNotification, - ArtistRemixContestEndedNotification, - ArtistRemixContestEndingSoonNotification, - ArtistRemixContestSubmissionsNotification, -] - - -class NotificationAction(TypedDict): - specifier: str - type: str - group_id: str - timestamp: datetime - data: NotificationData - - -class Notification(TypedDict): - type: str - group_id: str - is_seen: bool - seen_at: datetime - actions: List[NotificationAction] - - -notifications_sql = text( - """ -SELECT - id, - type, - specifier, - timestamp, - data, - group_id -FROM notification n -WHERE n.id in :notification_ids -""" -) -notifications_sql = notifications_sql.bindparams( - bindparam("notification_ids", expanding=True) -) - - -def get_notifications(session: Session, args: GetNotificationArgs): - args["valid_types"] = args.get("valid_types", []) + default_valid_notification_types # type: ignore - - notifications = get_notification_groups(session, args) - notification_ids = [] - for notification in notifications: - notification_ids.extend(notification["notification_ids"]) - - rows = session.execute(notifications_sql, {"notification_ids": notification_ids}) - - notification_id_data: Dict[int, NotificationAction] = defaultdict() - for row in rows: - notification_id_data[row[0]] = { - "type": row[1], - "specifier": row[2], - "timestamp": row[3], - "data": row[4], - "group_id": row[5], - } - - notifications_and_actions = [ - { - **notification, - "actions": sorted( - [notification_id_data[id] for id in notification["notification_ids"]], - key=lambda x: x["specifier"], - ), - } - for notification in notifications - ] - - # TODO(PAY-1880): Remove this check after launch - if NotificationType.USDC_PURCHASE_BUYER not in args["valid_types"]: # type: ignore - # Filter out usdc create tracks - filtered: List[NotificationGroup] = [] - for notification in notifications_and_actions: - is_track = "track_id" in notification["actions"][0]["data"] - if notification["type"] == NotificationType.CREATE and is_track: - res = ( - session.query(Track).filter( - Track.track_id == notification["actions"][0]["data"]["track_id"] - ) - ).one_or_none() - if ( - res - and res.stream_conditions - and "usdc_purchase" in res.stream_conditions - ): - # Filter out the notification - continue - filtered.append(notification) - return filtered - - return notifications_and_actions - - -class GetUnreadNotificationCount(TypedDict): - user_id: int - valid_types: Optional[List[NotificationType]] - - -def get_unread_notification_count(session: Session, args: GetUnreadNotificationCount): - args["valid_types"] = args.get("valid_types", []) + default_valid_notification_types # type: ignore - resultproxy = session.execute( - unread_notification_count_sql, - {"user_id": args["user_id"], "valid_types": args.get("valid_types", None)}, - ) - unread_count = 0 - for rowproxy in resultproxy: - unread_count = rowproxy[0] - return unread_count diff --git a/packages/discovery-provider/src/queries/get_personal_route_metrics.py b/packages/discovery-provider/src/queries/get_personal_route_metrics.py deleted file mode 100644 index bb7084d2485..00000000000 --- a/packages/discovery-provider/src/queries/get_personal_route_metrics.py +++ /dev/null @@ -1,304 +0,0 @@ -import functools as ft -import logging -from datetime import date, timedelta - -from sqlalchemy import asc, func - -from src import exceptions -from src.models.metrics.aggregate_daily_total_users_metrics import ( - AggregateDailyTotalUsersMetrics, -) -from src.models.metrics.aggregate_daily_unique_users_metrics import ( - AggregateDailyUniqueUsersMetrics, -) -from src.models.metrics.aggregate_monthly_total_users_metrics import ( - AggregateMonthlyTotalUsersMetric, -) -from src.models.metrics.aggregate_monthly_unique_users_metrics import ( - AggregateMonthlyUniqueUsersMetric, -) -from src.utils import db_session - -logger = logging.getLogger(__name__) - - -def get_personal_route_metrics(time_range, bucket_size): - """ - Returns a list of timestamps with unique count and total count for all routes - on this node based on given time range and grouped by bucket size - - Returns: - [{ timestamp, unique_count, total_count }] - """ - db = db_session.get_db_read_replica() - with db.scoped_session() as session: - return _get_personal_route_metrics(session, time_range, bucket_size) - - -def _get_personal_route_metrics(session, time_range, bucket_size): - today = date.today() - seven_days_ago = today - timedelta(days=7) - thirty_days_ago = today - timedelta(days=30) - first_day_of_month = today.replace(day=1) - - if time_range == "week": - if bucket_size == "day": - unique_counts = ( - session.query( - AggregateDailyUniqueUsersMetrics.timestamp, - AggregateDailyUniqueUsersMetrics.personal_count, - ) - .filter(seven_days_ago <= AggregateDailyUniqueUsersMetrics.timestamp) - .filter(AggregateDailyUniqueUsersMetrics.timestamp < today) - .order_by(asc("timestamp")) - .all() - ) - unique_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - unique_counts, - {}, - ) - - total_counts = ( - session.query( - AggregateDailyTotalUsersMetrics.timestamp, - AggregateDailyTotalUsersMetrics.personal_count, - ) - .filter(seven_days_ago <= AggregateDailyTotalUsersMetrics.timestamp) - .filter(AggregateDailyTotalUsersMetrics.timestamp < today) - .order_by(asc("timestamp")) - .all() - ) - total_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - total_counts, - {}, - ) - - metrics = [] - for timestamp, unique_count in unique_count_records.items(): - if timestamp in total_count_records: - metrics.append( - { - "timestamp": timestamp, - "unique_count": unique_count, - "total_count": total_count_records[timestamp], - } - ) - return metrics - raise exceptions.ArgumentError("Invalid bucket_size for time_range") - if time_range == "month": - if bucket_size == "day": - unique_counts = ( - session.query( - AggregateDailyUniqueUsersMetrics.timestamp, - AggregateDailyUniqueUsersMetrics.personal_count, - ) - .filter(thirty_days_ago <= AggregateDailyUniqueUsersMetrics.timestamp) - .filter(AggregateDailyUniqueUsersMetrics.timestamp < today) - .order_by(asc("timestamp")) - .all() - ) - unique_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - unique_counts, - {}, - ) - - total_counts = ( - session.query( - AggregateDailyTotalUsersMetrics.timestamp, - AggregateDailyTotalUsersMetrics.personal_count, - ) - .filter(thirty_days_ago <= AggregateDailyTotalUsersMetrics.timestamp) - .filter(AggregateDailyTotalUsersMetrics.timestamp < today) - .order_by(asc("timestamp")) - .all() - ) - total_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - total_counts, - {}, - ) - - metrics = [] - for timestamp, unique_count in unique_count_records.items(): - if timestamp in total_count_records: - metrics.append( - { - "timestamp": timestamp, - "unique_count": unique_count, - "total_count": total_count_records[timestamp], - } - ) - return metrics - if bucket_size == "week": - unique_counts = ( - session.query( - func.date_trunc( - bucket_size, AggregateDailyUniqueUsersMetrics.timestamp - ).label("timestamp"), - func.sum(AggregateDailyUniqueUsersMetrics.personal_count).label( - "count" - ), - ) - .filter(thirty_days_ago <= AggregateDailyUniqueUsersMetrics.timestamp) - .filter(AggregateDailyUniqueUsersMetrics.timestamp < today) - .group_by( - func.date_trunc( - bucket_size, AggregateDailyUniqueUsersMetrics.timestamp - ) - ) - .order_by(asc("timestamp")) - .all() - ) - unique_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - unique_counts, - {}, - ) - - total_counts = ( - session.query( - func.date_trunc( - bucket_size, AggregateDailyTotalUsersMetrics.timestamp - ).label("timestamp"), - func.sum(AggregateDailyTotalUsersMetrics.personal_count).label( - "count" - ), - ) - .filter(thirty_days_ago <= AggregateDailyTotalUsersMetrics.timestamp) - .filter(AggregateDailyTotalUsersMetrics.timestamp < today) - .group_by( - func.date_trunc( - bucket_size, AggregateDailyTotalUsersMetrics.timestamp - ) - ) - .order_by(asc("timestamp")) - .all() - ) - total_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - total_counts, - {}, - ) - - metrics = [] - for timestamp, unique_count in unique_count_records.items(): - if timestamp in total_count_records: - metrics.append( - { - "timestamp": timestamp, - "unique_count": unique_count, - "total_count": total_count_records[timestamp], - } - ) - return metrics - raise exceptions.ArgumentError("Invalid bucket_size for time_range") - if time_range == "all_time": - if bucket_size == "month": - unique_counts = ( - session.query( - AggregateMonthlyUniqueUsersMetric.timestamp, - AggregateMonthlyUniqueUsersMetric.personal_count, - ) - .filter( - AggregateMonthlyUniqueUsersMetric.timestamp < first_day_of_month - ) - .order_by(asc("timestamp")) - .all() - ) - unique_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - unique_counts, - {}, - ) - - total_counts = ( - session.query( - AggregateMonthlyTotalUsersMetric.timestamp, - AggregateMonthlyTotalUsersMetric.personal_count, - ) - .filter(AggregateMonthlyTotalUsersMetric.timestamp < first_day_of_month) - .order_by(asc("timestamp")) - .all() - ) - total_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - total_counts, - {}, - ) - - metrics = [] - for timestamp, unique_count in unique_count_records.items(): - if timestamp in total_count_records: - metrics.append( - { - "timestamp": timestamp, - "unique_count": unique_count, - "total_count": total_count_records[timestamp], - } - ) - return metrics - if bucket_size == "week": - unique_counts = ( - session.query( - func.date_trunc( - bucket_size, AggregateDailyUniqueUsersMetrics.timestamp - ).label("timestamp"), - func.sum(AggregateDailyUniqueUsersMetrics.personal_count).label( - "count" - ), - ) - .filter(AggregateDailyUniqueUsersMetrics.timestamp < today) - .group_by( - func.date_trunc( - bucket_size, AggregateDailyUniqueUsersMetrics.timestamp - ) - ) - .order_by(asc("timestamp")) - .all() - ) - unique_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - unique_counts, - {}, - ) - - total_counts = ( - session.query( - func.date_trunc( - bucket_size, AggregateDailyTotalUsersMetrics.timestamp - ).label("timestamp"), - func.sum(AggregateDailyTotalUsersMetrics.personal_count).label( - "count" - ), - ) - .filter(AggregateDailyTotalUsersMetrics.timestamp < today) - .group_by( - func.date_trunc( - bucket_size, AggregateDailyTotalUsersMetrics.timestamp - ) - ) - .order_by(asc("timestamp")) - .all() - ) - total_count_records = ft.reduce( - lambda acc, curr: acc.update({str(curr[0]): curr[1]}) or acc, - total_counts, - {}, - ) - - metrics = [] - for timestamp, unique_count in unique_count_records.items(): - if timestamp in total_count_records: - metrics.append( - { - "timestamp": timestamp, - "unique_count": unique_count, - "total_count": total_count_records[timestamp], - } - ) - return metrics - raise exceptions.ArgumentError("Invalid bucket_size for time_range") - raise exceptions.ArgumentError("Invalid time_range") diff --git a/packages/discovery-provider/src/queries/get_plays_metrics.py b/packages/discovery-provider/src/queries/get_plays_metrics.py deleted file mode 100644 index 10cf81a4e80..00000000000 --- a/packages/discovery-provider/src/queries/get_plays_metrics.py +++ /dev/null @@ -1,62 +0,0 @@ -import logging -import time -from typing import TypedDict - -from sqlalchemy import desc, func -from sqlalchemy.orm.session import Session - -from src.models.social.hourly_play_counts import HourlyPlayCount -from src.utils import db_session - -logger = logging.getLogger(__name__) - - -class GetPlayMetricsArgs(TypedDict): - # A date_trunc operation to aggregate timestamps by - bucket_size: int - - # The max number of responses to return - start_time: int - - # The max number of responses to return - limit: int - - -def get_plays_metrics(args: GetPlayMetricsArgs): - """ - Returns metrics for play counts - - Args: - args: GetPlayMetrics the parsed args from the request - - Returns: - Array of dictionaries with the play counts and timestamp - """ - db = db_session.get_db_read_replica() - with db.scoped_session() as session: - return _get_plays_metrics(session, args) - - -def _get_plays_metrics(session: Session, args: GetPlayMetricsArgs): - metrics_query = ( - session.query( - func.date_trunc( - args.get("bucket_size"), HourlyPlayCount.hourly_timestamp - ).label("timestamp"), - func.sum(HourlyPlayCount.play_count).label("count"), - ) - .filter(HourlyPlayCount.hourly_timestamp > args.get("start_time")) - .group_by( - func.date_trunc(args.get("bucket_size"), HourlyPlayCount.hourly_timestamp) - ) - .order_by(desc("timestamp")) - .limit(args.get("limit")) - ) - - metrics = metrics_query.all() - - metrics = [ - {"timestamp": int(time.mktime(metric[0].timetuple())), "count": metric[1]} - for metric in metrics - ] - return metrics diff --git a/packages/discovery-provider/src/queries/get_plays_metrics_unit_test.py b/packages/discovery-provider/src/queries/get_plays_metrics_unit_test.py deleted file mode 100644 index c74948a8499..00000000000 --- a/packages/discovery-provider/src/queries/get_plays_metrics_unit_test.py +++ /dev/null @@ -1,2 +0,0 @@ -def test(): - """See /integration_tests/test_get_plays_metrics.py""" diff --git a/packages/discovery-provider/src/queries/get_prev_track_entries.py b/packages/discovery-provider/src/queries/get_prev_track_entries.py deleted file mode 100644 index 0fd5ca1b358..00000000000 --- a/packages/discovery-provider/src/queries/get_prev_track_entries.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import List - -from sqlalchemy import and_, func, or_ -from sqlalchemy.orm.session import Session - -from src.models.tracks.track import Track - - -def get_prev_track_entries(session: Session, entries: List[Track]): - """ - Gets the previous state of tracks in the database given a list of tracks. - - Args: - session: (DB) sqlalchemy scoped db session - entries: (List) List of current track entries - - Returns: - prev_track_entries: (List) List of previous track entries corresponding to the passed track entries - """ - - if len(entries) == 0: - return [] - - def get_prev_query_pairs(entry): - return [entry.track_id, entry.blocknumber] - - prev_query_pairs = map(get_prev_query_pairs, entries) - - prev_entries_subquery = ( - session.query( - Track.track_id, func.max(Track.blocknumber).label("max_blocknumber") - ) - .filter( - or_( - *( - and_(Track.track_id == pair[0], Track.blocknumber < pair[1]) - for pair in prev_query_pairs - ) - ) - ) - .group_by(Track.track_id) - .subquery() - ) - - prev_entries_query = session.query(Track).join( - prev_entries_subquery, - and_( - prev_entries_subquery.c.track_id == Track.track_id, - prev_entries_subquery.c.max_blocknumber == Track.blocknumber, - ), - ) - - return prev_entries_query.all() diff --git a/packages/discovery-provider/src/queries/get_related_artists.py b/packages/discovery-provider/src/queries/get_related_artists.py deleted file mode 100644 index 404edca65d3..00000000000 --- a/packages/discovery-provider/src/queries/get_related_artists.py +++ /dev/null @@ -1,108 +0,0 @@ -from sqlalchemy import text -from sqlalchemy.orm import Session - -from src.models.users.user import User -from src.queries.query_helpers import helpers, populate_user_metadata -from src.utils.db_session import get_db_read_replica -from src.utils.helpers import time_method - -_genre_based_sql = text( - """ - with inp as ( - select - genre, - count(*) as track_count, - rank() over (order by count(*) desc) as genre_rank - from - tracks t - where - t.is_current is true - and t.is_delete is false - and t.is_unlisted is false - and t.is_available is true - and t.stem_of is null - and owner_id = :user_id - group by - genre - order by count(*) desc limit 5 - ) - - -- find similar aritst based on similar top genre + follower count - select - user_id, - dominant_genre, - follower_count, - genre_rank - from - aggregate_user au - join inp on dominant_genre = inp.genre and au.follower_count < (select follower_count * 3 from aggregate_user where user_id = :user_id) - where user_id != :user_id - and ( - :filter_followed = false - or :current_user_id is null - or not exists ( - select 1 from follows f - where f.is_current = true - and f.is_delete = false - and f.follower_user_id = :current_user_id - and f.followee_user_id = au.user_id - ) - ) - order by genre_rank asc, follower_count desc - - limit :limit - offset :offset -""" -) - - -def _genre_based_related_artists( - session: Session, - user_id: int, - current_user_id: int, - limit=100, - offset=0, - filter_followed=False, -): - result = session.execute( - _genre_based_sql, - { - "user_id": user_id, - "current_user_id": current_user_id, - "filter_followed": filter_followed, - "limit": limit, - "offset": offset, - }, - ).fetchall() - user_ids = [r["user_id"] for r in result] - - # Get all users in a single query - users_query = session.query(User).filter(User.user_id.in_(user_ids)) - users = users_query.all() - - # Convert to list and create a map of user_id -> user - users_list = helpers.query_result_to_list(users) - users_map = {user["user_id"]: user for user in users_list} - - # Preserve order from original query by mapping user_ids back to users - ordered_users = [users_map[user_id] for user_id in user_ids] - return ordered_users - - -@time_method -def get_related_artists( - user_id: int, - current_user_id: int, - limit: int = 100, - offset: int = 0, - filter_followed: bool = False, -): - db = get_db_read_replica() - users = [] - with db.scoped_session() as session: - users = _genre_based_related_artists( - session, user_id, current_user_id, limit, offset, filter_followed - ) - user_ids = list(map(lambda user: user["user_id"], users)) - users = populate_user_metadata(session, user_ids, users, current_user_id) - return users diff --git a/packages/discovery-provider/src/queries/get_remixable_tracks.py b/packages/discovery-provider/src/queries/get_remixable_tracks.py deleted file mode 100644 index 59c8cc1d91e..00000000000 --- a/packages/discovery-provider/src/queries/get_remixable_tracks.py +++ /dev/null @@ -1,90 +0,0 @@ -from sqlalchemy import desc -from sqlalchemy.orm import aliased - -from src.models.tracks.aggregate_track import AggregateTrack -from src.models.tracks.stem import Stem -from src.models.tracks.track import Track -from src.queries.query_helpers import ( - add_users_to_tracks, - decayed_score, - populate_track_metadata, -) -from src.utils import helpers -from src.utils.db_session import get_db_read_replica - - -def get_remixable_tracks(args): - """Gets a list of remixable tracks""" - db = get_db_read_replica() - limit = args.get("limit", 25) - current_user_id = args.get("current_user_id", None) - - StemTrack = aliased(Track) - - with db.scoped_session() as session: - # Subquery to get current tracks that have stems - remixable_tracks_subquery = ( - session.query(Track) - .join(Stem, Stem.parent_track_id == Track.track_id) - .join(StemTrack, Stem.child_track_id == StemTrack.track_id) - .filter( - Track.is_current == True, - Track.is_unlisted == False, - Track.is_delete == False, - Track.is_stream_gated == False, - StemTrack.is_current == True, - StemTrack.is_unlisted == False, - StemTrack.is_delete == False, - ) - .distinct(Track.track_id) - .subquery() - ) - track_alias = aliased(Track, remixable_tracks_subquery) - - count_subquery = session.query( - AggregateTrack.track_id.label("id"), - (AggregateTrack.repost_count + AggregateTrack.save_count).label("count"), - ).subquery() - - query = ( - session.query( - track_alias, - count_subquery.c["count"], - decayed_score(count_subquery.c["count"], track_alias.created_at).label( - "score" - ), - ) - .join( - count_subquery, - count_subquery.c["id"] == track_alias.track_id, - ) - .order_by(desc("score"), desc(track_alias.track_id)) - .limit(limit) - ) - - results = query.all() - - tracks = [] - for result in results: - track = result[0] - # Convert decimal to float for serialization - score = float(result[-1]) - track = helpers.model_to_dictionary(track) - track["score"] = score - tracks.append(track) - - track_ids = list(map(lambda track: track["track_id"], tracks)) - - # Get user specific data for tracks - tracks = populate_track_metadata(session, track_ids, tracks, current_user_id) - - if args.get("with_users", False): - add_users_to_tracks(session, tracks, current_user_id) - else: - # Remove the user from the tracks - tracks = [ - {key: val for key, val in dict.items() if key != "user"} - for dict in tracks - ] - - return tracks diff --git a/packages/discovery-provider/src/queries/get_remixes_of.py b/packages/discovery-provider/src/queries/get_remixes_of.py deleted file mode 100644 index 8ca976421be..00000000000 --- a/packages/discovery-provider/src/queries/get_remixes_of.py +++ /dev/null @@ -1,159 +0,0 @@ -from sqlalchemy import and_, desc, func, or_ -from sqlalchemy.orm import aliased - -from src import exceptions -from src.models.events.event import Event, EventType -from src.models.social.aggregate_plays import AggregatePlay -from src.models.social.repost import Repost, RepostType -from src.models.social.save import Save, SaveType -from src.models.tracks.aggregate_track import AggregateTrack -from src.models.tracks.remix import Remix -from src.models.tracks.track import Track -from src.queries.get_unpopulated_tracks import get_unpopulated_tracks -from src.queries.query_helpers import ( - RemixesSortMethod, - add_query_pagination, - add_users_to_tracks, - populate_track_metadata, -) -from src.utils import helpers -from src.utils.db_session import get_db_read_replica - -UNPOPULATED_REMIXES_CACHE_DURATION_SEC = 10 - - -def get_remixes_of(args): - track_id = args.get("track_id") - current_user_id = args.get("current_user_id") - limit, offset = args.get("limit"), args.get("offset") - sort_method = args.get("sort_method", "recent") - only_cosigns = args.get("only_cosigns", False) - only_contest_entries = args.get("only_contest_entries", False) - - db = get_db_read_replica() - - with db.scoped_session() as session: - - def get_unpopulated_remixes(): - # Fetch the parent track to get the track's owner id - parent_track_res = get_unpopulated_tracks( - session=session, - track_ids=[track_id], - filter_deleted=False, - filter_unlisted=False, - ) - - if not parent_track_res or parent_track_res[0] is None: - raise exceptions.ArgumentError("Invalid track_id provided") - - parent_track = parent_track_res[0] - track_owner_id = parent_track["owner_id"] - - # Get the 'children' remix tracks - # Use the track owner id to fetch reposted/saved tracks returned first - ParentTrack = aliased(Track) - - # Create a subquery to get only the most recent event per track - # This prevents duplicate rows when a track has multiple remix contest events - DistinctEvent = aliased( - session.query(Event) - .filter( - Event.event_type == EventType.remix_contest, - Event.is_deleted == False, - ) - .distinct(Event.entity_id) - .order_by(Event.entity_id, desc(Event.created_at)) - .subquery() - ) - - base_query = ( - session.query(Track) - .join( - Remix, - and_( - Remix.child_track_id == Track.track_id, - Remix.parent_track_id == track_id, - ), - ) - .outerjoin( - Save, - and_( - Save.save_item_id == Track.track_id, - Save.save_type == SaveType.track, - Save.is_current == True, - Save.is_delete == False, - Save.user_id == track_owner_id, - ), - ) - .outerjoin( - Repost, - and_( - Repost.repost_item_id == Track.track_id, - Repost.user_id == track_owner_id, - Repost.repost_type == RepostType.track, - Repost.is_current == True, - Repost.is_delete == False, - ), - ) - .outerjoin( - AggregateTrack, - AggregateTrack.track_id == Track.track_id, - ) - .outerjoin( - AggregatePlay, - AggregatePlay.play_item_id == Track.track_id, - ) - .outerjoin(ParentTrack, ParentTrack.track_id == Remix.parent_track_id) - .outerjoin( - DistinctEvent, - DistinctEvent.c.entity_id == ParentTrack.track_id, - ) - .filter( - Track.is_current == True, - Track.is_delete == False, - Track.is_unlisted == False, - ) - ) - if only_cosigns: - base_query = base_query.filter( - or_( - ParentTrack.owner_id == Save.user_id, - ParentTrack.owner_id == Repost.user_id, - ) - ) - if only_contest_entries: - base_query = base_query.filter( - and_( - DistinctEvent.c.created_at <= Track.created_at, - Track.created_at <= DistinctEvent.c.end_date, - ) - ) - - if sort_method == RemixesSortMethod.recent: - base_query = base_query.order_by( - desc(Track.created_at), desc(Track.track_id) - ) - elif sort_method == RemixesSortMethod.likes: - base_query = base_query.order_by( - desc(func.coalesce(AggregateTrack.save_count, 0)), - desc(Track.track_id), - ) - elif sort_method == RemixesSortMethod.plays: - base_query = base_query.order_by( - desc(func.coalesce(AggregatePlay.count, 0)), desc(Track.track_id) - ) - - (tracks, count) = add_query_pagination( - base_query, limit, offset, True, True - ) - tracks = tracks.all() - tracks = helpers.query_result_to_list(tracks) - track_ids = list(map(lambda track: track["track_id"], tracks)) - return (tracks, track_ids, count) - - (tracks, track_ids, count) = get_unpopulated_remixes() - tracks = populate_track_metadata(session, track_ids, tracks, current_user_id) - if args.get("with_users", False): - add_users_to_tracks(session, tracks, current_user_id) - - return {"tracks": tracks, "count": count} diff --git a/packages/discovery-provider/src/queries/get_repost_feed_for_user.py b/packages/discovery-provider/src/queries/get_repost_feed_for_user.py deleted file mode 100644 index f24e4c511c8..00000000000 --- a/packages/discovery-provider/src/queries/get_repost_feed_for_user.py +++ /dev/null @@ -1,178 +0,0 @@ -from typing import Optional, TypedDict, cast - -from sqlalchemy import desc -from sqlalchemy.orm.session import Session -from sqlalchemy.sql.elements import and_, or_ - -from src.models.playlists.playlist import Playlist -from src.models.social.repost import Repost, RepostType -from src.models.social.save import SaveType -from src.models.tracks.track import Track -from src.models.users.user import User -from src.queries import response_name_constants -from src.queries.query_helpers import ( - add_query_pagination, - get_users_by_id, - get_users_ids, - populate_playlist_metadata, - populate_track_metadata, -) -from src.utils import helpers -from src.utils.db_session import get_db_read_replica - - -class GetRepostFeedForUserArgs(TypedDict): - offset: int - limit: int - handle: Optional[str] - current_user_id: Optional[int] - with_suers: Optional[bool] - - -def get_repost_feed_for_user(user_id: int, args: GetRepostFeedForUserArgs): - """ - Gets the repost feed for a user (e.g. stalking a user) - - Args: - user_id: number The user id to request the repost feed for - args: GetRepostFeedForUserArgs The parsed args from the request - - Returns: - Array of tracks and playlists (albums) interspersed ordered by - most recent repost - """ - db = get_db_read_replica() - with db.scoped_session() as session: - return _get_repost_feed_for_user(session, user_id, args) - - -def _get_repost_feed_for_user( - session: Session, user_id: int, args: GetRepostFeedForUserArgs -): - feed_results = [] - current_user_id = args.get("current_user_id") - limit = args.get("limit") - offset = args.get("offset") - if "handle" in args: - handle = args.get("handle") or "" - user_id = cast( - int, - session.query(User.user_id) - .filter(User.handle_lc == handle.lower()) - .first(), - ) - - # Query all reposts by a user. - # Outerjoin both tracks and playlists to collect both - # so that a single limit/offset pagination does what we intend when tracks or playlists - # are deleted. - repost_query = ( - session.query(Repost, Track, Playlist) - .outerjoin( - Track, - and_( - Repost.repost_item_id == Track.track_id, - Repost.repost_type == "track", - Track.is_current == True, - Track.is_delete == False, - Track.is_unlisted == False, - Track.stem_of == None, - ), - ) - .outerjoin( - Playlist, - and_( - Repost.repost_item_id == Playlist.playlist_id, - or_(Repost.repost_type == "playlist", Repost.repost_type == "album"), - Playlist.is_current == True, - Playlist.is_delete == False, - Playlist.is_private == False, - ), - ) - .filter( - Repost.is_current == True, - Repost.is_delete == False, - Repost.user_id == user_id, - # Drop rows that have no join found for either track or playlist - or_(Track.track_id != None, Playlist.playlist_id != None), - ) - .order_by( - desc(Repost.created_at), - desc(Repost.repost_item_id), - desc(Repost.repost_type), - ) - ) - - reposts = add_query_pagination(repost_query, limit, offset).all() - # get track reposts from above - track_reposts = [r[0] for r in reposts if r[1] is not None] - track_reposts = helpers.query_result_to_list(track_reposts) - - # get playlist reposts from above - playlist_reposts = [r[0] for r in reposts if r[2] is not None] - playlist_reposts = helpers.query_result_to_list(playlist_reposts) - - # build track/playlist id --> repost dict from repost lists - track_repost_dict = {repost["repost_item_id"]: repost for repost in track_reposts} - playlist_repost_dict = { - repost["repost_item_id"]: repost for repost in playlist_reposts - } - - tracks = helpers.query_result_to_list( - filter(None, [repost[1] for repost in reposts]) - ) - playlists = helpers.query_result_to_list( - filter(None, [repost[2] for repost in reposts]) - ) - - # get track ids - track_ids = [track["track_id"] for track in tracks] - - # get playlist ids - playlist_ids = [playlist["playlist_id"] for playlist in playlists] - - # populate full metadata - tracks = populate_track_metadata(session, track_ids, tracks, current_user_id) - playlists = populate_playlist_metadata( - session, - playlist_ids, - playlists, - [RepostType.playlist, RepostType.album], - [SaveType.playlist, SaveType.album], - current_user_id, - ) - - # add activity timestamps - for track in tracks: - track[response_name_constants.activity_timestamp] = track_repost_dict[ - track["track_id"] - ]["created_at"] - - for playlist in playlists: - playlist[response_name_constants.activity_timestamp] = playlist_repost_dict[ - playlist["playlist_id"] - ]["created_at"] - - unsorted_feed = tracks + playlists - - # sort feed by repost timestamp desc - feed_results = sorted( - unsorted_feed, - key=lambda entry: entry[response_name_constants.activity_timestamp], - reverse=True, - ) - - if args.get("with_users", False): - user_id_list = get_users_ids(feed_results) - users = get_users_by_id(session, user_id_list, current_user_id=current_user_id) - for result in feed_results: - if "playlist_owner_id" in result: - user = users[result["playlist_owner_id"]] - if user: - result["user"] = user - elif "owner_id" in result: - user = users[result["owner_id"]] - if user: - result["user"] = user - - return feed_results diff --git a/packages/discovery-provider/src/queries/get_tips.py b/packages/discovery-provider/src/queries/get_tips.py deleted file mode 100644 index f0296078ef0..00000000000 --- a/packages/discovery-provider/src/queries/get_tips.py +++ /dev/null @@ -1,339 +0,0 @@ -import logging -from datetime import datetime -from typing import List, Tuple, TypedDict, Union, cast - -from sqlalchemy import func, or_ -from sqlalchemy.orm import Query, aliased -from sqlalchemy.orm.session import Session - -from src.models.social.follow import Follow -from src.models.users.aggregate_user import AggregateUser -from src.models.users.aggregate_user_tips import AggregateUserTip -from src.models.users.user import User -from src.models.users.user_tip import UserTip -from src.queries.get_unpopulated_users import get_unpopulated_users -from src.queries.query_helpers import paginate_query, populate_user_metadata -from src.utils.db_session import get_db - -logger = logging.getLogger(__name__) - - -class GetTipsArgs(TypedDict): - limit: int - offset: int - user_id: int - receiver_min_followers: int - receiver_is_verified: bool - current_user_follows: str - unique_by: str - min_slot: int - max_slot: int - tx_signatures: List[int] - exclude_recipients: List[int] - - -class TipResult(TypedDict): - amount: int - sender: str - receiver: str - slot: int - created_at: datetime - followee_supporters: List[str] - tx_signature: str - - -class PopulatedTipResult(TypedDict): - amount: int - sender: User - receiver: User - slot: int - created_at: datetime - followee_supporters: List[str] - tx_signature: str - - -# Example of query with inputs: -# limit=100 -# offset=0 -# user_id=8 -# current_user_follows="sender_or_receiver" -# receiver_is_verified=True, -# receiver_min_followers=10 -# min_slot=185524 -# max_slot=185673 -# --------------------------------------------------------- -# WITH followees AS -# ( -# SELECT -# follows.followee_user_id AS followee_user_id -# FROM -# follows -# WHERE -# follows.is_current = true -# AND follows.is_delete = false -# AND follows.follower_user_id = 8 -# ) -# , -# tips AS -# ( -# SELECT -# user_tips_1.signature AS signature, -# user_tips_1.slot AS slot, -# user_tips_1.sender_user_id AS sender_user_id, -# user_tips_1.receiver_user_id AS receiver_user_id, -# user_tips_1.amount AS amount, -# user_tips_1.created_at AS created_at, -# user_tips_1.updated_at AS updated_at -# FROM -# user_tips AS user_tips_1 -# JOIN -# aggregate_user -# ON aggregate_user.user_id = user_tips_1.receiver_user_id -# JOIN -# users -# ON users.user_id = user_tips_1.receiver_user_id -# LEFT OUTER JOIN -# followees AS followees_for_sender -# ON user_tips_1.sender_user_id = followees_for_sender.followee_user_id -# LEFT OUTER JOIN -# followees AS followees_for_receiver -# ON user_tips_1.receiver_user_id = followees_for_receiver.followee_user_id -# WHERE -# aggregate_user.follower_count >= 10 -# AND users.is_current = true -# AND users.is_verified = true -# AND user_tips_1.slot >= 185524 -# AND user_tips_1.slot <= 185673 -# AND -# ( -# followees_for_sender.followee_user_id IS NOT NULL -# OR followees_for_receiver.followee_user_id IS NOT NULL -# ) -# ORDER BY -# user_tips_1.slot DESC LIMIT 100 OFFSET 0 -# ) -# , -# followee_tippers AS -# ( -# SELECT -# aggregate_user_tips.sender_user_id AS sender_user_id, -# aggregate_user_tips.receiver_user_id AS receiver_user_id, -# followees_for_aggregate.followee_user_id AS followee_user_id -# FROM -# followees AS followees_for_aggregate -# LEFT OUTER JOIN -# aggregate_user_tips -# ON aggregate_user_tips.sender_user_id = followees_for_aggregate.followee_user_id -# ) -# SELECT -# tips.signature, -# tips.slot, -# tips.sender_user_id, -# tips.receiver_user_id, -# tips.amount, -# tips.created_at, -# tips.updated_at, -# array_agg(followee_tippers.sender_user_id) AS array_agg_1 -# FROM -# tips -# LEFT OUTER JOIN -# followee_tippers -# ON followee_tippers.receiver_user_id = tips.receiver_user_id -# GROUP BY -# tips.signature, -# tips.slot, -# tips.sender_user_id, -# tips.receiver_user_id, -# tips.amount, -# tips.created_at, -# tips.updated_at -# ORDER BY -# tips.slot DESC - - -def _get_tips(session: Session, args: GetTipsArgs): - UserTipAlias = aliased(UserTip) - query: Query = session.query(UserTipAlias) - has_pagination = False # Keeps track if we already paginated - - if args.get("exclude_recipients"): - query = query.filter( - UserTipAlias.receiver_user_id.notin_(args["exclude_recipients"]) - ) - - if args.get("tx_signatures"): - query = query.filter(UserTipAlias.signature.in_(args["tx_signatures"])) - if args.get("receiver_min_followers", 0) > 0: - query = query.join( - AggregateUser, AggregateUser.user_id == UserTipAlias.receiver_user_id - ).filter(AggregateUser.follower_count >= args["receiver_min_followers"]) - - if args.get("receiver_is_verified", False): - query = query.join(User, User.user_id == UserTipAlias.receiver_user_id).filter( - User.is_current == True, User.is_verified == True - ) - if args.get("min_slot", 0) > 0: - query = query.filter(UserTipAlias.slot >= args["min_slot"]) - if args.get("max_slot", 0) > 0: - query = query.filter(UserTipAlias.slot <= args["max_slot"]) - if args.get("unique_by"): - if args["unique_by"] == "sender": - distinct_inner = ( - query.order_by( - UserTipAlias.sender_user_id.asc(), UserTipAlias.slot.desc() - ) - .distinct(UserTipAlias.sender_user_id) - .subquery() - ) - UserTipAlias = aliased(UserTip, distinct_inner, name="user_tips_uniqued") - query = session.query(UserTipAlias) - elif args["unique_by"] == "receiver": - distinct_inner = ( - query.order_by( - UserTipAlias.receiver_user_id.asc(), UserTipAlias.slot.desc() - ) - .distinct(UserTipAlias.receiver_user_id) - .subquery() - ) - UserTipAlias = aliased(UserTip, distinct_inner, name="user_tips_uniqued") - query = session.query(UserTipAlias) - - if args.get("user_id"): - # We have to get the other users that this user follows for three potential uses: - # 1) To filter tips to recipients the user follows (if necessary) - # 2) To filter tips to senders the user follows (if necessary) - # 3) To get the followees of the current user that have also tipped the receiver - followees_query = ( - session.query(Follow.followee_user_id) - .filter( - Follow.is_current == True, - Follow.is_delete == False, - Follow.follower_user_id == args["user_id"], - ) - .cte("followees") - ) - # First, filter the senders/receivers as necessary - if args.get("current_user_follows"): - FolloweesSender = aliased(followees_query, name="followees_for_sender") - FolloweesReceiver = aliased(followees_query, name="followees_for_receiver") - if args["current_user_follows"] == "receiver": - query = query.join( - FolloweesReceiver, - UserTipAlias.receiver_user_id - == FolloweesReceiver.c.followee_user_id, - ) - elif args["current_user_follows"] == "sender": - query = query.join( - FolloweesSender, - UserTipAlias.receiver_user_id == FolloweesSender.c.followee_user_id, - ) - elif args["current_user_follows"] == "sender_or_receiver": - query = query.outerjoin( - FolloweesSender, - UserTipAlias.sender_user_id == FolloweesSender.c.followee_user_id, - ) - query = query.outerjoin( - FolloweesReceiver, - UserTipAlias.receiver_user_id - == FolloweesReceiver.c.followee_user_id, - ) - query = query.filter( - or_( - FolloweesSender.c.followee_user_id != None, - FolloweesReceiver.c.followee_user_id != None, - ) - ) - - # Order and paginate before adding follower filters/aggregates - query = query.order_by(UserTipAlias.slot.desc()) - query = paginate_query(query) - - # Get the tips for the user as a subquery - # because now we need to get the other users that tipped that receiver - # and joining on this already paginated/limited result will be much faster - Tips = query.cte("tips") - FolloweesAggregate = aliased(followees_query, name="followees_for_aggregate") - - # Get all of the followees joined on their aggregate user tips first - # rather than joining each on the tips separately to help with speed - FolloweeTippers = ( - session.query( - AggregateUserTip.sender_user_id, - AggregateUserTip.receiver_user_id, - FolloweesAggregate.c.followee_user_id, - ) - .select_from(FolloweesAggregate) - .outerjoin( - AggregateUserTip, - AggregateUserTip.sender_user_id - == FolloweesAggregate.c.followee_user_id, - ) - .cte("followee_tippers") - ) - # Now we have the tips listed multiple times, one for each followee sender. - # So group by the tip and aggregate up the followee sender IDs into a list - query = ( - session.query(UserTip, func.array_agg(FolloweeTippers.c.sender_user_id)) - .select_entity_from(Tips) - .outerjoin( - FolloweeTippers, - FolloweeTippers.c.receiver_user_id == Tips.c.receiver_user_id, - ) - .group_by(Tips) - ) - has_pagination = True - - query = query.order_by(UserTipAlias.slot.desc()) - if not has_pagination: - query = paginate_query(query) - - tips_results: List[UserTip] = query.all() - return tips_results - - -def get_tips(args: GetTipsArgs, db=None) -> List[PopulatedTipResult]: - db = get_db() - with db.scoped_session() as session: - results: Union[List[Tuple[UserTip, List[str]]], List[UserTip]] = _get_tips( - session, args - ) - tips_results: List[Tuple[UserTip, List[str]]] = [] - # Wrap in tuple for consistency - if results and isinstance(results[0], UserTip): - tips_results = [(cast(UserTip, tip), []) for tip in results] - else: - # MyPy doesn't seem smart enough to figure this out, help it with a cast - tips_results = cast(List[Tuple[UserTip, List[str]]], results) - - # Collect user IDs and fetch users - user_ids = set() - for result in tips_results: - user_ids.add(result[0].sender_user_id) - user_ids.add(result[0].receiver_user_id) - users = get_unpopulated_users(session, user_ids) - users = populate_user_metadata( - session, user_ids, users, args["user_id"] if "user_id" in args else None - ) - users_map = {} - for user in users: - users_map[user["user_id"]] = user - - # Not using model_to_dictionary() here because TypedDict complains about dynamic keys - tips: List[PopulatedTipResult] = [ - { - "amount": result[0].amount, - "sender": users_map[result[0].sender_user_id], - "receiver": users_map[result[0].receiver_user_id], - "followee_supporters": list( - filter( - lambda id: id is not None, - result[1], - ) - ), - "slot": result[0].slot, - "created_at": result[0].created_at, - "tx_signature": result[0].signature, - } - for result in tips_results - ] - return tips diff --git a/packages/discovery-provider/src/queries/get_top_genre_users.py b/packages/discovery-provider/src/queries/get_top_genre_users.py deleted file mode 100644 index 68b534a5a34..00000000000 --- a/packages/discovery-provider/src/queries/get_top_genre_users.py +++ /dev/null @@ -1,53 +0,0 @@ -import logging - -from sqlalchemy import asc, desc - -from src.models.users.aggregate_user import AggregateUser -from src.models.users.user import User -from src.queries.query_helpers import add_query_pagination, populate_user_metadata -from src.utils import helpers -from src.utils.db_session import get_db_read_replica - -logger = logging.getLogger(__name__) - - -def get_top_genre_users(args): - db = get_db_read_replica() - with db.scoped_session() as session: - return _get_top_genre_users(session, args) - - -def _get_top_genre_users(session, args): - genres = [] - if "genre" in args: - genres = args.get("genre") - if isinstance(genres, str): - genres = [genres] - - with_users = args.get("with_users", True) - limit = args.get("limit", 10) - offset = args.get("offset", 0) - - top_users_query = ( - session.query(User) - .join(AggregateUser, User.user_id == AggregateUser.user_id) - .filter( - AggregateUser.dominant_genre.in_(genres), - User.is_deactivated == False, - User.is_available == True, - ) - .order_by(desc(AggregateUser.follower_count), asc(User.user_id)) - ) - users = add_query_pagination(top_users_query, limit, offset).all() - users = helpers.query_result_to_list(users) - user_ids = list(map(lambda user: user["user_id"], users)) - - if with_users: - users = populate_user_metadata(session, user_ids, users, None) - - # Sort the users so that it's in the same order as the previous query - user_map = {user["user_id"]: user for user in users} - users = [user_map[user_id] for user_id in user_ids] - return users - - return user_ids diff --git a/packages/discovery-provider/src/queries/get_top_listeners_for_track.py b/packages/discovery-provider/src/queries/get_top_listeners_for_track.py deleted file mode 100644 index c34d0346f9c..00000000000 --- a/packages/discovery-provider/src/queries/get_top_listeners_for_track.py +++ /dev/null @@ -1,69 +0,0 @@ -from sqlalchemy import text - -from src import exceptions -from src.models.tracks.track import Track -from src.queries.get_unpopulated_users import get_unpopulated_users -from src.queries.query_helpers import populate_user_metadata -from src.utils.db_session import get_db_read_replica - - -def get_top_listeners_for_track(args): - db = get_db_read_replica() - with db.scoped_session() as session: - return _get_top_listeners_for_track(session, args) - - -def _get_top_listeners_for_track(session, args): - users_with_count = [] - current_user_id = args.get("current_user_id") - track_id = args.get("track_id") - limit = args.get("limit") - offset = args.get("offset") - - # Ensure Track exists for provided track_id. - track_entry = ( - session.query(Track) - .filter(Track.track_id == track_id, Track.is_current == True) - .first() - ) - if track_entry is None: - raise exceptions.NotFoundError("Resource not found for provided track id") - - cid_source_res = text( - """ - with - deduped as ( - select distinct play_item_id, user_id, date_trunc('hour', created_at) as created_at - from plays - where user_id is not null - and play_item_id = :track_id - ), - counted as ( - select user_id, count(*) as play_count - from deduped - group by 1 - ) - select * - from counted - left join aggregate_user using (user_id) - order by play_count desc, follower_count desc, counted.user_id asc - limit :limit - offset :offset - """ - ) - top_listener_rows = session.execute( - cid_source_res, {"track_id": track_id, "limit": limit, "offset": offset} - ).fetchall() - - top_listeners = {r["user_id"]: r["play_count"] for r in top_listener_rows} - - # Fix format to return only Users objects with follower_count field. - user_ids = top_listeners.keys() - users = get_unpopulated_users(session, user_ids) - users = populate_user_metadata(session, user_ids, users, current_user_id) - - users_with_count = [ - {"user": u, "count": top_listeners[u["user_id"]]} for u in users - ] - - return users_with_count diff --git a/packages/discovery-provider/src/queries/get_top_user_track_tags.py b/packages/discovery-provider/src/queries/get_top_user_track_tags.py deleted file mode 100644 index 08fdd0e1d02..00000000000 --- a/packages/discovery-provider/src/queries/get_top_user_track_tags.py +++ /dev/null @@ -1,45 +0,0 @@ -import logging # pylint: disable=C0302 - -from sqlalchemy import desc, func - -from src.models.tracks.tag_track_user_matview import t_tag_track_user -from src.models.tracks.track import Track -from src.utils.db_session import get_db_read_replica - -logger = logging.getLogger(__name__) - - -def get_top_user_track_tags(args): - """ - Gets the most used tags for tracks owned by the query user - - Args: - args: dict The parsed args from the request - args.limit: number optional The max number of tags to return - args.user_id: number The user id used to query for tracks - - Returns: - Array of strings ordered by most used tag in track - """ - db = get_db_read_replica() - with db.scoped_session() as session: - return _get_top_user_track_tags(session, args) - - -def _get_top_user_track_tags(session, args): - most_used_tags = ( - session.query(t_tag_track_user.c.tag) - .join(Track, Track.track_id == t_tag_track_user.c.track_id) - .filter(t_tag_track_user.c.owner_id == args["user_id"]) - .filter(Track.is_delete == False) - .group_by(t_tag_track_user.c.tag) - .order_by( - desc(func.count(t_tag_track_user.c.tag)), desc(t_tag_track_user.c.tag) - ) - .all() - ) - - tags = [tag for tag, in most_used_tags] - if "limit" in args: - return tags[: args["limit"]] - return tags diff --git a/packages/discovery-provider/src/queries/get_top_users.py b/packages/discovery-provider/src/queries/get_top_users.py deleted file mode 100644 index e25e4a93c17..00000000000 --- a/packages/discovery-provider/src/queries/get_top_users.py +++ /dev/null @@ -1,36 +0,0 @@ -from sqlalchemy import text - -from src.queries.query_helpers import get_pagination_vars, populate_user_metadata -from src.utils.db_session import get_db_read_replica - -sql = """ -select users.* -from users -join aggregate_user using (user_id) -where - is_current - and user_id in ( - select user_id from aggregate_user - where track_count > 0 - order by follower_count desc, user_id asc - limit :limit - offset :offset - ) -order by follower_count desc, user_id asc; -""" - - -def get_top_users(current_user_id): - """Gets the top users by follows of all of Audius""" - (limit, offset) = get_pagination_vars() - db = get_db_read_replica() - with db.scoped_session() as session: - return _get_top_users(session, current_user_id, limit, offset) - - -def _get_top_users(session, current_user_id, limit, offset): - top_users = session.execute(text(sql), {"limit": limit, "offset": offset}) - top_users = [dict(row) for row in top_users] - user_ids = list(map(lambda user: user["user_id"], top_users)) - top_users = populate_user_metadata(session, user_ids, top_users, current_user_id) - return top_users diff --git a/packages/discovery-provider/src/queries/get_total_plays.py b/packages/discovery-provider/src/queries/get_total_plays.py deleted file mode 100644 index 78361a73e97..00000000000 --- a/packages/discovery-provider/src/queries/get_total_plays.py +++ /dev/null @@ -1,26 +0,0 @@ -from sqlalchemy import func - -from src.models.social.aggregate_plays import AggregatePlay -from src.utils.db_session import get_db_read_replica - - -def get_total_plays(): - db = get_db_read_replica() - with db.scoped_session() as session: - return _get_total_plays(session) - - -def _get_total_plays(session): - """Gets the total number of plays across all tracks - - Args: - session: SQLAlchemy session - - Returns: - int: Total number of plays - """ - total_plays = session.query( - func.sum(func.coalesce(AggregatePlay.count, 0)) - ).scalar() - - return int(total_plays) if total_plays else 0 diff --git a/packages/discovery-provider/src/queries/get_trailing_metrics.py b/packages/discovery-provider/src/queries/get_trailing_metrics.py deleted file mode 100644 index 322ec7a4287..00000000000 --- a/packages/discovery-provider/src/queries/get_trailing_metrics.py +++ /dev/null @@ -1,60 +0,0 @@ -import logging -from datetime import date, timedelta - -from sqlalchemy import func - -from src.models.metrics.aggregate_daily_total_users_metrics import ( - AggregateDailyTotalUsersMetrics, -) -from src.models.metrics.aggregate_daily_unique_users_metrics import ( - AggregateDailyUniqueUsersMetrics, -) -from src.utils import db_session - -logger = logging.getLogger(__name__) - - -def get_aggregate_route_metrics_trailing(time_range): - """ - Returns trailing count and unique count for all routes in the last trailing month or year - - Accepts: time_range = 'month' or 'year' or 'week' - Returns: - { unique_count, total_count } - """ - db = db_session.get_db_read_replica() - with db.scoped_session() as session: - return _get_aggregate_route_metrics_trailing(session, time_range or "month") - - -def _get_aggregate_route_metrics_trailing(session, time_range): - today = date.today() - if time_range == "year": - days_ago = today - timedelta(days=365) - elif time_range == "week": - days_ago = today - timedelta(days=7) - else: - days_ago = today - timedelta(days=30) - - counts = ( - session.query( - func.sum(AggregateDailyUniqueUsersMetrics.count), - func.sum(AggregateDailyUniqueUsersMetrics.summed_count), - ) - .filter(days_ago <= AggregateDailyUniqueUsersMetrics.timestamp) - .filter(AggregateDailyUniqueUsersMetrics.timestamp < today) - .first() - ) - - total_count = ( - session.query(func.sum(AggregateDailyTotalUsersMetrics.count)) - .filter(days_ago <= AggregateDailyTotalUsersMetrics.timestamp) - .filter(AggregateDailyTotalUsersMetrics.timestamp < today) - .first() - ) - - return { - "unique_count": counts[0], - "summed_unique_count": counts[1], - "total_count": total_count[0], - } diff --git a/packages/discovery-provider/src/queries/get_undisbursed_challenges.py b/packages/discovery-provider/src/queries/get_undisbursed_challenges.py deleted file mode 100644 index 559b970a457..00000000000 --- a/packages/discovery-provider/src/queries/get_undisbursed_challenges.py +++ /dev/null @@ -1,128 +0,0 @@ -from typing import List, Optional, Tuple, TypedDict - -from sqlalchemy import and_, asc - -from src.models.rewards.challenge import Challenge -from src.models.rewards.challenge_disbursement import ChallengeDisbursement -from src.models.rewards.user_challenge import UserChallenge -from src.models.users.user import User - - -class UndisbursedChallengeResponse(TypedDict): - challenge_id: str - user_id: int - specifier: str - amount: str - completed_blocknumber: Optional[int] - handle: str - wallet: str - created_at: str - completed_at: str - cooldown_days: Optional[int] - - -def to_challenge_response( - user_challenge: UserChallenge, - challenge: Challenge, - handle: str, - wallet: str, -) -> UndisbursedChallengeResponse: - return { - "challenge_id": challenge.id, - "user_id": user_challenge.user_id, - "specifier": user_challenge.specifier, - "amount": str(user_challenge.amount), - "completed_blocknumber": user_challenge.completed_blocknumber, - "handle": handle, - "wallet": wallet, - "created_at": str(user_challenge.created_at), - "completed_at": str(user_challenge.completed_at), - "cooldown_days": challenge.cooldown_days, - } - - -class UndisbursedChallengesArgs(TypedDict): - user_id: Optional[int] - limit: Optional[int] - offset: Optional[int] - completed_blocknumber: Optional[int] - challenge_id: Optional[str] - - -MAX_LIMIT = 500 -DEFAULT_LIMIT = 100 - - -# Gets undisbursed challenges -# returning a list of challenge responses -def get_undisbursed_challenges( - session, args: UndisbursedChallengesArgs -) -> List[UndisbursedChallengeResponse]: - undisbursed_challenges_query = ( - session.query(UserChallenge, Challenge, User.handle, User.wallet) - .outerjoin( - ChallengeDisbursement, - and_( - ChallengeDisbursement.specifier == UserChallenge.specifier, - ChallengeDisbursement.challenge_id == UserChallenge.challenge_id, - ChallengeDisbursement.user_id == UserChallenge.user_id, - ), - ) - .outerjoin( - Challenge, - Challenge.id == UserChallenge.challenge_id, - ) - .join(User, UserChallenge.user_id == User.user_id) - .filter( - # Check that there is no matching challenge disburstment - ChallengeDisbursement.challenge_id == None, - UserChallenge.is_complete == True, - Challenge.active == True, - User.is_current == True, - User.is_deactivated == False, - ) - .order_by( - asc(UserChallenge.completed_blocknumber), - asc(UserChallenge.user_id), - asc(UserChallenge.challenge_id), - ) - ) - - # Add Filters to the queries - - if args["user_id"] is not None: - undisbursed_challenges_query = undisbursed_challenges_query.filter( - UserChallenge.user_id == args["user_id"] - ) - - if args["completed_blocknumber"] is not None: - undisbursed_challenges_query = undisbursed_challenges_query.filter( - UserChallenge.completed_blocknumber > args["completed_blocknumber"] - ) - - if args["challenge_id"] is not None: - undisbursed_challenges_query = undisbursed_challenges_query.filter( - UserChallenge.challenge_id == args["challenge_id"] - ) - - limit = DEFAULT_LIMIT - - if args["limit"] is not None: - limit = min(MAX_LIMIT, args["limit"]) - undisbursed_challenges_query = undisbursed_challenges_query.limit(limit) - - if args["offset"] is not None: - undisbursed_challenges_query = undisbursed_challenges_query.offset( - args["offset"] - ) - - undisbursed_challenges: List[ - Tuple[UserChallenge, Challenge, str, str] - ] = undisbursed_challenges_query.all() - - undisbursed_challenges_response: List[UndisbursedChallengeResponse] = [ - to_challenge_response(user_challenge, challenge, handle, wallet) - for user_challenge, challenge, handle, wallet in undisbursed_challenges - ] - - return undisbursed_challenges_response diff --git a/packages/discovery-provider/src/queries/get_usdc_purchases.py b/packages/discovery-provider/src/queries/get_usdc_purchases.py deleted file mode 100644 index abe18e81d54..00000000000 --- a/packages/discovery-provider/src/queries/get_usdc_purchases.py +++ /dev/null @@ -1,150 +0,0 @@ -from typing import List, Optional, TypedDict - -from sqlalchemy import asc, desc, or_ - -from src.models.playlists.playlist import Playlist -from src.models.tracks.track import Track -from src.models.users.usdc_purchase import PurchaseType, USDCPurchase -from src.models.users.user import User -from src.queries.query_helpers import ( - PurchaseSortMethod, - SortDirection, - add_query_pagination, -) -from src.utils import helpers -from src.utils.db_session import get_db_read_replica - - -class GetUSDCPurchasesCountArgs(TypedDict): - seller_user_id: Optional[int] - buyer_user_id: Optional[int] - content_ids: Optional[List[int]] - content_type: Optional[PurchaseType] - - -class GetUSDCPurchasesArgs(TypedDict): - seller_user_id: Optional[int] - buyer_user_id: Optional[int] - content_ids: Optional[List[int]] - content_type: Optional[PurchaseType] - limit: int - offset: int - sort_method: Optional[PurchaseSortMethod] - sort_direction: Optional[SortDirection] - - -def _get_usdc_purchases(session, args: GetUSDCPurchasesCountArgs): - base_query = session.query(USDCPurchase) - buyer_user_id = args.get("buyer_user_id") - seller_user_id = args.get("seller_user_id") - content_ids = args.get("content_ids") - content_type = args.get("content_type") - sort_method = args.get("sort_method", None) - sort_direction = args.get("sort_direction", SortDirection.desc) - - # Basic filters - if content_ids: - base_query = base_query.filter(USDCPurchase.content_id.in_(content_ids)) - if buyer_user_id: - base_query = base_query.filter(USDCPurchase.buyer_user_id == buyer_user_id) - if seller_user_id: - base_query = base_query.filter(USDCPurchase.seller_user_id == seller_user_id) - if content_type: - base_query = base_query.filter(USDCPurchase.content_type == content_type) - - sort_fn = desc if sort_direction == SortDirection.desc else asc - if ( - sort_method == PurchaseSortMethod.artist_name - or sort_method == PurchaseSortMethod.content_title - ): - # If we're sorting by content related fields we need to join to get those fields - # First get the playlists - playlists_query = ( - base_query.filter( - or_( - USDCPurchase.content_type == PurchaseType.playlist, - USDCPurchase.content_type == PurchaseType.album, - ) - ) - .join(Playlist, Playlist.playlist_id == USDCPurchase.content_id) - .filter(Playlist.is_current == True) - .add_columns( - Playlist.playlist_name.label("content_title"), - Playlist.playlist_owner_id.label("owner_id"), - ) - ) - # Then the tracks - tracks_query = ( - base_query.filter(USDCPurchase.content_type == PurchaseType.track) - .join(Track, Track.track_id == USDCPurchase.content_id) - .filter(Track.is_current == True) - .add_columns( - Track.title.label("content_title"), - Track.owner_id.label("owner_id"), - ) - ) - if content_type == PurchaseType.track: - # Just tracks - subquery = tracks_query.subquery(name="track_subquery") - elif ( - content_type == PurchaseType.album or content_type == PurchaseType.playlist - ): - # Just playlists - subquery = playlists_query.subquery(name="playlist_subquery") - else: - # Union them together - subquery = playlists_query.union(tracks_query).subquery( - name="union_subquery" - ) - # Select the purchase entities from our subquery, sorting by the extra fields we added - if sort_method == PurchaseSortMethod.artist_name: - # Also get users as necessary - base_query = ( - session.query(USDCPurchase) - .select_entity_from(subquery) - .join(User, User.user_id == subquery.c.owner_id) - .filter(User.is_current == True) - .order_by(sort_fn(User.name), sort_fn(USDCPurchase.created_at)) - ) - elif sort_method == PurchaseSortMethod.content_title: - base_query = ( - session.query(USDCPurchase) - .select_entity_from(subquery) - .order_by( - sort_fn(subquery.c.content_title), - sort_fn(USDCPurchase.created_at), - ) - ) - elif sort_method == PurchaseSortMethod.buyer_name: - # Join users to get buyers and sort by username - base_query = ( - base_query.join(User, User.user_id == USDCPurchase.buyer_user_id) - .filter(User.is_current == True) - .order_by(sort_fn(User.name), sort_fn(USDCPurchase.created_at)) - ) - elif sort_method == PurchaseSortMethod.date: - base_query = base_query.order_by(sort_fn(USDCPurchase.created_at)) - - return base_query - - -def get_usdc_purchases_count(args: GetUSDCPurchasesCountArgs): - """Gets the count of all the USDC purchase entries fitting the given criteria.""" - db = get_db_read_replica() - with db.scoped_session() as session: - query = _get_usdc_purchases(session, args) - return query.count() - - -def get_usdc_purchases(args: GetUSDCPurchasesArgs): - """Gets all the USDC purchase entries fitting the given criteria. - - Does not include any other entity metadatas (no tracks, users, playlists).""" - db = get_db_read_replica() - with db.scoped_session() as session: - query = _get_usdc_purchases(session, args) - limit = args.get("limit") - offset = args.get("offset") - - results = add_query_pagination(query, limit, offset).all() - return helpers.query_result_to_list(results) diff --git a/packages/discovery-provider/src/queries/get_usdc_transactions_history.py b/packages/discovery-provider/src/queries/get_usdc_transactions_history.py deleted file mode 100644 index 76febf13d32..00000000000 --- a/packages/discovery-provider/src/queries/get_usdc_transactions_history.py +++ /dev/null @@ -1,155 +0,0 @@ -import logging -from typing import List, Optional, TypedDict - -from sqlalchemy import asc, desc -from sqlalchemy.orm import Query -from sqlalchemy.orm.session import Session - -from src.models.users.usdc_transactions_history import ( - USDCTransactionMethod, - USDCTransactionsHistory, - USDCTransactionType, -) -from src.models.users.user import User -from src.models.users.user_bank import USDCUserBankAccount -from src.queries.query_helpers import ( - SortDirection, - TransactionSortMethod, - paginate_query, -) -from src.utils import helpers -from src.utils.db_session import get_db_read_replica - -logger = logging.getLogger(__name__) - - -class GetUSDCTransactionsCountArgs(TypedDict): - user_id: int - transaction_type: Optional[List[USDCTransactionType]] - transaction_method: Optional[USDCTransactionMethod] - include_system_transactions: Optional[bool] - - -class GetUSDCTransactionsArgs(TypedDict): - user_id: int - sort_direction: SortDirection - sort_method: TransactionSortMethod - transaction_type: Optional[List[USDCTransactionType]] - transaction_method: Optional[USDCTransactionMethod] - include_system_transactions: Optional[bool] - limit: int - offset: int - - -USDC_SYSTEM_TRANSACTION_TYPES = [ - USDCTransactionType.prepare_withdrawal, - USDCTransactionType.recover_withdrawal, -] - - -# SELECT count(*) -# FROM users -# JOIN usdc_user_bank_accounts ON usdc_user_bank_accounts.ethereum_address = users.wallet -# JOIN usdc_transactions_history ON usdc_transactions_history.user_bank = usdc_user_bank_accounts.bank_account -# WHERE users.user_id = AND users.is_current = TRUE - - -def _get_usdc_transactions_history_count( - session: Session, args: GetUSDCTransactionsCountArgs -): - user_id = args.get("user_id") - transaction_type = args.get("transaction_type", None) - transaction_method = args.get("transaction_method", None) - include_system_transactions = args.get("include_system_transactions", False) - query: Query = ( - session.query(USDCTransactionsHistory) - .select_from(User) - .filter(User.user_id == user_id, User.is_current == True) - .join( - USDCUserBankAccount, - USDCUserBankAccount.ethereum_address == User.wallet, - ) - .join( - USDCTransactionsHistory, - USDCTransactionsHistory.user_bank == USDCUserBankAccount.bank_account, - ) - ) - if transaction_type is not None: - query = query.filter( - USDCTransactionsHistory.transaction_type.in_(transaction_type) - ) - if not include_system_transactions: - query = query.filter( - USDCTransactionsHistory.transaction_type.notin_( - USDC_SYSTEM_TRANSACTION_TYPES - ) - ) - if transaction_method is not None: - query = query.filter(USDCTransactionsHistory.method == transaction_method) - count: int = query.count() - return count - - -def get_usdc_transactions_history_count(args: GetUSDCTransactionsCountArgs): - db = get_db_read_replica() - with db.scoped_session() as session: - count = _get_usdc_transactions_history_count(session, args) - return count - - -# SELECT usdc_transactions_history.* -# FROM users -# JOIN usdc_user_bank_accounts ON usdc_user_bank_accounts.ethereum_address = users.wallet -# JOIN usdc_transactions_history ON usdc_transactions_history.user_bank = usdc_user_bank_accounts.bank_account -# WHERE users.user_id = AND users.is_current = TRUE -# ORDER BY usdc_transactions_history.transaction_created_at ASC - - -def _get_usdc_transactions_history(session: Session, args: GetUSDCTransactionsArgs): - transaction_type = args.get("transaction_type", None) - transaction_method = args.get("transaction_method", None) - sort_method = args.get("sort_method") - include_system_transactions = args.get("include_system_transactions", False) - sort_direction = args.get("sort_direction") - - query: Query = ( - session.query(USDCTransactionsHistory) - .select_from(User) - .filter(User.user_id == args["user_id"], User.is_current == True) - .join(USDCUserBankAccount, USDCUserBankAccount.ethereum_address == User.wallet) - .join( - USDCTransactionsHistory, - USDCTransactionsHistory.user_bank == USDCUserBankAccount.bank_account, - ) - ) - if transaction_type is not None: - query = query.filter( - USDCTransactionsHistory.transaction_type.in_(transaction_type) - ) - if not include_system_transactions: - query = query.filter( - USDCTransactionsHistory.transaction_type.notin_( - USDC_SYSTEM_TRANSACTION_TYPES - ) - ) - if transaction_method is not None: - query = query.filter(USDCTransactionsHistory.method == transaction_method) - - sort_fn = desc if sort_direction == SortDirection.desc else asc - if sort_method == TransactionSortMethod.date: - query = query.order_by(sort_fn(USDCTransactionsHistory.transaction_created_at)) - elif sort_method == TransactionSortMethod.transaction_type: - query = query.order_by( - sort_fn(USDCTransactionsHistory.transaction_type), - desc(USDCTransactionsHistory.transaction_created_at), - ) - query = paginate_query(query) - results: List[USDCTransactionsHistory] = query.all() - return results - - -def get_usdc_transactions_history(args: GetUSDCTransactionsArgs): - db = get_db_read_replica() - with db.scoped_session() as session: - history = _get_usdc_transactions_history(session, args) - return helpers.query_result_to_list(history) diff --git a/packages/discovery-provider/src/queries/get_user_listen_counts_monthly.py b/packages/discovery-provider/src/queries/get_user_listen_counts_monthly.py deleted file mode 100644 index 90919e0db3d..00000000000 --- a/packages/discovery-provider/src/queries/get_user_listen_counts_monthly.py +++ /dev/null @@ -1,54 +0,0 @@ -from typing import TypedDict - -from sqlalchemy import text -from sqlalchemy.orm.session import Session - -from src.utils.db_session import get_db_read_replica - - -class GetUserListenCountsMonthlyArgs(TypedDict): - # The current user logged in (from route param) - user_id: int - - # The start time from which to search (from query arg) - start_time: str - - # The end time until which to search (from query arg) - end_time: str - - -def get_user_listen_counts_monthly(args: GetUserListenCountsMonthlyArgs): - """ - Returns a user's listen counts for all tracks they own, grouped by month. - - Args: - args: GetUserListenCountsMonthlyArgs The parsed args from the request - - Returns: - Array of AggregateMonthlyPlay objects that are timestamped within the given - time frame and whose tracks all belong to the given user id. - """ - - db = get_db_read_replica() - with db.scoped_session() as session: - return list(_get_user_listen_counts_monthly(session, args)) - - -def _get_user_listen_counts_monthly( - session: Session, args: GetUserListenCountsMonthlyArgs -): - sql = text( - """ - select - play_item_id, - timestamp, - sum(count) as count - from aggregate_monthly_plays - where play_item_id in (select track_id from tracks where owner_id = :user_id) - and timestamp >= :start_time - and timestamp < :end_time - group by play_item_id, timestamp - """ - ) - - return session.execute(sql, args) diff --git a/packages/discovery-provider/src/queries/get_user_listening_history.py b/packages/discovery-provider/src/queries/get_user_listening_history.py deleted file mode 100644 index b12066a3394..00000000000 --- a/packages/discovery-provider/src/queries/get_user_listening_history.py +++ /dev/null @@ -1,180 +0,0 @@ -from typing import Optional, TypedDict - -from sqlalchemy import asc, desc, func, or_ -from sqlalchemy.orm.session import Session -from sqlalchemy.sql.functions import coalesce - -from src.models.social.aggregate_plays import AggregatePlay -from src.models.tracks.aggregate_track import AggregateTrack -from src.models.tracks.track_with_aggregates import TrackWithAggregates -from src.models.users.user import User -from src.models.users.user_listening_history import UserListeningHistory -from src.queries import response_name_constants -from src.queries.query_helpers import ( - SortDirection, - SortMethod, - add_query_pagination, - add_users_to_tracks, - populate_track_metadata, -) -from src.utils import helpers -from src.utils.db_session import get_db_read_replica - - -class GetUserListeningHistoryArgs(TypedDict): - # The current user logged in (from route param) - user_id: int - - # The maximum number of listens to return - limit: int - - # The offset for the listen history - offset: int - - # Optional filter for the returned results - query: Optional[str] - - # Optional sort method for the returned results - sort_method: Optional[SortMethod] - sort_direction: Optional[SortDirection] - - -def get_user_listening_history(args: GetUserListeningHistoryArgs): - """ - Returns a user's listening history. - - Args: - args: GetUserListeningHistoryArgs The parsed args from the request - - Returns: - Array of tracks the user listened to starting from most recently listened - """ - - db = get_db_read_replica() - with db.scoped_session() as session: - return _get_user_listening_history(session, args) - - -def _get_user_listening_history(session: Session, args: GetUserListeningHistoryArgs): - user_id = args["user_id"] - limit = args["limit"] - offset = args["offset"] - query = args["query"] - sort_method = args["sort_method"] - sort_direction = args["sort_direction"] - sort_fn = desc if sort_direction == SortDirection.desc else asc - - listening_history_results = ( - session.query(UserListeningHistory.listening_history).filter( - UserListeningHistory.user_id == user_id - ) - ).scalar() - - if not listening_history_results: - return [] - - # order listening history entries by their user's play counts so our track ids will be - # correct order when querying for track ids - if sort_method == SortMethod.most_listens_by_user: - listening_history_results = sort_listening_history_results_by_play_count_desc( - listening_history_results - ) - - # Map out all track ids and listen dates - track_ids = [] - listen_dates = {} - for listen in listening_history_results: - track_ids.append(listen["track_id"]) - listen_dates[listen["track_id"]] = listen["timestamp"] - - base_query = ( - session.query(TrackWithAggregates) - .filter(TrackWithAggregates.track_id.in_(track_ids)) - .filter(TrackWithAggregates.is_current == True) - .filter(TrackWithAggregates.is_delete == False) - .join(TrackWithAggregates.user) - .filter(User.is_deactivated == False) - ) - - if query is not None: - base_query = base_query.filter( - or_( - TrackWithAggregates.title.ilike(f"%{query.lower()}%"), - User.name.ilike(f"%{query.lower()}%"), - ) - ) - - base_query = sort_by_sort_method(sort_method, sort_fn, track_ids, base_query) - - # Add pagination - base_query = add_query_pagination(base_query, limit, offset) - query_results = base_query.all() - track_ids = track_ids[offset : offset + limit] - - tracks = helpers.query_result_to_list(query_results) - - # bundle peripheral info into track results - tracks = populate_track_metadata( - session, track_ids, tracks, current_user_id=user_id, track_has_aggregates=True - ) - tracks = add_users_to_tracks(session, tracks, current_user_id=user_id) - - for track in tracks: - track[response_name_constants.activity_timestamp] = listen_dates[ - track[response_name_constants.track_id] - ] - - return tracks - - -def sort_listening_history_results_by_play_count_desc(listening_history_results): - listening_histories_by_plays = [ - listening_history for listening_history in listening_history_results - ] - listening_histories_by_plays.sort( - key=lambda listen: listen.get("play_count", 1), reverse=True - ) - listening_history_results = listening_histories_by_plays - return listening_history_results - - -def sort_by_sort_method(sort_method, sort_fn, track_ids, base_query): - if sort_method == SortMethod.title: - return base_query.order_by(sort_fn(TrackWithAggregates.title)) - elif sort_method == SortMethod.artist_name: - return base_query.join(TrackWithAggregates.user, aliased=True).order_by( - sort_fn(User.name) - ) - elif sort_method == SortMethod.release_date: - return base_query.order_by( - sort_fn( - coalesce( - TrackWithAggregates.release_date, - TrackWithAggregates.created_at, - ) - ) - ) - elif sort_method == SortMethod.last_listen_date: - return base_query.order_by( - sort_fn(func.array_position(track_ids, TrackWithAggregates.track_id)) - ) - elif sort_method == SortMethod.plays: - return base_query.join(TrackWithAggregates.aggregate_play).order_by( - sort_fn(AggregatePlay.count) - ) - elif sort_method == SortMethod.reposts: - return base_query.join(TrackWithAggregates.aggregate_track).order_by( - sort_fn(AggregateTrack.repost_count) - ) - elif sort_method == SortMethod.saves: - return base_query.join(TrackWithAggregates.aggregate_track).order_by( - sort_fn(AggregateTrack.save_count) - ) - elif sort_method == SortMethod.most_listens_by_user: - return base_query.order_by( - (func.array_position(track_ids, TrackWithAggregates.track_id)) - ) - else: - return base_query.order_by( - sort_fn(func.array_position(track_ids, TrackWithAggregates.track_id)) - ) diff --git a/packages/discovery-provider/src/queries/get_user_playlist_update.py b/packages/discovery-provider/src/queries/get_user_playlist_update.py deleted file mode 100644 index e3e2b6096c2..00000000000 --- a/packages/discovery-provider/src/queries/get_user_playlist_update.py +++ /dev/null @@ -1,73 +0,0 @@ -import logging -from datetime import datetime -from typing import List, TypedDict - -from sqlalchemy.sql import text - -from src.utils import db_session - -logger = logging.getLogger(__name__) - - -sql = text( - """ -SELECT - p.playlist_id, - p.updated_at, - ps.seen_at -from - playlists p -INNER JOIN - saves s ON - s.save_item_id = p.playlist_id AND - s.is_current AND - NOT s.is_delete AND - s.save_type = 'playlist' AND - s.user_id = :user_id -LEFT JOIN - playlist_seen ps ON - ps.is_current AND - ps.playlist_id = p.playlist_id AND - ps.user_id = :user_id -where - p.is_current = true AND - p.is_delete = false AND - s.created_at < p.updated_at AND - (ps.seen_at is NULL OR p.updated_at > ps.seen_at) -order by p.playlist_id -""" -) - - -class PlaylistUpdate(TypedDict): - playlist_id: int - updated_at: datetime - last_seen_at: datetime - - -def get_user_playlist_update(user_id: int) -> List[PlaylistUpdate]: - """ - Returns list of playlist_ids for a user_id which have been updated - and the user has not seen - - Args: - user_id: int - - Returns: - List of playlist id - """ - db = db_session.get_db_read_replica() - with db.scoped_session() as session: - rows = session.execute( - sql, - {"user_id": user_id}, - ) - playlists_with_updates: List[PlaylistUpdate] = [ - { - "playlist_id": row[0], - "updated_at": row[1], - "last_seen_at": row[2], - } - for row in rows - ] - return playlists_with_updates diff --git a/packages/discovery-provider/src/queries/get_user_signals.py b/packages/discovery-provider/src/queries/get_user_signals.py deleted file mode 100644 index 5cf50ed6d05..00000000000 --- a/packages/discovery-provider/src/queries/get_user_signals.py +++ /dev/null @@ -1,54 +0,0 @@ -from src.models.users.aggregate_user import AggregateUser -from src.models.users.user import User -from src.utils import db_session - - -def get_user_signals(handle): - db = db_session.get_db_read_replica() - with db.scoped_session() as session: - return _get_user_signals(session, handle) - - -def _get_user_signals(session, handle): - user_result = ( - session.query( - User.user_id, - User.profile_picture, - User.profile_picture_sizes, - User.cover_photo, - User.cover_photo_sizes, - User.wallet, - ) - .filter(User.handle == handle) - .filter(User.is_current == True) - .first() - ) - - if not user_result: - raise Exception(f"Could not find user id for handle '{handle}'") - - ( - user_id, - profile_picture, - profile_picture_sizes, - cover_photo, - cover_photo_sizes, - wallet, - ) = user_result - - user_follow_result = ( - session.query(AggregateUser.follower_count, AggregateUser.following_count) - .filter(AggregateUser.user_id == user_id) - .first() - ) - num_followers = user_follow_result[0] if user_follow_result else 0 - num_following = user_follow_result[1] if user_follow_result else 0 - - return { - "num_followers": num_followers, - "num_following": num_following, - "has_profile_picture": profile_picture is not None - or profile_picture_sizes is not None, - "has_cover_photo": cover_photo is not None or cover_photo_sizes is not None, - "wallet": wallet, - } diff --git a/packages/discovery-provider/src/queries/get_users_account.py b/packages/discovery-provider/src/queries/get_users_account.py deleted file mode 100644 index 4d7b9aadf9a..00000000000 --- a/packages/discovery-provider/src/queries/get_users_account.py +++ /dev/null @@ -1,159 +0,0 @@ -from typing import TypedDict - -from sqlalchemy import and_, asc, desc, or_ - -from src import exceptions -from src.models.playlists.playlist import Playlist -from src.models.social.save import Save, SaveType -from src.models.users.user import User -from src.queries.get_managed_users import is_active_manager -from src.queries.get_unpopulated_users import get_unpopulated_users -from src.queries.query_helpers import populate_user_metadata -from src.utils import helpers -from src.utils.db_session import get_db_read_replica - - -class GetAccountArgs(TypedDict): - wallet: str - authed_user_id: int - - -class GetAccountResponse(TypedDict): - user: dict - playlists: list[dict] - - -def _get_user_from_wallet(session, wallet: str): - # Create initial query - base_query = session.query(User) - # Don't return the user if they have no wallet or handle (user creation did not finish properly on chain) - base_query = base_query.filter(User.is_current == True, User.wallet != None) - - wallet = wallet.lower() - if len(wallet) == 42: - base_query = base_query.filter_by(wallet=wallet) - base_query = base_query.order_by( - desc(User.handle.isnot(None)), asc(User.created_at) - ) - else: - raise exceptions.ArgumentError("Invalid wallet length") - - # If user cannot be found, exit early and return empty response - user = base_query.first() - if not user: - return None - - return helpers.model_to_dictionary(user) - - -def _get_account_playlists(session, user_id: int): - # Get saved playlists / albums ids - saved_query = session.query(Save.save_item_id).filter( - Save.user_id == user_id, - Save.is_current == True, - Save.is_delete == False, - or_(Save.save_type == SaveType.playlist, Save.save_type == SaveType.album), - ) - - saved_query_results = saved_query.all() - save_collection_ids = [item[0] for item in saved_query_results] - - # Get Playlist/Albums saved or owned by the user - playlist_query = ( - session.query(Playlist) - .filter( - or_( - and_( - Playlist.is_current == True, - Playlist.is_delete == False, - Playlist.playlist_owner_id == user_id, - ), - and_( - Playlist.is_current == True, - Playlist.is_delete == False, - Playlist.playlist_id.in_(save_collection_ids), - ), - ) - ) - .order_by(desc(Playlist.created_at)) - ) - playlists = playlist_query.all() - playlists = helpers.query_result_to_list(playlists) - - playlist_owner_ids = list({playlist["playlist_owner_id"] for playlist in playlists}) - - # Get Users for the Playlist/Albums - users = get_unpopulated_users(session, playlist_owner_ids) - - user_map = {} - - stripped_playlists = [] - # Map the users to the playlists/albums - for playlist_owner in users: - user_map[playlist_owner["user_id"]] = playlist_owner - for playlist in playlists: - playlist_owner = user_map[playlist["playlist_owner_id"]] - stripped_playlist = { - "id": playlist["playlist_id"], - "name": playlist["playlist_name"], - "is_album": playlist["is_album"], - "permalink": playlist["permalink"], - "user": { - "id": playlist_owner["user_id"], - "handle": playlist_owner["handle"], - }, - } - if playlist_owner["is_deactivated"]: - stripped_playlist["user"]["is_deactivated"] = True - stripped_playlists.append(stripped_playlist) - return stripped_playlists - - -def get_account(args: GetAccountArgs) -> GetAccountResponse | None: - wallet = args.get("wallet") - authed_user_id = args.get("authed_user_id") - if not wallet: - raise exceptions.ArgumentError("Missing wallet param") - if not authed_user_id: - raise exceptions.ArgumentError("Missing authed_user_id param") - - db = get_db_read_replica() - with db.scoped_session() as session: - user = _get_user_from_wallet(session, args["wallet"]) - if not user: - return None - - user_id = user["user_id"] - - if user_id != authed_user_id and not is_active_manager(user_id, authed_user_id): - raise exceptions.PermissionError( - "You do not have permission to view this account" - ) - - # bundle peripheral info into user results - users = populate_user_metadata(session, [user_id], [user], user_id, True) - user = users[0] - playlists = _get_account_playlists(session, user_id) - return {"user": user, "playlists": playlists} - - -# DEPRECATED: Legacy query used by v0 endpoint -def get_users_account(args): - if "wallet" not in args: - raise exceptions.ArgumentError("Missing wallet param") - - db = get_db_read_replica() - with db.scoped_session() as session: - user = _get_user_from_wallet(session, args["wallet"]) - if not user: - return None - - user_id = user["user_id"] - - # bundle peripheral info into user results - users = populate_user_metadata(session, [user_id], [user], user_id, True) - user = users[0] - - user["playlists"] = _get_account_playlists(session, user_id) - - return user