def get_all_records( self, game: str, version: Optional[int] = None, userlist: Optional[List[UserID]] = None, locationlist: Optional[List[int]] = None, ) -> List[Tuple[UserID, Score]]: # First, pass off to local-only if this was called with parameters we don't support if (version is None or userlist is not None or locationlist is not None): return self.music.get_all_records(game, version, userlist, locationlist) # Now, fetch all records remotely and locally localcards, localscores, remotescores = Parallel.execute([ self.user.get_all_cards, lambda: self.music.get_all_records(game, version, userlist, locationlist), lambda: Parallel.flatten( Parallel.call( [client.get_records for client in self.clients], game, version, APIConstants.ID_TYPE_SERVER, [], )), ]) return self.__merge_global_scores(game, version, localcards, localscores, remotescores)
def test_empty(self) -> None: results = Parallel.execute([]) self.assertEqual(results, []) results = Parallel.map(lambda x: x, []) self.assertEqual(results, []) results = Parallel.call([]) self.assertEqual(results, []) results = Parallel.flatten([]) self.assertEqual(results, [])
def test_basic(self) -> None: results = Parallel.execute([ lambda: 1, lambda: 2, lambda: 3, lambda: 4, lambda: 5, ]) self.assertEqual(results, [1, 2, 3, 4, 5])
def test_function(self) -> None: def fun(x: int) -> int: return -x results = Parallel.execute([ lambda: fun(1), lambda: fun(2), lambda: fun(3), lambda: fun(4), lambda: fun(5), ]) self.assertEqual(results, [-1, -2, -3, -4, -5])
def get_score(self, game: str, version: int, userid: UserID, songid: int, songchart: int) -> Optional[Score]: # Helper function so we can iterate over all servers for a single card def get_scores_for_card(cardid: str) -> List[Score]: return Parallel.flatten( Parallel.call( [client.get_records for client in self.clients], game, version, APIConstants.ID_TYPE_INSTANCE, [songid, songchart, cardid], )) relevant_cards = self.__get_cardids(userid) if RemoteUser.is_remote(userid): # No need to look up local score for this user scores = Parallel.flatten( Parallel.map( get_scores_for_card, relevant_cards, )) localscore = None else: localscore, scores = Parallel.execute([ lambda: self.music.get_score(game, version, userid, songid, songchart), lambda: Parallel.flatten( Parallel.map( get_scores_for_card, relevant_cards, )), ]) topscore = localscore for score in scores: if int(score['song']) != songid: continue if int(score['chart']) != songchart: continue newscore = self.__format_score(game, version, songid, songchart, score) if topscore is None: # No merging needed topscore = newscore continue topscore = self.__merge_score(game, version, topscore, newscore) return topscore
def get_all_profiles(self, game: str, version: int) -> List[Tuple[UserID, ValidatedDict]]: # Fetch local and remote profiles, and then merge by adding remote profiles to local # profiles when we don't have a profile for that user ID yet. local_cards, local_profiles, remote_profiles = Parallel.execute([ self.user.get_all_cards, lambda: self.user.get_all_profiles(game, version), lambda: Parallel.flatten(Parallel.call( [client.get_profiles for client in self.clients], game, version, APIConstants.ID_TYPE_SERVER, [], )), ]) card_to_id = {cardid: userid for (cardid, userid) in local_cards} id_to_profile = {userid: profile for (userid, profile) in local_profiles} for profile in remote_profiles: cardids = sorted([card.upper() for card in profile.get('cards', [])]) if len(cardids) == 0: # We don't care about anonymous profiles continue local_cards = [cardid for cardid in cardids if cardid in card_to_id] if len(local_cards) > 0: # We have a local version of this profile! continue # Create a fake user with this profile del profile['cards'] exact_match = profile.get('match', 'partial') == 'exact' if not exact_match: continue userid = RemoteUser.card_to_userid(cardids[0]) refid = self.user.get_refid(game, version, userid) extid = self.user.get_extid(game, version, userid) # Add in our defaults we always provide profile['game'] = game profile['version'] = version profile['refid'] = refid profile['extid'] = extid id_to_profile[userid] = self.__format_profile(ValidatedDict(profile)) return [(userid, id_to_profile[userid]) for userid in id_to_profile]
def get_all_scores( self, game: str, version: Optional[int] = None, userid: Optional[UserID] = None, songid: Optional[int] = None, songchart: Optional[int] = None, since: Optional[int] = None, until: Optional[int] = None, ) -> List[Tuple[UserID, Score]]: # First, pass off to local-only if this was called with parameters we don't support if (version is None or userid is not None or songid is None): return self.music.get_all_scores(game, version, userid, songid, songchart, since, until) # Now, figure out the request key based on passed in parameters if songchart is None: songkey = [songid] else: songkey = [songid, songchart] # Now, fetch all the scores remotely and locally localcards, localscores, remotescores = Parallel.execute([ self.user.get_all_cards, lambda: self.music.get_all_scores(game, version, userid, songid, songchart, since, until), lambda: Parallel.flatten( Parallel.call( [client.get_records for client in self.clients], game, version, APIConstants.ID_TYPE_SONG, songkey, since, until, )), ]) return self.__merge_global_scores(game, version, localcards, localscores, remotescores)
def get_clear_rates( self, songid: Optional[int] = None, songchart: Optional[int] = None, ) -> Dict[int, Dict[int, Dict[str, int]]]: """ Returns a dictionary similar to the following: { musicid: { chart: { total: total plays, clears: total clears, fcs: total full combos, }, }, } """ all_attempts, remote_attempts = Parallel.execute([ lambda: self.data.local.music.get_all_attempts( game=self.game, version=self.music_version, songid=songid, songchart=songchart, ), lambda: self.data.remote.music.get_clear_rates( game=self.game, version=self.music_version, songid=songid, songchart=songchart, ), ]) attempts: Dict[int, Dict[int, Dict[str, int]]] = {} for (_, attempt) in all_attempts: if attempt.data.get_int( 'clear_status') == self.CLEAR_STATUS_NO_PLAY: # This attempt was outside of the clear infra, so don't bother with it. continue # Terrible temporary structure is terrible. if attempt.id not in attempts: attempts[attempt.id] = {} if attempt.chart not in attempts[attempt.id]: attempts[attempt.id][attempt.chart] = { 'total': 0, 'clears': 0, 'fcs': 0, } # We saw an attempt, keep the total attempts in sync. attempts[attempt.id][attempt.chart]['total'] = attempts[ attempt.id][attempt.chart]['total'] + 1 if attempt.data.get_int( 'clear_status', self.CLEAR_STATUS_FAILED) == self.CLEAR_STATUS_FAILED: # This attempt was a failure, so don't count it against clears of full combos continue # It was at least a clear attempts[attempt.id][attempt.chart]['clears'] = attempts[ attempt.id][attempt.chart]['clears'] + 1 if attempt.data.get_int( 'clear_status') == self.CLEAR_STATUS_FULL_COMBO: # This was a full combo clear, so it also counts here attempts[attempt.id][attempt.chart]['fcs'] = attempts[ attempt.id][attempt.chart]['fcs'] + 1 # Merge in remote attempts for songid in remote_attempts: if songid not in attempts: attempts[songid] = {} for songchart in remote_attempts[songid]: if songchart not in attempts[songid]: attempts[songid][songchart] = { 'total': 0, 'clears': 0, 'fcs': 0, } attempts[songid][songchart]['total'] += remote_attempts[ songid][songchart]['plays'] attempts[songid][songchart]['clears'] += remote_attempts[ songid][songchart]['clears'] attempts[songid][songchart]['fcs'] += remote_attempts[songid][ songchart]['combos'] # If requesting a specific song/chart, make sure its in the dict if songid is not None: if songid not in attempts: attempts[songid] = {} if songchart is not None: if songchart not in attempts[songid]: attempts[songid][songchart] = { 'total': 0, 'clears': 0, 'fcs': 0, } return attempts
def get_scores( self, game: str, version: int, userid: UserID, since: Optional[int] = None, until: Optional[int] = None, ) -> List[Score]: relevant_cards = self.__get_cardids(userid) if RemoteUser.is_remote(userid): # No need to look up local score for this user scores = Parallel.flatten( Parallel.call( [client.get_records for client in self.clients], game, version, APIConstants.ID_TYPE_CARD, relevant_cards, since, until, )) localscores: List[Score] = [] else: localscores, scores = Parallel.execute([ lambda: self.music.get_scores(game, version, userid, since, until), lambda: Parallel.flatten( Parallel.call( [client.get_records for client in self.clients], game, version, APIConstants.ID_TYPE_CARD, relevant_cards, since, until, )), ]) allscores: Dict[int, Dict[int, Score]] = {} def add_score(score: Score) -> None: if score.id not in allscores: allscores[score.id] = {} allscores[score.id][score.chart] = score def get_score(songid: int, songchart: int) -> Optional[Score]: return allscores.get(songid, {}).get(songchart) # First, seed with local scores for score in localscores: add_score(score) # Second, merge in remote scorse for remotescore in scores: songid = int(remotescore['song']) chart = int(remotescore['chart']) newscore = self.__format_score(game, version, songid, chart, remotescore) oldscore = get_score(songid, chart) if oldscore is None: add_score(newscore) else: add_score(self.__merge_score(game, version, oldscore, newscore)) # Finally, flatten and return finalscores: List[Score] = [] for songid in allscores: for chart in allscores[songid]: finalscores.append(allscores[songid][chart]) return finalscores
def get_any_profiles(self, game: str, version: int, userids: List[UserID]) -> List[Tuple[UserID, Optional[ValidatedDict]]]: if len(userids) == 0: return [] remote_ids = [ userid for userid in userids if RemoteUser.is_remote(userid) ] local_ids = [ userid for userid in userids if not RemoteUser.is_remote(userid) ] if len(remote_ids) == 0: # We only have local profiles here, just pass on to the underlying layer return self.user.get_any_profiles(game, version, local_ids) else: # We have to fetch some local profiles and some remote profiles, and then # merge them together card_to_userid = { RemoteUser.userid_to_card(userid): userid for userid in remote_ids } local_profiles, remote_profiles = Parallel.execute([ lambda: self.user.get_any_profiles(game, version, local_ids), lambda: Parallel.flatten(Parallel.call( [client.get_profiles for client in self.clients], game, version, APIConstants.ID_TYPE_CARD, [RemoteUser.userid_to_card(userid) for userid in remote_ids], )) ]) for profile in remote_profiles: cards = [card.upper() for card in profile.get('cards', [])] for card in cards: # Map it back to the requested user userid = card_to_userid.get(card) if userid is None: continue # Sanitize the returned data profile = copy.deepcopy(profile) del profile['cards'] exact_match = profile.get('match', 'partial') == 'exact' if 'match' in profile: del profile['match'] refid = self.user.get_refid(game, version, userid) extid = self.user.get_extid(game, version, userid) # Add in our defaults we always provide profile['game'] = game profile['version'] = version if exact_match else 0 profile['refid'] = refid profile['extid'] = extid local_profiles.append( (userid, self.__format_profile(ValidatedDict(profile))), ) # Mark that we saw this card/user del card_to_userid[card] # Finally, mark all missing remote profiles as None for card in card_to_userid: local_profiles.append((card_to_userid[card], None)) return local_profiles
def get_clear_rates(self) -> Dict[int, Dict[int, Dict[str, int]]]: """ Returns a dictionary similar to the following: { musicid: { chart: { total: total plays, clears: total clears, average: average score, }, }, } """ all_attempts, remote_attempts = Parallel.execute([ lambda: self.data.local.music.get_all_attempts( game=self.game, version=self.version, ), lambda: self.data.remote.music.get_clear_rates( game=self.game, version=self.version, ) ]) attempts: Dict[int, Dict[int, Dict[str, int]]] = {} for (_, attempt) in all_attempts: # Terrible temporary structure is terrible. if attempt.id not in attempts: attempts[attempt.id] = {} if attempt.chart not in attempts[attempt.id]: attempts[attempt.id][attempt.chart] = { 'total': 0, 'clears': 0, 'average': 0, } # We saw an attempt, keep the total attempts in sync. attempts[attempt.id][attempt.chart]['average'] = int( ((attempts[attempt.id][attempt.chart]['average'] * attempts[attempt.id][attempt.chart]['total']) + attempt.points) / (attempts[attempt.id][attempt.chart]['total'] + 1)) attempts[attempt.id][attempt.chart]['total'] += 1 if attempt.data.get_int('clear_type', self.CLEAR_TYPE_NO_PLAY) in [ self.CLEAR_TYPE_NO_PLAY, self.CLEAR_TYPE_FAILED ]: # This attempt was a failure, so don't count it against clears of full combos continue # It was at least a clear attempts[attempt.id][attempt.chart]['clears'] += 1 # Merge in remote attempts for songid in remote_attempts: if songid not in attempts: attempts[songid] = {} for songchart in remote_attempts[songid]: if songchart not in attempts[songid]: attempts[songid][songchart] = { 'total': 0, 'clears': 0, 'average': 0, } attempts[songid][songchart]['total'] += remote_attempts[ songid][songchart]['plays'] attempts[songid][songchart]['clears'] += remote_attempts[ songid][songchart]['clears'] return attempts