From cfd8e29449341ab0647949b25d7384ea296ec0ed Mon Sep 17 00:00:00 2001 From: Matthias Veit Date: Tue, 6 Feb 2024 12:43:43 +0100 Subject: [PATCH] [resotocore][feat] Allow history search with multiple change types (#1900) --- resotocore/resotocore/cli/command.py | 23 ++++++++++++++----- resotocore/resotocore/db/graphdb.py | 12 +++++----- resotocore/resotocore/web/api.py | 4 ++-- .../tests/resotocore/cli/command_test.py | 2 +- .../tests/resotocore/db/graphdb_test.py | 4 ++-- 5 files changed, 28 insertions(+), 17 deletions(-) diff --git a/resotocore/resotocore/cli/command.py b/resotocore/resotocore/cli/command.py index 0b840090d2..e15c68730b 100644 --- a/resotocore/resotocore/cli/command.py +++ b/resotocore/resotocore/cli/command.py @@ -477,6 +477,7 @@ def args_info(self) -> ArgsInfo: help_text="type of change", expects_value=True, possible_values=[e.value for e in list(HistoryChange)], + can_occur_multiple_times=True, ), ArgInfo(expects_value=True, value_hint="search"), ] @@ -1391,7 +1392,7 @@ def parse_known(arg: str) -> Tuple[Dict[str, Any], str]: parser.add_argument("--history", dest="history", default=None, action="store_true") parser.add_argument("--after", dest="after", default=None) parser.add_argument("--before", dest="before", default=None) - parser.add_argument("--change", dest="change", default=None) + parser.add_argument("--change", dest="change", action="append", default=[]) parser.add_argument("--at", dest="at", default=None) try: # try to parse as many arguments as possible @@ -1408,12 +1409,20 @@ def parse_known(arg: str) -> Tuple[Dict[str, Any], str]: @staticmethod def argument_string(args: Dict[str, Any]) -> str: result = [] + + def render_flag(name: str, value: Any) -> None: + result.append(f"--{name}") + if value is not True: + result.append(f"'{value}'") # put the value into single quotes to maintain the spaces + for key, value in args.items(): if value is None or value is False: continue - result.append(f"--{key}") - if value is not True: - result.append(f"'{value}'") # put the value into single quotes to maintain the spaces + if isinstance(value, list): + for v in value: + render_flag(key, v) + else: + render_flag(key, value) return " ".join(result) + " " if result else "" def parse(self, arg: Optional[str] = None, ctx: CLIContext = EmptyContext, **kwargs: Any) -> CLISource: @@ -1473,8 +1482,10 @@ async def prepare() -> Tuple[CLISourceContext, AsyncIterator[Json]]: if history: before = if_set(parsed.get("before"), lambda x: parse_time_or_delta(strip_quotes(x))) after = if_set(parsed.get("after"), lambda x: parse_time_or_delta(strip_quotes(x))) - change = if_set(parsed.get("change"), lambda x: HistoryChange[strip_quotes(x)]) - context = await db.search_history(query_model, change, before, after, with_count=count, timeout=timeout) + changes = [HistoryChange[strip_quotes(x)] for x in parsed.get("change", [])] + context = await db.search_history( + query_model, changes, before, after, with_count=count, timeout=timeout + ) elif query.aggregate: context = await db.search_aggregation(query_model) elif with_edges: diff --git a/resotocore/resotocore/db/graphdb.py b/resotocore/resotocore/db/graphdb.py index cbc9da5e4d..2ac0542eb8 100644 --- a/resotocore/resotocore/db/graphdb.py +++ b/resotocore/resotocore/db/graphdb.py @@ -170,7 +170,7 @@ async def search_list( async def search_history( self, query: QueryModel, - change: Optional[HistoryChange] = None, + changes: Optional[List[HistoryChange]] = None, before: Optional[datetime] = None, after: Optional[datetime] = None, with_count: bool = False, @@ -691,7 +691,7 @@ async def search_list( async def search_history( self, query: QueryModel, - change: Optional[HistoryChange] = None, + changes: Optional[List[HistoryChange]] = None, before: Optional[datetime] = None, after: Optional[datetime] = None, with_count: bool = False, @@ -705,8 +705,8 @@ async def search_history( raise AttributeError("Fulltext, merge terms and navigation is not supported in history queries!") # adjust query term = query.query.current_part.term - if change: - term = term.and_term(P.single("change").eq(change.value)) + if changes: + term = term.and_term(P.single("change").is_in([c.value for c in changes])) if after: term = term.and_term(P.single("changed_at").gt(utc_str(after))) if before: @@ -1767,7 +1767,7 @@ async def search_list( async def search_history( self, query: QueryModel, - change: Optional[HistoryChange] = None, + changes: Optional[List[HistoryChange]] = None, before: Optional[datetime] = None, after: Optional[datetime] = None, with_count: bool = False, @@ -1776,7 +1776,7 @@ async def search_history( ) -> AsyncCursorContext: counters, context = query.query.analytics() await self.event_sender.core_event(CoreEvent.HistoryQuery, context, **counters) - return await self.real.search_history(query, change, before, after, with_count, timeout, **kwargs) + return await self.real.search_history(query, changes, before, after, with_count, timeout, **kwargs) async def search_graph_gen( self, query: QueryModel, with_count: bool = False, timeout: Optional[timedelta] = None diff --git a/resotocore/resotocore/web/api.py b/resotocore/resotocore/web/api.py index ac44c81e68..d4a1c0142e 100644 --- a/resotocore/resotocore/web/api.py +++ b/resotocore/resotocore/web/api.py @@ -1213,10 +1213,10 @@ async def query_history(self, request: Request, deps: TenantDependencies) -> Str graph_db, query_model = await self.graph_query_model_from_request(request, deps) before = request.query.get("before") after = request.query.get("after") - change = request.query.get("change") + changes = if_set(request.query.get("change"), lambda x: x.split(",")) async with await graph_db.search_history( query=query_model, - change=HistoryChange[change] if change else None, + change=[HistoryChange[change] for change in changes] if changes else None, before=parse_utc(before) if before else None, after=parse_utc(after) if after else None, ) as cursor: diff --git a/resotocore/tests/resotocore/cli/command_test.py b/resotocore/tests/resotocore/cli/command_test.py index 9b9b0f59a1..55458ce3c6 100644 --- a/resotocore/tests/resotocore/cli/command_test.py +++ b/resotocore/tests/resotocore/cli/command_test.py @@ -1034,7 +1034,7 @@ async def history_count(cmd: str) -> int: assert await history_count(f"history --change node_created") == 112 assert await history_count(f"history --change node_updated") == 1 assert await history_count(f"history --change node_deleted") == 0 - assert await history_count(f"history --change node_deleted") == 0 + assert await history_count(f"history --change node_created --change node_updated --change node_deleted") == 113 assert await history_count(f"history is(foo)") == 10 # combine all selectors assert await history_count(f"history --after 5m --before {five_min_later} --change node_created is(foo)") == 10 diff --git a/resotocore/tests/resotocore/db/graphdb_test.py b/resotocore/tests/resotocore/db/graphdb_test.py index 92fcb6082a..f58512ce3b 100644 --- a/resotocore/tests/resotocore/db/graphdb_test.py +++ b/resotocore/tests/resotocore/db/graphdb_test.py @@ -430,8 +430,8 @@ async def nodes(query: Query, **args: Any) -> List[Json]: assert len(await nodes(Query.by("foo"))) == 10 assert len(await nodes(Query.by("foo"), after=five_min_ago)) == 10 assert len(await nodes(Query.by("foo"), before=five_min_ago)) == 0 - assert len(await nodes(Query.by("foo"), after=five_min_ago, change=HistoryChange.node_created)) == 10 - assert len(await nodes(Query.by("foo"), after=five_min_ago, change=HistoryChange.node_deleted)) == 0 + assert len(await nodes(Query.by("foo"), after=five_min_ago, changes=[HistoryChange.node_created])) == 10 + assert len(await nodes(Query.by("foo"), after=five_min_ago, changes=[HistoryChange.node_deleted])) == 0 @mark.asyncio