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_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 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], ))
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 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 __profile_request(self, game: str, version: int, userid: UserID, exact: bool) -> Optional[ValidatedDict]: # First, get or create the extid/refid for this virtual user cardid = RemoteUser.userid_to_card(userid) refid = self.user.get_refid(game, version, userid) extid = self.user.get_extid(game, version, userid) profiles = Parallel.flatten(Parallel.call( [client.get_profiles for client in self.clients], game, version, APIConstants.ID_TYPE_CARD, [cardid], )) for profile in profiles: cards = [card.upper() for card in profile.get('cards', [])] if cardid in cards: # Sanitize the returned data profile = copy.deepcopy(profile) del profile['cards'] exact_match = profile.get('match', 'partial') == 'exact' if exact and (not exact_match): # This is a partial match, not for this game/version continue if 'match' in profile: del profile['match'] # Add in our defaults we always provide profile['game'] = game profile['version'] = version if exact_match else 0 profile['refid'] = refid profile['extid'] = extid return self.__format_profile(ValidatedDict(profile)) return None
def get_clear_rates( self, game: str, version: int, songid: Optional[int] = None, songchart: Optional[int] = None, ) -> Dict[int, Dict[int, Dict[str, int]]]: """ Given an optional songid, or optional songid and songchart, looks up clear rates in remote servers that are connected to us. If neither id or chart is given, looks up global clear rates. If songid is given, looks up clear rates for each chart for the song. If songid and chart is given, looks up clear rates for that song/chart. Returns a dictionary keyed by songid, whos values are a dictionary keyed by chart, whos values are a dictionary containing integer counts keyed by 'plays', 'clears', and 'combos'. An example is as follows: { musicid: { chart: { plays: total plays, clears: total clears, combos: total full combos, }, }, } """ if songid is None and songchart is None: statistics = Parallel.flatten( Parallel.call( [client.get_statistics for client in self.clients], game, version, APIConstants.ID_TYPE_SERVER, [], )) elif songid is not None: if songchart is None: ids = [songid] else: ids = [songid, songchart] statistics = Parallel.flatten( Parallel.call( [client.get_statistics for client in self.clients], game, version, APIConstants.ID_TYPE_SONG, ids, )) else: statistics = [] retval: Dict[int, Dict[int, Dict[str, int]]] = {} for stat in statistics: songid = stat.get('song') songchart = stat.get('chart') if songid is None or songchart is None: continue songid = int(songid) songchart = int(songchart) if songid not in retval: retval[songid] = {} if songchart not in retval[songid]: retval[songid][songchart] = { 'plays': 0, 'clears': 0, 'combos': 0, } def get_val(v: str) -> int: out = stat.get(v, -1) if out < 0: out = 0 return out retval[songid][songchart]['plays'] += get_val('plays') retval[songid][songchart]['clears'] += get_val('clears') retval[songid][songchart]['combos'] += get_val('combos') return retval
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 test_flatten(self) -> None: results = Parallel.flatten([[1, 2, 3], [4, 5, 6], [7, 8, 9], []]) self.assertEqual(results, [1, 2, 3, 4, 5, 6, 7, 8, 9])