@@ -185,6 +185,19 @@ async def search_history(
185185 ) -> AsyncCursorContext :
186186 pass
187187
188+ @abstractmethod
189+ async def history_timeline (
190+ self ,
191+ query : QueryModel ,
192+ before : datetime ,
193+ after : datetime ,
194+ granularity : Optional [timedelta ] = None ,
195+ changes : Optional [List [HistoryChange ]] = None ,
196+ timeout : Optional [timedelta ] = None ,
197+ ** kwargs : Any ,
198+ ) -> AsyncCursorContext :
199+ pass
200+
188201 @abstractmethod
189202 async def list_possible_values (
190203 self ,
@@ -731,21 +744,21 @@ async def search_list(
731744 ttl = cast (Number , int (timeout .total_seconds ())) if timeout else None ,
732745 )
733746
734- async def search_history (
747+ def _history_query_model (
735748 self ,
736749 query : QueryModel ,
737750 changes : Optional [List [HistoryChange ]] = None ,
738751 before : Optional [datetime ] = None ,
739752 after : Optional [datetime ] = None ,
740- with_count : bool = False ,
741- timeout : Optional [timedelta ] = None ,
742- ** kwargs : Any ,
743- ) -> AsyncCursorContext :
753+ ) -> QueryModel :
744754 more_than_one = len (query .query .parts ) > 1
745755 has_invalid_terms = any (query .query .find_terms (lambda t : isinstance (t , (FulltextTerm , MergeTerm ))))
746756 has_navigation = any (p .navigation for p in query .query .parts )
747757 if more_than_one or has_invalid_terms or has_navigation :
748758 raise AttributeError ("Fulltext, merge terms and navigation is not supported in history queries!" )
759+ if before and after and before < after :
760+ raise AttributeError ("Before marks the end and should be greater than after!" )
761+
749762 # adjust query
750763 term = query .query .current_part .term
751764 if changes :
@@ -754,16 +767,25 @@ async def search_history(
754767 term = term .and_term (P .single ("changed_at" ).gt (utc_str (after )))
755768 if before :
756769 term = term .and_term (P .single ("changed_at" ).lt (utc_str (before )))
757- query = QueryModel (evolve (query .query , parts = [evolve (query .query .current_part , term = term )]), query .model )
770+ return QueryModel (evolve (query .query , parts = [evolve (query .query .current_part , term = term )]), query .model )
771+
772+ async def search_history (
773+ self ,
774+ query : QueryModel ,
775+ changes : Optional [List [HistoryChange ]] = None ,
776+ before : Optional [datetime ] = None ,
777+ after : Optional [datetime ] = None ,
778+ with_count : bool = False ,
779+ timeout : Optional [timedelta ] = None ,
780+ ** kwargs : Any ,
781+ ) -> AsyncCursorContext :
782+ query = self ._history_query_model (query , changes , before , after )
758783 q_string , bind = arango_query .history_query (self , query )
759784 trafo = (
760785 None
761786 if query .query .aggregate
762787 else self .document_to_instance_fn (
763- query .model ,
764- query ,
765- ["change" , "changed_at" , "before" , "diff" ],
766- id_column = "id" ,
788+ query .model , query , ["change" , "changed_at" , "before" , "diff" ], id_column = "id"
767789 )
768790 )
769791 ttl = cast (Number , int (timeout .total_seconds ())) if timeout else None
@@ -777,6 +799,26 @@ async def search_history(
777799 ttl = ttl ,
778800 )
779801
802+ async def history_timeline (
803+ self ,
804+ query : QueryModel ,
805+ before : datetime ,
806+ after : datetime ,
807+ granularity : Optional [timedelta ] = None ,
808+ changes : Optional [List [HistoryChange ]] = None ,
809+ timeout : Optional [timedelta ] = None ,
810+ ** kwargs : Any ,
811+ ) -> AsyncCursorContext :
812+ # ignore aggregates functions for timelines
813+ if query .query .aggregate is not None :
814+ query = evolve (query , query = evolve (query .query , aggregate = None ))
815+ # in case no granularity is provided we will compute one: 1/25 of the time range but at least one hour
816+ gran = max (granularity or abs (before - after ) / 25 , timedelta (hours = 1 ))
817+ query = self ._history_query_model (query , changes , before , after )
818+ q_string , bind = arango_query .history_query_histogram (self , query , gran )
819+ ttl = cast (Number , int (timeout .total_seconds ())) if timeout else None
820+ return await self .db .aql_cursor (query = q_string , bind_vars = bind , batch_size = 10000 , ttl = ttl )
821+
780822 async def search_graph_gen (
781823 self , query : QueryModel , with_count : bool = False , timeout : Optional [timedelta ] = None , ** kwargs : Any
782824 ) -> AsyncCursorContext :
@@ -1849,6 +1891,18 @@ async def search_history(
18491891 await self .event_sender .core_event (CoreEvent .HistoryQuery , context , ** counters )
18501892 return await self .real .search_history (query , changes , before , after , with_count , timeout , ** kwargs )
18511893
1894+ async def history_timeline (
1895+ self ,
1896+ query : QueryModel ,
1897+ before : datetime ,
1898+ after : datetime ,
1899+ granularity : Optional [timedelta ] = None ,
1900+ changes : Optional [List [HistoryChange ]] = None ,
1901+ timeout : Optional [timedelta ] = None ,
1902+ ** kwargs : Any ,
1903+ ) -> AsyncCursorContext :
1904+ return await self .real .history_timeline (query , before , after , granularity , changes , timeout , ** kwargs )
1905+
18521906 async def search_graph_gen (
18531907 self , query : QueryModel , with_count : bool = False , timeout : Optional [timedelta ] = None , ** kwargs : Any
18541908 ) -> AsyncCursorContext :
0 commit comments