Пример #1
0
    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
Пример #2
0
 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
Пример #3
0
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
Пример #4
0
    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)
Пример #5
0
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
Пример #6
0
    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])
Пример #7
0
    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})
Пример #8
0
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
Пример #9
0
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
Пример #10
0
    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()