Example #1
0
def test_sort_order() -> None:
    assert sort_parser.parse("sort foo") == [Sort("foo", "asc")]
    assert sort_parser.parse("sort foo asc") == [Sort("foo", "asc")]
    parsed = sort_parser.parse("sort foo asc, bla desc, bar")
    assert parsed == [
        Sort("foo", "asc"),
        Sort("bla", "desc"),
        Sort("bar", "asc")
    ]
    assert_round_trip(query_parser,
                      Query.by("test").add_sort("test").add_sort("goo"))
Example #2
0
def test_query() -> None:
    query = (Query.by(
        "ec2",
        P("cpu") > 4, (P("mem") < 23) | (P("mem") < 59)).merge_with(
            "cloud", Navigation(1, Navigation.Max,
                                direction=Direction.inbound),
            Query.mk_term("cloud")).traverse_out().filter(
                P("some.int.value") < 1,
                P("some.other") == 23).traverse_out().filter(
                    P("active") == 12,
                    P.function("in_subnet").on(
                        "ip", "1.2.3.4/96")).filter_with(
                            WithClause(WithClauseFilter(
                                "==", 0), Navigation())).group_by([
                                    AggregateVariable(
                                        AggregateVariableName("foo"))
                                ], [AggregateFunction("sum", "cpu")]).add_sort(
                                    Sort("test", "asc")).with_limit(10))
    assert str(query) == (
        'aggregate(foo: sum(cpu)):((is("ec2") and cpu > 4) and (mem < 23 or mem < 59)) '
        '{cloud: all <-default[1:]- is("cloud")} -default-> '
        "(some.int.value < 1 and some.other == 23) -default-> "
        '(active == 12 and in_subnet(ip, "1.2.3.4/96")) '
        "with(empty, -default->) sort test asc limit 10")
    assert_round_trip(query_parser, query)
Example #3
0
def test_sort_order_for_synthetic_prop(foo_model: Model,
                                       graph_db: GraphDB) -> None:
    def check_sort_in_query(q: Query, expected_sort: str) -> None:
        query_str, _ = to_query(graph_db, QueryModel(q, foo_model))
        assert f"SORT {expected_sort}" in query_str, f"Expected {expected_sort} in {query_str}"

    check_sort_in_query(
        Query.by("foo").add_sort(Sort("reported.age")),
        "m0.reported.ctime desc")
    check_sort_in_query(
        Query.by("foo").add_sort(Sort("some.age")), "m0.some.age asc")
    check_sort_in_query(
        Query.by("foo").add_sort(Sort("reported.ctime")),
        "m0.reported.ctime asc")
    check_sort_in_query(
        Query.by("foo").add_sort(Sort("metadata.expired")),
        "m0.metadata.expired asc")
Example #4
0
def single_sort_arg_parser() -> Parser:
    name = yield variable_dp
    order = yield (space_dp >> sort_order_p).optional()
    return Sort(name, order if order else SortOrder.Asc)
Example #5
0
                result = self.all_parts[arg].rendered_help(ctx)
            elif arg and arg in self.aliases:
                alias = self.aliases[arg]
                explain = f"{arg} is an alias for {alias}\n\n"
                result = explain + self.all_parts[alias].rendered_help(ctx)
            else:
                result = f"No command found with this name: {arg}"

            return stream.just(result)

        return CLISource.single(help_command)


CLIArg = Tuple[CLICommand, Optional[str]]
# If no sort is defined in the part, we use this default sort order
DefaultSort = [Sort("/reported.kind"), Sort("/reported.name"), Sort("/reported.id")]


class CLI:
    """
    The CLI has a defined set of dependencies and knows a list if commands.
    A string can be parsed into a command line that can be executed based on the list of available commands.
    """

    def __init__(
        self, dependencies: CLIDependencies, parts: List[CLICommand], env: Dict[str, Any], aliases: Dict[str, str]
    ):
        dependencies.extend(cli=self)
        help_cmd = HelpCommand(dependencies, parts, aliases)
        cmds = {p.name: p for p in parts + [help_cmd]}
        alias_cmds = {alias: cmds[name] for alias, name in aliases.items() if name in cmds and alias not in cmds}
