feat: pagination improvements + tests (#4163)

* 📝 (utils.py): Add Query import and custom_params function to handle custom pagination parameters
📝 (flows.py): Update read_flows function to handle get_all flag and add error handling for missing folders
📝 (folders.py): Add custom_params dependency to read_folder endpoint for custom pagination handling
📝 (model.py): Remove PaginatedFlowResponse class as it is no longer used, add FlowHeader class to represent flow headers without data

✨ (test_database.py): Add pagination support for reading flows with different parameters to improve testing coverage and flexibility.

✅ (test_database.py): add unit tests for reading folders with different scenarios such as pagination, flows, search, and component filter to ensure proper functionality and data retrieval.

* 📝 (utils.py): Add constants MAX_PAGE_SIZE and MIN_PAGE_SIZE for better readability and maintainability
📝 (flows.py): Update get_all parameter default value to True for backward compatibility with frontend
📝 (flows.py): Update error message formatting for clarity
📝 (model.py): Update comments for folder_id, is_component, endpoint_name, and description fields for clarity

* ✨ (test_database.py): Simplify test functions by removing unnecessary parameters and code duplication to improve readability and maintainability.

* formatter

* [autofix.ci] apply automated fixes

* ruff fix

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Cristhian Zanforlin Lousa 2024-10-20 14:04:26 -03:00 • committed by GitHub
commit b969818738
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 356 additions and 44 deletions

View file

@ -4,7 +4,8 @@ import uuid
from datetime import timedelta
from typing import TYPE_CHECKING, Any
from fastapi import HTTPException
from fastapi import HTTPException, Query
from fastapi_pagination import Params
from loguru import logger
from sqlalchemy import delete
@ -23,6 +24,9 @@ if TYPE_CHECKING:
API_WORDS = ["api", "key", "token"]
MAX_PAGE_SIZE = 50
MIN_PAGE_SIZE = 1
def has_api_terms(word: str):
return "api" in word and ("key" in word or ("token" in word and "tokens" not in word))
@ -263,3 +267,12 @@ async def cascade_delete_flow(session: Session, flow: Flow):
except Exception as e:
msg = f"Unable to cascade delete flow: ${flow.id}"
raise RuntimeError(msg, e) from e
def custom_params(
page: int | None = Query(None),
size: int | None = Query(None),
):
if page is None and size is None:
return None
return Params(page=page or MIN_PAGE_SIZE, size=size or MAX_PAGE_SIZE)

View file

@ -132,7 +132,7 @@ def read_flows(
settings_service: SettingsService = Depends(get_settings_service),
remove_example_flows: bool = False,
components_only: bool = False,
get_all: bool = False,
get_all: bool = True,
folder_id: UUID | None = None,
params: Params = Depends(),
header_flows: bool = False,
@ -144,14 +144,18 @@ def read_flows(
session (Session): The database session.
settings_service (SettingsService): The settings service.
components_only (bool, optional): Whether to return only components. Defaults to False.
get_all (bool, optional): Whether to return all flows without pagination. Defaults to False.
get_all (bool, optional): Whether to return all flows without pagination. Defaults to True.
**This field must be True because of backward compatibility with the frontend - Release: 1.0.20**
folder_id (UUID, optional): The folder ID. Defaults to None.
params (Params): Pagination parameters.
remove_example_flows (bool, optional): Whether to remove example flows. Defaults to False.
header_flows (bool, optional): Whether to return only specific headers of the flows. Defaults to False.
Returns:
Union[list[FlowRead], Page[FlowRead]]: A list of flows or a paginated response containing the list of flows.
list[FlowRead] | Page[FlowRead] | list[FlowHeader]
A list of flows or a paginated response containing the list of flows or a list of flow headers.
"""
try:
auth_settings = settings_service.auth_settings
@ -162,6 +166,12 @@ def read_flows(
starter_folder = session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
starter_folder_id = starter_folder.id if starter_folder else None
if not starter_folder and not default_folder:
raise HTTPException(
status_code=404,
detail="Starter folder and default folder not found. Please create a folder and add flows to it.",
)
if not folder_id:
folder_id = default_folder_id
@ -178,9 +188,6 @@ def read_flows(
if components_only:
stmt = stmt.where(Flow.is_component == True) # noqa: E712
if not get_all:
stmt = stmt.where(Flow.folder_id == folder_id)
if get_all:
flows = session.exec(stmt).all()
flows = validate_is_component(flows)
@ -194,6 +201,8 @@ def read_flows(
for flow in flows
]
return flows
stmt = stmt.where(Flow.folder_id == folder_id)
return paginate(session, stmt, params=params)
except Exception as e:

View file

@ -5,7 +5,7 @@ from fastapi_pagination.ext.sqlmodel import paginate
from sqlalchemy import or_, update
from sqlmodel import Session, select
from langflow.api.utils import cascade_delete_flow
from langflow.api.utils import cascade_delete_flow, custom_params
from langflow.api.v1.flows import create_flows
from langflow.api.v1.schemas import FlowListCreate, FlowListReadWithFolderName
from langflow.helpers.flow import generate_unique_flow_name
@ -18,6 +18,7 @@ from langflow.services.database.models.folder.model import (
Folder,
FolderCreate,
FolderRead,
FolderReadWithFlows,
FolderUpdate,
)
from langflow.services.database.models.folder.pagination_model import FolderWithPaginatedFlows
@ -99,13 +100,13 @@ def read_folders(
raise HTTPException(status_code=500, detail=str(e)) from e
@router.get("/{folder_id}", response_model=FolderWithPaginatedFlows, status_code=200)
@router.get("/{folder_id}", response_model=FolderWithPaginatedFlows | FolderReadWithFlows, status_code=200)
def read_folder(
*,
session: Session = Depends(get_session),
folder_id: str,
current_user: User = Depends(get_current_active_user),
params: Params = Depends(),
params: Params | None = Depends(custom_params),
is_component: bool = False,
is_flow: bool = False,
search: str = "",
@ -121,19 +122,25 @@ def read_folder(
raise HTTPException(status_code=404, detail="Folder not found")
try:
stmt = select(Flow).where(Flow.folder_id == folder_id, Flow.user_id == current_user.id)
if params and params.page and params.size:
stmt = select(Flow).where(Flow.folder_id == folder_id)
if Flow.updated_at is not None:
stmt = stmt.order_by(Flow.updated_at.desc()) # type: ignore[attr-defined]
if is_component:
stmt = stmt.where(Flow.is_component == True) # noqa: E712
if is_flow:
stmt = stmt.where(Flow.is_component == False) # noqa: E712
if search:
stmt = stmt.where(Flow.name.like(f"%{search}%")) # type: ignore[attr-defined]
paginated_flows = paginate(session, stmt, params=params)
if Flow.updated_at is not None:
stmt = stmt.order_by(Flow.updated_at.desc()) # type: ignore[attr-defined]
if is_component:
stmt = stmt.where(Flow.is_component == True) # noqa: E712
if is_flow:
stmt = stmt.where(Flow.is_component == False) # noqa: E712
if search:
stmt = stmt.where(Flow.name.like(f"%{search}%")) # type: ignore[attr-defined]
paginated_flows = paginate(session, stmt, params=params)
return FolderWithPaginatedFlows(folder=FolderRead.model_validate(folder), flows=paginated_flows)
flows_from_current_user_in_folder = [flow for flow in folder.flows if flow.user_id == current_user.id]
folder.flows = flows_from_current_user_in_folder
return folder # noqa: TRY300
return FolderWithPaginatedFlows(folder=FolderRead.model_validate(folder), flows=paginated_flows)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e

View file

@ -196,6 +196,24 @@ class FlowRead(FlowBase):
class FlowHeader(BaseModel):
"""Model representing a header for a flow - Without the data.
Attributes:
-----------
id : UUID
Unique identifier for the flow.
name : str
The name of the flow.
folder_id : UUID | None, optional
The ID of the folder containing the flow. None if not associated with a folder.
is_component : bool | None, optional
Flag indicating whether the flow is a component.
endpoint_name : str | None, optional
The name of the endpoint associated with this flow.
description : str | None, optional
A description of the flow.
"""
id: UUID
name: str
folder_id: UUID | None = None
@ -204,13 +222,6 @@ class FlowHeader(BaseModel):
description: str | None = None
class PaginatedFlowResponse(BaseModel):
flows: list[FlowRead]
total: int
page_size: int
page_index: int
class FlowUpdate(SQLModel):
name: str | None = None
description: str | None = None