edda-framework 0.6.0__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
edda/storage/protocol.py CHANGED
@@ -238,20 +238,34 @@ class StorageProtocol(Protocol):
238
238
  async def list_instances(
239
239
  self,
240
240
  limit: int = 50,
241
+ page_token: str | None = None,
241
242
  status_filter: str | None = None,
242
- ) -> list[dict[str, Any]]:
243
+ workflow_name_filter: str | None = None,
244
+ instance_id_filter: str | None = None,
245
+ started_after: datetime | None = None,
246
+ started_before: datetime | None = None,
247
+ ) -> dict[str, Any]:
243
248
  """
244
- List workflow instances with optional filtering.
249
+ List workflow instances with cursor-based pagination and filtering.
245
250
 
246
251
  This method JOINs workflow_instances with workflow_definitions to
247
252
  return instances along with their source code.
248
253
 
249
254
  Args:
250
- limit: Maximum number of instances to return
255
+ limit: Maximum number of instances to return per page
256
+ page_token: Cursor for pagination (format: "ISO_DATETIME||INSTANCE_ID")
251
257
  status_filter: Optional status filter (e.g., "running", "completed", "failed")
258
+ workflow_name_filter: Optional workflow name filter (partial match, case-insensitive)
259
+ instance_id_filter: Optional instance ID filter (partial match, case-insensitive)
260
+ started_after: Filter instances started after this datetime (inclusive)
261
+ started_before: Filter instances started before this datetime (inclusive)
252
262
 
253
263
  Returns:
254
- List of workflow instances, ordered by started_at DESC.
264
+ Dictionary containing:
265
+ - instances: List of workflow instances, ordered by started_at DESC
266
+ - next_page_token: Cursor for the next page, or None if no more pages
267
+ - has_more: Boolean indicating if there are more pages
268
+
255
269
  Each instance contains: instance_id, workflow_name, source_hash,
256
270
  owner_service, status, current_activity_id, started_at, updated_at,
257
271
  input_data, source_code, output_data, locked_by, locked_at
@@ -774,11 +774,17 @@ class SQLAlchemyStorage:
774
774
  async def list_instances(
775
775
  self,
776
776
  limit: int = 50,
777
+ page_token: str | None = None,
777
778
  status_filter: str | None = None,
778
- ) -> list[dict[str, Any]]:
779
- """List workflow instances with optional filtering."""
779
+ workflow_name_filter: str | None = None,
780
+ instance_id_filter: str | None = None,
781
+ started_after: datetime | None = None,
782
+ started_before: datetime | None = None,
783
+ ) -> dict[str, Any]:
784
+ """List workflow instances with cursor-based pagination and filtering."""
780
785
  session = self._get_session_for_operation()
781
786
  async with self._session_scope(session) as session:
787
+ # Base query with JOIN
782
788
  stmt = (
783
789
  select(WorkflowInstance, WorkflowDefinition.source_code)
784
790
  .join(
@@ -788,17 +794,105 @@ class SQLAlchemyStorage:
788
794
  WorkflowInstance.source_hash == WorkflowDefinition.source_hash,
789
795
  ),
790
796
  )
791
- .order_by(WorkflowInstance.started_at.desc())
792
- .limit(limit)
797
+ .order_by(
798
+ WorkflowInstance.started_at.desc(),
799
+ WorkflowInstance.instance_id.desc(),
800
+ )
793
801
  )
794
802
 
803
+ # Apply cursor-based pagination (page_token format: "ISO_DATETIME||INSTANCE_ID")
804
+ if page_token:
805
+ # Parse page_token: || separates datetime and instance_id
806
+ separator = "||"
807
+ if separator in page_token:
808
+ cursor_time_str, cursor_id = page_token.split(separator, 1)
809
+ cursor_time = datetime.fromisoformat(cursor_time_str)
810
+ # Use _make_datetime_comparable for SQLite compatibility
811
+ started_at_comparable = self._make_datetime_comparable(
812
+ WorkflowInstance.started_at
813
+ )
814
+ # For SQLite, also wrap the cursor_time in func.datetime()
815
+ cursor_time_comparable: Any
816
+ if self.engine.dialect.name == "sqlite":
817
+ cursor_time_comparable = func.datetime(cursor_time_str)
818
+ else:
819
+ cursor_time_comparable = cursor_time
820
+ # For DESC order, we want rows where (started_at, instance_id) < cursor
821
+ stmt = stmt.where(
822
+ or_(
823
+ started_at_comparable < cursor_time_comparable,
824
+ and_(
825
+ started_at_comparable == cursor_time_comparable,
826
+ WorkflowInstance.instance_id < cursor_id,
827
+ ),
828
+ )
829
+ )
830
+
831
+ # Apply status filter
795
832
  if status_filter:
796
833
  stmt = stmt.where(WorkflowInstance.status == status_filter)
797
834
 
835
+ # Apply workflow name and/or instance ID filter (partial match, case-insensitive)
836
+ # When both filters have the same value (unified search), use OR logic
837
+ if workflow_name_filter and instance_id_filter:
838
+ if workflow_name_filter == instance_id_filter:
839
+ # Unified search: match either workflow name OR instance ID
840
+ stmt = stmt.where(
841
+ or_(
842
+ WorkflowInstance.workflow_name.ilike(f"%{workflow_name_filter}%"),
843
+ WorkflowInstance.instance_id.ilike(f"%{instance_id_filter}%"),
844
+ )
845
+ )
846
+ else:
847
+ # Separate filters: match both (AND logic)
848
+ stmt = stmt.where(
849
+ WorkflowInstance.workflow_name.ilike(f"%{workflow_name_filter}%")
850
+ )
851
+ stmt = stmt.where(WorkflowInstance.instance_id.ilike(f"%{instance_id_filter}%"))
852
+ elif workflow_name_filter:
853
+ stmt = stmt.where(WorkflowInstance.workflow_name.ilike(f"%{workflow_name_filter}%"))
854
+ elif instance_id_filter:
855
+ stmt = stmt.where(WorkflowInstance.instance_id.ilike(f"%{instance_id_filter}%"))
856
+
857
+ # Apply date range filters (use _make_datetime_comparable for SQLite)
858
+ if started_after or started_before:
859
+ started_at_comparable = self._make_datetime_comparable(WorkflowInstance.started_at)
860
+ if started_after:
861
+ started_after_comparable: Any
862
+ if self.engine.dialect.name == "sqlite":
863
+ started_after_comparable = func.datetime(started_after.isoformat())
864
+ else:
865
+ started_after_comparable = started_after
866
+ stmt = stmt.where(started_at_comparable >= started_after_comparable)
867
+ if started_before:
868
+ started_before_comparable: Any
869
+ if self.engine.dialect.name == "sqlite":
870
+ started_before_comparable = func.datetime(started_before.isoformat())
871
+ else:
872
+ started_before_comparable = started_before
873
+ stmt = stmt.where(started_at_comparable <= started_before_comparable)
874
+
875
+ # Fetch limit+1 to determine if there are more pages
876
+ stmt = stmt.limit(limit + 1)
877
+
798
878
  result = await session.execute(stmt)
799
879
  rows = result.all()
800
880
 
801
- return [
881
+ # Determine has_more and next_page_token
882
+ has_more = len(rows) > limit
883
+ if has_more:
884
+ rows = rows[:limit] # Trim to actual limit
885
+
886
+ # Generate next_page_token from last row
887
+ next_page_token: str | None = None
888
+ if has_more and rows:
889
+ last_instance = rows[-1][0]
890
+ # Format: ISO_DATETIME||INSTANCE_ID (using || as separator)
891
+ next_page_token = (
892
+ f"{last_instance.started_at.isoformat()}||{last_instance.instance_id}"
893
+ )
894
+
895
+ instances = [
802
896
  {
803
897
  "instance_id": instance.instance_id,
804
898
  "workflow_name": instance.workflow_name,
@@ -820,6 +914,12 @@ class SQLAlchemyStorage:
820
914
  for instance, source_code in rows
821
915
  ]
822
916
 
917
+ return {
918
+ "instances": instances,
919
+ "next_page_token": next_page_token,
920
+ "has_more": has_more,
921
+ }
922
+
823
923
  # -------------------------------------------------------------------------
824
924
  # Distributed Locking Methods (ALWAYS use separate session/transaction)
825
925
  # -------------------------------------------------------------------------