import copy from dataclasses import dataclass, asdict, field from datetime import datetime from typing import List, Dict, Union, Tuple, Any from faker import Faker from charts_messages.constants import Changelog, TRACKS_TO_PROCESS faker = Faker() TEST_TTL = 10 @dataclass class IncomingEventNestedMeta: """Nested meta information for incoming event""" domain: str id: str dsp: str country_code: str type: str breakdown: str @dataclass class IncomingEventNestedPublisher: """Nested publisher information for incoming event""" id: str = "user_data_api_ecs_web_test" owner: str = "test" type: str = "test" instance: str = "test" env: str = "testing" @dataclass class IncomingEvent: """Helper for incoming event creation id: should be an integer represented as a string created_at: isoformat datetime """ meta: IncomingEventNestedMeta publisher: IncomingEventNestedPublisher id: str = "1234567890" app: str = "apollo_test" code: str = "charts_updated" ttl: int = TEST_TTL created_at: str = datetime.utcnow().isoformat() def create_incoming_event( event_id: str = "123456789", event_ttl: int = TEST_TTL, event_created_at: str = datetime.utcnow().isoformat(), meta_dsp: str = "spotify", meta_country_code: str = "us", meta_domain: str = "charts", meta_type: str = "viral", meta_breakdown: str = "daily" ): event = IncomingEvent( meta=IncomingEventNestedMeta( domain=meta_domain, id=f"{meta_dsp}_{meta_country_code}_{meta_type}_{meta_breakdown}", dsp=meta_dsp, country_code=meta_country_code, type=meta_type, breakdown=meta_breakdown ), id=event_id, publisher=IncomingEventNestedPublisher(), ttl=event_ttl, created_at=event_created_at ) return asdict(event) def get_string_fake_field(field_name: str = "fake_field", i: int = 0) -> str: """Get fake field Args: field_name: name of a field to return i: field index to add Returns: str """ return f"fake_{field_name}_{i}" def get_artists(i: int) -> List[Dict[str, str]]: """Get fake tracks artists Args: i: field index to add Returns: List of artists dict """ return [{ "id": faker.random_int(100, 999), "name": faker.name() }] @dataclass class EventTrack: """Nested track information for any event""" id: str isrc: str name: str image_url: str artists: list = field(default_factory=list) @dataclass class Event: """Dataclass to create events such as additions/removals/moves""" track: EventTrack current_position: Union[int, None] = None previous_position: Union[int, None] = None def get_fake_track_positions( changelog_type: Changelog, min_random_position: int = 0, max_random_position: int = 20 ) -> Tuple[Union[int, None], Union[int, None]]: """Get fake tracks positions based on its changelog type such as entry/exit/move Args: changelog_type: ENTRY/EXIT/MOVE of a <> min_random_position: min of random integer for a position max_random_position: max ^^ --""--""-- ^^ Returns: (current_position, previous_position) """ changelog_specific_positions = { Changelog.ENTRY: ( faker.random_int(min_random_position, max_random_position), None ), Changelog.EXIT: ( None, faker.random_int(min_random_position, max_random_position) ), Changelog.MOVE: ( faker.random_int(min_random_position, max_random_position), faker.random_int(min_random_position, max_random_position) ) } return changelog_specific_positions[changelog_type] def get_track(i: int = 0, changelog_type: Changelog = Changelog.ENTRY) -> Dict[str, Any]: """Get single fake track""" current_position, previous_position = get_fake_track_positions(changelog_type) track = Event( track=EventTrack( id=get_string_fake_field("id", i), isrc=get_string_fake_field("isrc", i), name=get_string_fake_field("name", i), image_url=get_string_fake_field("image_url", i), artists=get_artists(i) ), current_position=current_position, previous_position=previous_position ) return asdict(track) def get_tracklist(start_index: int = 0, finnish_index: int = 5, changelog_type: Changelog = Changelog.ENTRY): tracklist = [] for i in range(start_index, finnish_index): tracklist.append(get_track(i=i, changelog_type=changelog_type)) return tracklist def get_additions_removals_moves( additions_removals: int, moves: int ): return { "additions": get_tracklist(0, additions_removals), "removals": get_tracklist(additions_removals, 2*additions_removals), "moves": get_tracklist(2*additions_removals, 2*additions_removals+moves) } def check_and_return_tracklist( tracklist: Dict[str, Any], changelog_to_compare: Changelog, changelog_compare_with: Tuple[Changelog] ) -> List: """Check condition and return tracklist or an empty list Args: tracklist: created additions/removals/moves dict of tracklists changelog_to_compare: Changelog key changelog_compare_with: Tuple of Changelogs to add to event Returns: List """ return tracklist[TRACKS_TO_PROCESS[changelog_to_compare]]\ if changelog_to_compare in changelog_compare_with else [] def get_full_event_with_data( event: Event = None, event_id: str = "123456789", event_ttl: int = TEST_TTL, event_created_at: str = datetime.utcnow().isoformat(), meta_dsp: str = "spotify", meta_country_code: str = "worldwide", meta_domain: str = "charts", meta_type: str = "viral", meta_breakdown: str = "daily", data_changelog_type: Tuple[Changelog] = (Changelog.ENTRY, Changelog.EXIT, Changelog.MOVE), additions_removals: int = 5, moves: int = 10 ): event_without_data = create_incoming_event( event_id=event_id, event_ttl=event_ttl, event_created_at=event_created_at, meta_dsp=meta_dsp, meta_country_code=meta_country_code, meta_domain=meta_domain, meta_type=meta_type, meta_breakdown=meta_breakdown ) if not event else event entry_exit_move = get_additions_removals_moves(additions_removals=additions_removals, moves=moves) data = { "data": { "changelog": { "additions": check_and_return_tracklist(entry_exit_move, Changelog.ENTRY, data_changelog_type), "removals": check_and_return_tracklist(entry_exit_move, Changelog.EXIT, data_changelog_type), "moves": check_and_return_tracklist(entry_exit_move, Changelog.MOVE, data_changelog_type) } } } return {**event_without_data, **data} @dataclass class AccountSettingsDataNestedVendors: """ ex: { "apple": true, "spotify": true } """ apple: bool = True spotify: bool = True @dataclass class AccountSettingsNestedData: """ ex: { "markets": ["global"], "vendors": { "apple": true, "spotify": true }, "notifications": true } """ vendors: AccountSettingsDataNestedVendors markets: list = field(default_factory=list) notifications: bool = True @dataclass class AccountNestedSettings: """ ex: { "id" "type": str = "mobile", "version": str = "1", "data": dict } """ data: AccountSettingsNestedData id: int = faker.random_int(0, 100) type: str = "mobile" version: str = "1" @dataclass class AccountDeviceStructure: """ ex: { "id": int "is_active": bool, "os": str = "ios" | "android", "expo_token": str } """ expo_token: str id: int is_active: bool = True os: str = "ios" def get_device( expo_token: str, device_id: int, is_active: bool = True, os: str = "ios" ) -> Dict[str, Union[str, int, bool]]: """ Get single device structure dictionary Args: expo_token: expo token string device_id: int device id is_active: True or False os: "ios" | "android" Returns: Dict[] """ return asdict( AccountDeviceStructure( expo_token=expo_token, id=device_id, is_active=is_active, os=os ) ) @dataclass class AccountStructure: account_id: int user_id: str settings: AccountNestedSettings devices: list = field(default_factory=list) def get_single_account( user_id: str = None, settings_id: int = None, settings_type: str = "mobile", settings_version: str = "1", account_id: int = faker.random_int(1000), settings_markets: List[str] = ["global"], number_of_devices: int = faker.random_int(1, 3) ): devices = [ get_device( expo_token=get_string_fake_field("expo_token", faker.random_int(1000)), device_id=faker.random_int(1000) ) for _ in range(number_of_devices) ] account = AccountStructure( account_id=account_id, user_id=user_id or get_string_fake_field("user_id", faker.random_int(100)), settings=AccountNestedSettings( id=settings_id or faker.random_int(100), type=settings_type, version=settings_version, data=AccountSettingsNestedData( vendors=AccountSettingsDataNestedVendors(), markets=settings_markets ) ), devices=devices ) return asdict(account) @dataclass class AccountsReponseNestedData: """ entity_id: isrc accounts: List of [AccountStructure,] with devices: ex: { "entity_type": str = "track" "entity_id": str = isrc "accounts": List[dict] = [ # list of interested accounts { "account_id": int, "user_id": str, "devices": Optional[List[dict]] = [ # account devices { "id": int "is_active": bool, "os": str = "ios" | "android", "expo_token": str }, ... ] "settings": Optional[dict] = { # account settings "id" "type": str = "mobile", "version": str = "1", "data": dict } }, ] }, """ entity_type: str entity_id: str accounts: list = field(default_factory=list) def get_accounts_nested_data_by_isrc( isrc_list: List[str], entity_type: str = "track", number_of_acc_for_isrc: int = faker.random_int(0, 5) ) -> List[Dict[str, Any]]: result = [] for isrc in isrc_list: account = AccountsReponseNestedData( entity_type=entity_type, entity_id=isrc, accounts=[get_single_account(account_id=faker.random_int(1, 10000)) for _ in range(number_of_acc_for_isrc)] ) result.append(asdict(account)) return result DEFAULT_V2_NOTIFICATION_SETTINGS = { "markets": ["global"], "categories": { "charts": { "apple": False, "spotify": False }, "starred_tracks": { "apple": True, "spotify": True }, "starred_playlists": { "apple": True, "spotify": True } }, "notifications": True } def get_fake_notification_settings_v2( categories: Dict[str, bool] = None, markets: List[str] = None, notifications: bool = None, category_key: str = "starred_tracks" ): """Get notification settings v2""" result = DEFAULT_V2_NOTIFICATION_SETTINGS if categories: result["categories"][category_key].update(categories) if markets: result["markets"] = markets if notifications is not None: result["notifications"] = notifications result = copy.deepcopy(result) return result def get_fake_user_accounts_with_devices( number_of_accounts: int = 10, number_of_devices: int = 1, markets: List[str] = None, settings_version: str = "2" ): result = [] for acc in range(number_of_accounts): settings_data = get_fake_notification_settings_v2( categories={"apple": bool((acc % 2) == 0), "spotify": bool((acc % 2) == 0)}, markets=["global", "uk", "fr", "us", "de"] if not markets else markets, category_key="charts" ) settings = { "data": settings_data, "version": settings_version, "id": acc, "type": "mobile" } devices = [ get_device( expo_token=get_string_fake_field("expo_token", faker.random_int(1000)), device_id=faker.random_int(1000) ) for _ in range(number_of_devices) ] fake_acc = { "account_id": acc, "devices": devices, "user_id": "89719218-0ff2-49df-8ac4-14e90671918b", "settings": settings, } result.append(fake_acc) return result