def parse(cls, zebra_motion_json): """ Parse JSON that contains Zebra MotionWorks data Format is as follows: [{ key: '2020casj_qm1', times: [<elapsed time (float seconds)>, 0.0, 0.5, 1.0, 1.5, ...], alliances: { red: [ { team_key: "frc254", xs: [<float feet or null>, null, 1.2, 1.3, 1.4, ...], ys: [<float feet or null>, null, 0.1, 0.1, 0.1, ...], }, { team_key: "frc971", xs: [1.1, 1.2, 1.3, 1.4, ...], ys: [0.1, 0.1, 0.1, 0.1, ...], }, ... ], blue: [...], } }] """ try: data = json.loads(zebra_motion_json) except Exception: raise ParserInputException("Invalid JSON. Please check input.") parsed_data = [] for zebra_data in data: # Check 'times' is an array of floats times = zebra_data.get('times', []) if not isinstance(times, list): raise ParserInputException("Zebra MotionWorks data must have a array of 'times'.") data_length = len(times) if data_length == 0: raise ParserInputException("Length of 'times' must be non-zero.") for time in times: if not isinstance(time, float): raise ParserInputException("'times' must be an array of floats.") # Check 'alliances' alliances = zebra_data.get('alliances') if not isinstance(alliances, dict): raise ParserInputException("Zebra MotionWorks data must have a dictionary of 'alliances'.") red = cls._parse_alliance(alliances.get('red', []), data_length) blue = cls._parse_alliance(alliances.get('blue', []), data_length) parsed_data.append({ 'key': zebra_data.get('key'), 'times': times, 'alliances': { 'red': red, 'blue': blue, }, }) return parsed_data
def parse(self, alliances_json): """ Parse JSON that contains team_keys Format is as follows: [[captain1, pick1-1, pick1-2(, ...)], ['frc254', 'frc971', 'frc604'], ... [captain8, pick8-1, pick8-2(, ...)]] """ try: alliances = json.loads(alliances_json) except: raise ParserInputException("Invalid JSON. Please check input.") alliance_selections = [] for alliance in alliances: selection = {'picks': [], 'declines': []} for team_key in alliance: if not re.match(r'frc\d+', str(team_key)): raise ParserInputException( "Bad team_key: '{}'. Must follow format: 'frcXXX'". format(team_key)) else: selection['picks'].append(team_key) alliance_selections.append(selection) return alliance_selections
def _parse_coord(cls, coords, data_length): if len(coords) != data_length: raise ParserInputException("Length of coordinate data must be consistent.") for coord in coords: if not (isinstance(coord, float) or coord is None): raise ParserInputException("Coordinate data must be an array of floats or nulls.") return coords
def parse(self, awards_json, event_key): """ Parse JSON that contains a list of awards where each award is a dict of: name_str: String of award name. ex: "Tournament Winner" or "Dean's List Finalist" team_key: String in the format "frcXXX" for the team that won the award. Can be null. awardee: String corresponding to the name of an individual that won the award. Can be null. """ try: awards = json.loads(awards_json) except: raise ParserInputException("Invalid JSON. Please check input.") awards_by_key = {} for award in awards: if type(award) is not dict: raise ParserInputException("Awards must be dicts.") name_str = award.get('name_str', None) team_key = award.get('team_key', None) awardee = award.get('awardee', None) if name_str is None: raise ParserInputException("Award must have a 'name_str'") if team_key and not re.match(r'frc\d+', str(team_key)): raise ParserInputException("Bad team_key: '{}'. Must follow format 'frcXXX' or be null.".format(team_key)) award_type_enum = AwardHelper.parse_award_type(name_str) if award_type_enum is None: raise ParserInputException("Cannot determine award type from: '{}'. Please contact a www.thebluealliance.com admin.".format(name_str)) recipient_json = json.dumps({ 'team_number': int(team_key[3:]) if team_key else None, 'awardee': awardee, }) award_key_name = Award.render_key_name(event_key, award_type_enum) if award_key_name in awards_by_key: if team_key is not None: awards_by_key[award_key_name]['team_key_list'].append(team_key) awards_by_key[award_key_name]['recipient_json_list'].append(recipient_json) else: awards_by_key[award_key_name] = { 'name_str': name_str, 'award_type_enum': award_type_enum, 'team_key_list': [team_key] if team_key else [], 'recipient_json_list': [recipient_json], } return awards_by_key.values()
def parse(self, team_list_json): """ Parse JSON that contains team_keys in the format "frcXXX" Format is as follows: [team1, team1, team3, ...] """ try: team_keys = json.loads(team_list_json) except: raise ParserInputException("Invalid JSON. Please check input.") for team_key in team_keys: if not re.match(r'frc\d+', str(team_key)): raise ParserInputException("Bad team_key: '{}'. Must follow format 'frcXXX' or be null.".format(team_key)) return team_keys
def _parse_alliance(cls, alliance, data_length): if len(alliance) != 3: # Not necessarily true in the future, but encofing this for now raise ParserInputException("Must have 3 teams per alliance.") parsed_alliance = [] for team in alliance: parsed_alliance.append(cls._parse_team(team, data_length)) return parsed_alliance
def _parse_team(cls, team, data_length): # Check team_key format team_key = team.get('team_key') if not re.match(r'frc\d+', str(team_key)): raise ParserInputException("Bad 'team_key': '{}'. Must follow format 'frcXXX'.".format(team_key)) # Check xs, ys format xs = cls._parse_coord(team.get('xs', []), data_length) ys = cls._parse_coord(team.get('ys', []), data_length) for x, y in zip(xs, ys): if (x is None and y is not None) or (x is not None and y is None): raise ParserInputException("Coordinate data at a given time index for a given team may not have a mix of null and non-null values.") parsed_team = { 'team_key': team_key, 'xs': xs, 'ys': ys, } return parsed_team
def parse(self, rankings_json): """ Parse JSON that contains a dict of: breakdowns: List of ranking breakdowns, such as "QS", "Auton", "Teleop", etc. Breakdowns will be shown in the order given. rankings: List of ranking dicts Ranking dict format: team_key: String corresponding to a particular team in the format "frcXXX" rank: Integer rank of the particular team wins: Integer of the number of non-surrogate wins losses: Integer of the number of non-surrogate losses ties: Integer of the number of non-surrogate ties played: Integer of the number of non-surrogate matches played dqs: Integer of the number of non-surrogate DQed matches breakdown: Dict where the key is a breakdown and the value is its value """ try: data = json.loads(rankings_json) except: raise ParserInputException("Invalid JSON. Please check input.") if type(data) is not dict: raise ParserInputException("Data must be a dict.") if 'breakdowns' not in data or type(data['breakdowns']) is not list: raise ParserInputException("Data must have a list 'breakdowns'") if 'rankings' not in data or type(data['rankings']) is not list: raise ParserInputException("Data must have a list 'rankings'") rankings = [['Rank', 'Team'] + data['breakdowns'] + ['Record (W-L-T)', 'DQ', 'Played']] for ranking in data['rankings']: if type(ranking) is not dict: raise ParserInputException("Ranking must be a dict.") if 'team_key' not in ranking or not re.match( r'frc\d+', str(ranking['team_key'])): raise ParserInputException( "Ranking must have a 'team_key' that follows the format 'frcXXX'" ) for attr in ['rank', 'wins', 'losses', 'ties', 'played', 'dqs']: if attr not in ranking or type(ranking[attr]) is not int: raise ParserInputException( "Ranking must have a integer '{}'".format(attr)) row = [ranking['rank'], ranking['team_key'][3:]] for b in data['breakdowns']: row.append(ranking.get(b, '--')) row.extend([ '{}-{}-{}'.format(ranking['wins'], ranking['losses'], ranking['ties']), ranking['dqs'], ranking['played'] ]) rankings.append(row) return rankings
def _process_request(self, request, event_key): try: event_info = json.loads(request.body) except Exception: self._errors = json.dumps({"Error": "Invalid json. Check input."}) self.abort(400) if not isinstance(event_info, dict) or not event_info: self._errors = json.dumps({"Error": "Invalid json. Check input."}) self.abort(400) event = Event.get_by_id(event_key) if not event: self._errors = json.dumps( {"Error": "Event {} not found".format(event_key)}) self.abort(404) do_team_remap = False for field, value in event_info.iteritems(): if field not in self.ALLOWED_EVENT_PARAMS: continue if field == "webcasts": # Do special processing here because webcasts are janky if not isinstance(value, list): self._errors = json.dumps( {"Error": "Invalid json. Check input"}) self.abort(400) return webcast_list = [] for webcast in value: if not isinstance(webcast, dict): self._errors = json.dumps( {"Error": "Invalid json. Check input"}) self.abort(400) return if 'url' in webcast: webcast_list.append( WebcastParser.webcast_dict_from_url( webcast['url'])) elif 'type' in webcast and 'channel' in webcast: webcast_list.append(webcast) webcast_list = [w for w in webcast_list if w is not None] EventWebcastAdder.add_webcast( event, webcast_list, False, # Don't createOrUpdate yet ) elif field == "remap_teams": # Validate remap_teams if not isinstance(value, dict): raise ParserInputException( "Invalid reamap_teams. Check input") for temp_team, remapped_team in value.items(): temp_match = re.match(r'frc\d+', str(temp_team)) remapped_match = re.match(r'frc\d+[B-Z]?', str(remapped_team)) if not temp_match or ( temp_match and (temp_match.group(0) != str(temp_team))): raise ParserInputException( "Bad team: '{}'. Must follow format 'frcXXX'.". format(temp_team)) if not remapped_match or ( remapped_match and (remapped_match.group(0) != str(remapped_team))): raise ParserInputException( "Bad team: '{}'. Must follow format 'frcXXX' or 'frcXXX[B-Z]'." .format(remapped_team)) do_team_remap = True setattr(event, field, value) else: try: if field == "first_event_code": event.official = value is not None field = "first_code" # Internal property is different setattr(event, field, value) except Exception, e: self._errors({ "Error": "Unable to set event field", "Message": str(e) }) self.abort(400)
def parse(self, matches_json, year): """ Parse JSON that contains a list of matches for a given year where each match is a dict of: comp_level: String in the set {"qm", "ef", "qf", "sf", "f"} set_number: Integer identifying the elim set number. Ignored for qual matches. ex: the 4 in qf4m2 match_number: Integer identifying the match number within a set. ex: the 2 in qf4m2 alliances: Dict of {'red': {'teams': ['frcXXX'...], 'score': S, 'surrogates': ['frcXXX'...], 'dqs': ['frcXXX'...]}, 'blue': {...}}. Where scores (S) are integers. Null scores indicate that a match has not yet been played. surrogates and dqs are optional. score_breakdown: Dict of {'red': {K1: V1, K2: V2, ...}, 'blue': {...}}. Where Kn are keys and Vn are values for those keys. time_string: String in the format "(H)H:MM AM/PM" for when the match will be played in the event's local timezone. ex: "9:15 AM" time: UTC time of the match as a string in ISO 8601 format (YYYY-MM-DDTHH:MM:SS). """ try: matches = json.loads(matches_json) except: raise ParserInputException("Invalid JSON. Please check input.") parsed_matches = [] for match in matches: if type(match) is not dict: raise ParserInputException("Matches must be dicts.") comp_level = match.get('comp_level', None) set_number = match.get('set_number', None) match_number = match.get('match_number', None) alliances = match.get('alliances', None) score_breakdown = match.get('score_breakdown', None) time_string = match.get('time_string', None) time_utc = match.get('time_utc', None) if comp_level is None: raise ParserInputException("Match must have a 'comp_level'") if comp_level not in Match.COMP_LEVELS: raise ParserInputException( "'comp_level' must be one of: {}".format( Match.COMP_LEVELS)) if comp_level == 'qm': set_number = 1 elif set_number is None or type(set_number) is not int: raise ParserInputException( "Match must have an integer 'set_number'") if match_number is None or type(match_number) is not int: raise ParserInputException( "Match must have an integer 'match_number'") if type(alliances) is not dict: raise ParserInputException("'alliances' must be a dict") else: for color, details in alliances.items(): if color not in {'red', 'blue'}: raise ParserInputException( "Alliance color '{}' not recognized".format(color)) if 'teams' not in details: raise ParserInputException( "alliances[color] must have key 'teams'") if 'score' not in details: raise ParserInputException( "alliances[color] must have key 'score'") for team_key in details['teams']: if not re.match(r'frc\d+', str(team_key)): raise ParserInputException( "Bad team: '{}'. Must follow format 'frcXXX'.". format(team_key)) if details['score'] is not None and type( details['score']) is not int: raise ParserInputException( "alliances[color]['score'] must be an integer or null" ) for team_key in details.get('surrogates', []): if not re.match(r'frc\d+', str(team_key)): raise ParserInputException( "Bad surrogate team: '{}'. Must follow format 'frcXXX'." .format(team_key)) if team_key not in details['teams']: raise ParserInputException( "Bad surrogate team: '{}'. Must be a team in the match.'." .format(team_key)) for team_key in details.get('dqs', []): if not re.match(r'frc\d+', str(team_key)): raise ParserInputException( "Bad dq team: '{}'. Must follow format 'frcXXX'." .format(team_key)) if team_key not in details['teams']: raise ParserInputException( "Bad dq team: '{}'. Must be a team in the match.'." .format(team_key)) if score_breakdown is not None: if type(score_breakdown) is not dict: raise ParserInputException( "'score_breakdown' must be a dict") else: for color, breakdown in score_breakdown.items(): if color not in {'red', 'blue'}: raise ParserInputException( "Alliance color '{}' not recognized".format( color)) for k in breakdown.keys(): is_valid = MatchHelper.is_valid_score_breakdown_key( k, year) if is_valid != True: raise ParserInputException( "Valid score breakdowns for {} are: {}". format(year, is_valid)) datetime_utc = None if time_utc is not None: try: import iso8601 datetime_utc = iso8601.parse_date(time_utc) # remove timezone info because DatetimeProperty can't handle timezones datetime_utc = datetime_utc.replace(tzinfo=None) except Exception: raise ParserInputException( "Could not parse 'time_utc'. Check that it is in ISO 8601 format." ) # validation passed. build new dicts to sanitize parsed_alliances = { 'red': { 'teams': alliances['red']['teams'], 'score': alliances['red']['score'], 'surrogates': alliances['red'].get('surrogates', []), 'dqs': alliances['red'].get('dqs', []), }, 'blue': { 'teams': alliances['blue']['teams'], 'score': alliances['blue']['score'], 'surrogates': alliances['blue'].get('surrogates', []), 'dqs': alliances['blue'].get('dqs', []), }, } parsed_match = { 'comp_level': comp_level, 'set_number': set_number, 'match_number': match_number, 'alliances_json': json.dumps(parsed_alliances), 'score_breakdown_json': json.dumps(score_breakdown) if score_breakdown is not None else None, 'time_string': time_string, 'time': datetime_utc, 'team_key_names': parsed_alliances['red']['teams'] + parsed_alliances['blue']['teams'], } parsed_matches.append(parsed_match) return parsed_matches
def parse(self, rankings_json): """ Parse JSON that contains a dict of: breakdowns: List of ranking breakdowns, such as "Wins", "Qual Avg", "QS", "Auton", "Teleop", etc. Breakdowns will be shown in the order given. rankings: List of ranking dicts Ranking dict format: team_key: String corresponding to a particular team in the format "frcXXX" rank: Integer rank of the particular team played: Integer of the number of non-surrogate matches played dqs: Integer of the number of non-surrogate DQed matches breakdown: Dict where the key is a breakdown and the value is its value """ try: data = json.loads(rankings_json) except: raise ParserInputException("Invalid JSON. Please check input.") if type(data) is not dict: raise ParserInputException("Data must be a dict.") if 'breakdowns' not in data or type(data['breakdowns']) is not list: raise ParserInputException("Data must have a list 'breakdowns'") if 'rankings' not in data or type(data['rankings']) is not list: raise ParserInputException("Data must have a list 'rankings'") # Account for wins/losses/ties to be present and show them as Record (W-L-T) post_breakdowns = ['DQ', 'Played'] has_record = False if "wins" in data["breakdowns"] and "losses" in data[ "breakdowns"] and "ties" in data["breakdowns"]: post_breakdowns.insert(0, 'Record (W-L-T)') has_record = True # remove wlt from breakdown list so they're not added twice data["breakdowns"].remove("wins") data["breakdowns"].remove("losses") data["breakdowns"].remove("ties") rankings = [['Rank', 'Team'] + data['breakdowns'] + post_breakdowns] for ranking in data['rankings']: if type(ranking) is not dict: raise ParserInputException("Ranking must be a dict.") if 'team_key' not in ranking or not re.match( r'frc\d+', str(ranking['team_key'])): raise ParserInputException( "Ranking must have a 'team_key' that follows the format 'frcXXX'" ) for attr in ['rank', 'played', 'dqs']: if attr not in ranking or type(ranking[attr]) is not int: raise ParserInputException( "Ranking must have a integer '{}'".format(attr)) row = [ranking['rank'], ranking['team_key'][3:]] for b in data['breakdowns']: row.append(ranking.get(b, '--')) # Special case for when wlt passed if has_record: row.extend([ '{}-{}-{}'.format(ranking['wins'], ranking['losses'], ranking['ties']), ranking['dqs'], ranking['played'] ]) else: row.extend([ranking['dqs'], ranking['played']]) rankings.append(row) return rankings