def match(self, route: str, method: str): """ Match a route. This will search down our routes, and then down our children's routes, to see if we can find a match for the specified route. .. versionchanged:: 1.8.5 This is now effectively only used on the root blueprint. Children blueprints should have this called explicitly by other handlers to match routes only on those blueprints. .. versionchanged:: 1.9.2 This now does a best attempt at getting the correct blueprint for the 405 in the tree. :param route: The route to match, e.g ``/abc/def``. :param method: The method of the route. :raises: A :class:`kyoukai.exc.HTTPException` with code 415 if the method is not valid. :returns: The :class:`Route` if the route was matched, or None. """ matches = self.gather_routes(route, method) if not matches: return None else: # Loop through each route, and check the method. # If no method matches, then raise the HTTPException. Otherwise, return the route. # This allows for multiple routes with the same method. for route in matches: if route.kyokai_method_allowed(method): return route else: # This is called when no return successfully hit. # Do a best attempt at getting the common ancestor blueprint. common_blueprints = None for route in matches: full_match = set(route.bp.tree_path) # If common_blueprints is defined, we can do an intersection on it. # Otherwise, we just set it to the current set of Blueprints. if common_blueprints is not None: common_blueprints = common_blueprints.intersection( full_match) else: common_blueprints = full_match # Turn that set back into a list, and sort it by depth. cmn_bp = list(common_blueprints) cmn_bp = sorted(cmn_bp, key=lambda bp: bp.depth) # Get the bottom most blueprint, which is the best ancestor for both of these in the tree. blueprint_to_use = cmn_bp[-1] # Build the exception with the new blueprint. exc = HTTPException(405) exc.bp = blueprint_to_use raise exc
def matcher(self): """ Gets the compiled matcher for the current route. :return: """ if self._matcher is None: try: self._matcher = re.compile(self.bp.prefix + self._match_str) except sre_constants.error as e: # Raise a new HTTPException(500) from this. exc = HTTPException(500) exc.route = self raise exc from e return self._matcher
async def get_heroes(mode, ctx, battletag): data = await bz.region_helper_v2(ctx, battletag, region=ctx.request.args.get("region", None), platform=ctx.request.args.get("platform", "pc")) if data == (None, None): raise HTTPException(404) parsed, region = data built_dict = {"region": region, "battletag": battletag, "heroes": {}} if mode == "competitive": _hero_info = parsed.findall(".//div[@id='competitive-play']/section/div/div[@data-group-id='comparisons']")[0] elif mode == "quickplay": _hero_info = parsed.findall(".//div[@data-group-id='comparisons']")[0] else: _hero_info = parsed.findall(".//div[@data-group-id='comparisons']")[0] hero_info = _hero_info.findall(".//div[@class='bar-text']") # Loop over each one, extracting the name and hours counted. for child in hero_info: name, played = child.getchildren() name, played = name.text.lower(), played.text.lower() name = unidecode.unidecode(name) name = name.replace(".", "").replace(": ", "") # d.va and soldier: 76 special cases if played == "--": time = 0 else: time = util.try_extract(played) built_dict["heroes"][name] = time return built_dict
def match(self, route: str, method: str): """ Match a route. This will search down our routes, and then down our children's routes, to see if we can find a match for the specified route. .. versionchanged:: 1.8.5 This is now effectively only used on the root blueprint. Children blueprints should have this called explicitly by other handlers to match routes only on those blueprints. :param route: The route to match, e.g ``/abc/def``. :param method: The method of the route. :raises: A :class:`kyoukai.exc.HTTPException` with code 415 if the method is not valid. :returns: The :class:`Route` if the route was matched, or None. """ matches = self.gather_routes(route, method) if not matches: return None else: # Loop through each route, and check the method. # If no method matches, then raise the HTTPException. Otherwise, return the route. # This allows for multiple routes with the same method. for route in matches: if route.kyokai_method_allowed(method): return route else: # This is called when no return successfully hit. raise HTTPException(405)
async def convert_args(ctx, coro, *args, bound=False): """ Converts a the arguments of a function using it's signature. Will ignore `self` if bound is True. :param coro: The coroutine function to inspect for the signature. :param args: The arguments that are to be passed into the function. The first one should be the HTTPRequestContext; this is ignored. :param bound: If this route is bound to a View. Setting this will ignore the first parameter of the signature. """ signature = inspect.signature(coro) params = signature.parameters if len(args) != len(params): raise IndexError( "Arguments passed in were not the same length as {}'s function signature" .format(coro)) new_args = [] for num, (name, value) in enumerate(params.items()): # If bound, just ignore the `self` param. if bound and num == 0: new_args.append(args[0]) continue item = args[num] # Skip over the HTTPRequestContext. if isinstance(item, HTTPRequestContext): new_args.append(item) continue # Extract the annotation from the parameter. assert isinstance(value, inspect.Parameter) type_ = value.annotation if type_ not in _converters: # Just add the argument, without converting. new_args.append(item) else: _converter = _converters[type_] # Convert the arg. try: converted = _converter(ctx, item) if inspect.isawaitable(converted): result = await converted else: result = converted new_args.append(result) except (TypeError, ValueError) as e: # Raise a bad request error. ctx.app.logger.error("Failed to convert {} to {}\n{}".format( item, type_, ''.join(traceback.format_exc()))) raise HTTPException(400) from e return new_args
def _parse_body(self): """ Parses the body data. """ if self.headers.get("Content-Type") != "application/json": # Parse the form data out. f_parser = formparser.FormDataParser() # Wrap the body in a BytesIO. body = BytesIO(self.body.encode()) # The headers can't be directly passed into Werkzeug. # Instead, we have to get a the custom content type, then pass in some fake WSGI options. mimetype, c_t_args = parse_options_header( self.headers.get("Content-Type")) if mimetype: # We have a valid mimetype. # This is good! # Now parse the body. # Construct a fake WSGI environment. env = { "Content-Type": self.headers.get("Content-Type"), "Content-Length": self.headers.get("Content-Length") } # Take the boundary out of the Content-Type, if applicable. boundary = c_t_args.get("boundary") if boundary is not None: env["boundary"] = boundary # Get a good content length. content_length = self.headers.get("Content-Length") try: content_length = int(content_length) except ValueError: content_length = len(self.body) except TypeError: # NoneType... raise HTTPException(411) # Then, the form body itself is parsed. data = f_parser.parse(body, mimetype, content_length, options=env) # Extract the new data from the form parser. self._form.update(data[1]) self.files.update(data[2])
def get_static(self, filename: str) -> Response: """ Gets a file, using static, but returns a Response instead of the file handle. """ content = self.get_static_file(filename) if not content: raise HTTPException(404) with content: path = self.get_static_path(filename) mimetype = mimetypes.guess_type(path)[0] if not mimetype: if _has_magic: mimetype = magic.from_file(path, mime=True) if isinstance(mimetype, bytes): mimetype = mimetype.decode() else: mimetype = "application/octet-stream" return Response(200, body=content.read(), headers={"Content-Type": mimetype})
async def bl_get_stats(mode, ctx, battletag): data = await bz.region_helper_v2(ctx, battletag, region=ctx.request.args.get("region", None), platform=ctx.request.args.get("platform", "pc")) if data == (None, None): raise HTTPException(404) parsed, region = data # Start the dict. built_dict = {"region": region, "battletag": battletag, "game_stats": [], "overall_stats": {}, "average_stats": []} # Get the prestige. prestige = parsed.xpath(".//div[@class='player-level']")[0] # Extract the background-image from the styles. try: bg_image = [x for x in prestige.values() if 'background-image' in x][0] except IndexError: # Cannot find background-image. # Yikes! # Don't set a prestige. built_dict["overall_stats"]["prestige"] = 0 else: for key, val in PRESTIGE.items(): if key in bg_image: prestige_num = val break else: # Unknown. prestige_num = None built_dict["overall_stats"]["prestige"] = prestige_num # Parse out the HTML. level = int(parsed.findall(".//div[@class='player-level']/div")[0].text) built_dict["overall_stats"]["level"] = level hasrank = parsed.findall(".//div[@class='competitive-rank']/div") if hasrank: comprank = int(hasrank[0].text) else: comprank = None built_dict["overall_stats"]["comprank"] = comprank # Fetch Avatar built_dict["overall_stats"]["avatar"] = parsed.find(".//img[@class='player-portrait']").attrib['src'] if mode == "competitive": hascompstats = parsed.xpath(".//div[@data-group-id='stats' and @data-category-id='0x02E00000FFFFFFFF']") if len(hascompstats) != 2: return {"error": 404, "msg": "competitive stats not found", "region": region}, 404 stat_groups = hascompstats[1] elif mode == "quickplay": stat_groups = parsed.xpath(".//div[@data-group-id='stats' and @data-category-id='0x02E00000FFFFFFFF']")[0] else: # how else to handle fallthrough case? stat_groups = parsed.xpath(".//div[@data-group-id='stats' and @data-category-id='0x02E00000FFFFFFFF']")[0] # Highlight specific stat groups. death_box = stat_groups[4] try: game_box = stat_groups[6] except IndexError: game_box = stat_groups[5] # Calculate the wins, losses, and win rate. try: wins = int(game_box.xpath(".//text()[. = 'Games Won']/../..")[0][1].text.replace(",", "")) except IndexError: # weird edge case wins = 0 g = game_box.xpath(".//text()[. = 'Games Played']/../..") if len(g) < 1: # Blizzard f****d up, temporary quick fix for #70 games, losses = 0, 0 wr = 0 else: games = int(g[0][1].text.replace(",", "")) losses = games - wins wr = floor((wins / games) * 100) # Update the dictionary. built_dict["overall_stats"]["games"] = games built_dict["overall_stats"]["losses"] = losses built_dict["overall_stats"]["wins"] = wins built_dict["overall_stats"]["win_rate"] = wr # Build a dict using the stats. _t_d = {} _a_d = {} for subbox in stat_groups: trs = subbox.findall(".//tbody/tr") # Update the dict with [0]: [1] for subval in trs: name, value = subval[0].text.lower().replace(" ", "_").replace("_-_", "_"), subval[1].text # Try and parse out the value. It might be a time! # If so, try and extract the time. nvl = util.try_extract(value) if 'average' in name.lower(): _a_d[name.replace("_average", "_avg")] = nvl else: _t_d[name] = nvl # Manually add the KPD. _t_d["kpd"] = round(_t_d["eliminations"] / _t_d["deaths"], 2) built_dict["game_stats"] = _t_d built_dict["average_stats"] = _a_d built_dict["competitive"] = mode == "competitive" return built_dict
async def _get_extended_data(ctx, battletag, hero_name, competitive=False): if not hero_name: return { "error": 400, "msg": "missing hero name" }, 400 hero_name = unidecode.unidecode(hero_name) if hero_name in hero_data_div_ids: requested_hero_div_id = hero_data_div_ids[hero_name] else: return { "error": 404, "msg": "bad hero name" }, 404 data = await bz.region_helper_v2(ctx, battletag, region=ctx.request.args.get("region", None), platform=ctx.request.args.get("platform", "pc")) if data == (None, None): raise HTTPException(404) parsed, region = data # Start the dict. built_dict = {"region": region, "battletag": battletag} _root = parsed.xpath( ".//div[@id='{}']".format("competitive-play" if competitive else "quick-play") )[0] _stat_groups = _root.xpath( ".//div[@data-group-id='stats' and @data-category-id='{0}']".format(requested_hero_div_id) ) if len(_stat_groups) == 0: # no hero data return {"error": 404, "msg": "hero data not found"}, 404 stat_groups = _stat_groups[0] _t_d = {} hero_specific_box = stat_groups[0] trs = hero_specific_box.findall(".//tbody/tr") # Update the dict with [0]: [1] for subval in trs: name, value = subval[0].text, subval[1].text if 'average' in name.lower(): # No averages, ty continue nvl = util.try_extract(value) _t_d[name.lower().replace(" ", "_").replace("_-_", "_")] = nvl built_dict["hero_stats"] = _t_d _t_d = {} for subbox in stat_groups[1:]: trs = subbox.findall(".//tbody/tr") # Update the dict with [0]: [1] for subval in trs: name, value = subval[0].text, subval[1].text if 'average' in name.lower(): # No averages, ty continue nvl = util.int_or_string(value) _t_d[name.lower().replace(" ", "_").replace("_-_", "_")] = nvl built_dict["general_stats"] = _t_d built_dict["competitive"] = competitive return built_dict
async def delegate_request(self, protocol, ctx: HTTPRequestContext): """ Handles a :class:`kyoukai.context.HTTPRequestContext` and it's underlying request, processing it to the route handlers and such in the blueprints. This is an **internal** method, and should not be used outside of the protocol, or for testing. """ async with ctx: # Acquire the lock on the protocol. async with protocol.lock: # Check if we should skip our own handling and go straight to the debugger. if self.debug: if '__debugger__' in ctx.request.args and ctx.request.args[ "__debugger__"] == "yes": resp = self._debugger.debug(ctx, None) protocol.handle_resp(resp[1]) return # Check if there's a host header. if ctx.request.version != "1.0": host = ctx.request.headers.get("host", None) if not host: exc = HTTPException(400) self.log_request(ctx, code=400) await self.handle_http_error(exc, protocol, ctx) return # First, try and match the route. try: route = self._match_route(ctx.request.path, ctx.request.method) except HTTPException as e: # We matched it; but the route doesn't work for this method. # So we catch the 405 error, if e.code == 405: self.log_request(ctx, code=e.code) await self.handle_http_error(e, protocol, ctx) return elif e.code == 500: # Failure matching, probably. self.log_request(ctx, code=e.code) await self.handle_http_error(e, protocol, ctx) self.logger.error( "Unhandled exception in route matching:\n {}". format(''.join(traceback.format_exc()))) return else: self.logger.error( "??????? Something went terribly wrong.") return # If the route did not match, return a 404. if not route: fof = HTTPException(404) self.log_request(ctx, code=404) await self.handle_http_error(fof, protocol, ctx) return # Set the `route` and `bp` items on the context. ctx.blueprint = route.bp ctx.route = route # Try and invoke the Route. try: # Note that this will already be a Response. # The route should call `app._wrap_response` when handling the response. # This is because routes are responsible for pre-route and post-route hooks, calling them in the # blueprint as appropriate. # So we just pass ourselves to the route and hope it invokes properly. response = await route.invoke(ctx) except HTTPException as e: # Handle a HTTPException normally. self.log_request(ctx, e.code) # Set the route of the exception. e.route = route await self.handle_http_error(e, protocol, ctx) return except Exception as e: # An uncaught exception has propogated down to our level - oh dear. # Catch it, turn it into a 500, and return. exc = HTTPException(500) # Set the cause of the HTTP exception. Useful for 500 error handlers. exc.__cause__ = e # Set the route of the exception. exc.route = route self.log_request(ctx, 500) should_err = await self.handle_http_error( exc, protocol, ctx) if should_err: self.logger.exception( "Unhandled exception in route `{}`:".format( repr(route))) return else: # If there is no error happening, just log it as normal. self.log_request(ctx, response.code) # Respond with the response. protocol.handle_resp(response) # Check if we should Keep-Alive it. if not ctx.request.should_keep_alive: protocol.close()