Example #6
0
    async def create_query(
        self, commands: List[ExecutableCommand], ctx: CLIContext
    ) -> Tuple[Query, Dict[str, Any], List[ExecutableCommand]]:
        """
        Takes a list of query part commands and combine them to a single executable query command.
        This process can also introduce new commands that should run after the query is finished.
        Therefore, a list of executable commands is returned.
        :param commands: the incoming executable commands, which actions are all instances of SearchCLIPart.
        :param ctx: the context to execute within.
        :return: the resulting list of commands to execute.
        """

        # Pass parsed options to execute query
        # Multiple query commands are possible - so the dict is combined with every parsed query.
        parsed_options: Dict[str, Any] = {}

        async def parse_query(query_arg: str) -> Query:
            nonlocal parsed_options
            parsed, query_part = ExecuteSearchCommand.parse_known(query_arg)
            parsed_options = {**parsed_options, **parsed}
            # section expansion is disabled here: it will happen on the final query after all parts have been combined
            return await self.dependencies.template_expander.parse_query(
                "".join(query_part),
                None,
                omit_section_expansion=True,
                **ctx.env)

        query: Query = Query.by(AllTerm())
        additional_commands: List[ExecutableCommand] = []
        # We need to remember the first head/tail, since tail will reverse the sort order
        first_head_tail_in_a_row: Optional[CLICommand] = None
        head_tail_keep_order = True
        for command in commands:
            part = command.command
            arg = command.arg if command.arg else ""
            if isinstance(part, SearchPart):
                query = query.combine(await parse_query(arg))
            elif isinstance(part, SortPart):
                if query.current_part.sort == DefaultSort:
                    query = query.set_sort(*sort_args_p.parse(arg))
                else:
                    query = query.add_sort(*sort_args_p.parse(arg))
            elif isinstance(part, LimitPart):
                query = query.with_limit(limit_parser_direct.parse(arg))
            elif isinstance(part, PredecessorsPart):
                origin, edge = PredecessorsPart.parse_args(arg, ctx)
                query = query.traverse_in(origin, 1, edge)
            elif isinstance(part, SuccessorsPart):
                origin, edge = PredecessorsPart.parse_args(arg, ctx)
                query = query.traverse_out(origin, 1, edge)
            elif isinstance(part, AncestorsPart):
                origin, edge = PredecessorsPart.parse_args(arg, ctx)
                query = query.traverse_in(origin, Navigation.Max, edge)
            elif isinstance(part, DescendantsPart):
                origin, edge = PredecessorsPart.parse_args(arg, ctx)
                query = query.traverse_out(origin, Navigation.Max, edge)
            elif isinstance(part, AggregatePart):
                group_vars, group_function_vars = aggregate_parameter_parser.parse(
                    arg)
                query = replace(query,
                                aggregate=Aggregate(group_vars,
                                                    group_function_vars))
            elif isinstance(part, CountCommand):
                # count command followed by a query: make it an aggregation
                # since the output of aggregation is not exactly the same as count
                # we also add the aggregate_to_count command after the query
                assert query.aggregate is None, "Can not combine aggregate and count!"
                group_by_var = [
                    AggregateVariable(AggregateVariableName(arg), "name")
                ] if arg else []
                aggregate = Aggregate(
                    group_by_var, [AggregateFunction("sum", 1, [], "count")])
                # If the query should be explained, we want the output as is
                if "explain" not in parsed_options:
                    additional_commands.append(
                        self.command("aggregate_to_count", None, ctx))
                query = replace(query, aggregate=aggregate)
                query = query.set_sort(Sort(f"{PathRoot}count"))
            elif isinstance(part, HeadCommand):
                size = HeadCommand.parse_size(arg)
                limit = query.parts[0].limit or Limit(0, size)
                if first_head_tail_in_a_row and head_tail_keep_order:
                    query = query.with_limit(
                        Limit(limit.offset, min(limit.length, size)))
                elif first_head_tail_in_a_row and not head_tail_keep_order:
                    length = min(limit.length, size)
                    query = query.with_limit(
                        Limit(limit.offset + limit.length - length, length))
                else:
                    query = query.with_limit(size)
            elif isinstance(part, TailCommand):
                size = HeadCommand.parse_size(arg)
                limit = query.parts[0].limit or Limit(0, size)
                if first_head_tail_in_a_row and head_tail_keep_order:
                    query = query.with_limit(
                        Limit(limit.offset + max(0, limit.length - size),
                              min(limit.length, size)))
                elif first_head_tail_in_a_row and not head_tail_keep_order:
                    query = query.with_limit(
                        Limit(limit.offset, min(limit.length, size)))
                else:
                    head_tail_keep_order = False
                    query = query.with_limit(size)
                    p = query.current_part
                    # the limit might have created a new part - make sure there is a sort order
                    p = p if p.sort else replace(p, sort=DefaultSort)
                    # reverse the sort order -> limit -> reverse the result
                    query.parts[0] = replace(
                        p,
                        sort=[s.reversed() for s in p.sort],
                        reverse_result=True)
            else:
                raise AttributeError(
                    f"Do not understand: {part} of type: {class_fqn(part)}")

            # Remember the first head tail in a row of head tails
            if isinstance(part, (HeadCommand, TailCommand)):
                if not first_head_tail_in_a_row:
                    first_head_tail_in_a_row = part
            else:
                first_head_tail_in_a_row = None
                head_tail_keep_order = True

            # Define default sort order, if not already defined
            # A sort order is required to always return the result in a deterministic way to the user.
            # Deterministic order is required for head/tail to work
            parts = [
                pt if pt.sort else replace(pt, sort=DefaultSort)
                for pt in query.parts
            ]
            query = replace(query, parts=parts)

        # If the last part is a navigation, we need to add sort which will ingest a new part.
        with_sort = query.set_sort(
            *DefaultSort) if query.current_part.navigation else query
        # When all parts are combined, interpret the result on defined section.
        final_query = with_sort.on_section(ctx.env.get("section", PathRoot))
        options = ExecuteSearchCommand.argument_string(parsed_options)
        query_string = str(final_query)
        execute_search = self.command("execute_search", options + query_string,
                                      ctx)
        return final_query, parsed_options, [
            execute_search, *additional_commands
        ]
Example #7
0
                explain = f"{arg} is an alias for {alias}\n\n"
                result = explain + self.all_parts[alias].rendered_help(ctx)
            elif arg in self.alias_templates:
                result = self.alias_templates[arg].rendered_help(ctx)
            else:
                result = f"No command found with this name: {arg}"

            return stream.just(result)

        return CLISource.single(help_command)


CLIArg = Tuple[CLICommand, Optional[str]]
# If no sort is defined in the part, we use this default sort order
DefaultSort = [
    Sort("/reported.kind"),
    Sort("/reported.name"),
    Sort("/reported.id")
]


class CIKeyDict(Dict[str, Any]):
    """
    Special purpose dict used to lookup replacement values:
    - the dict should be case-insensitive: so now and NOW does not matter
    - if no replacement value is found, the key is returned.
    """
    def __init__(self, **kwargs: Any) -> None:
        super().__init__({k.lower(): v for k, v in kwargs.items()})

    def __getitem__(self, item: str) -> Any: