fastapi-pagination icon indicating copy to clipboard operation
fastapi-pagination copied to clipboard

Paginate inner model field

Open davidbrochart opened this issue 3 years ago • 9 comments

Great library, thanks!

How would you go about using it on an endpoint which doesn't directly return a sequence, but a model in which a field is the sequence to paginate, e.g.:

class ResponseType(BaseModel):
    other_data: int
    data_to_paginate: Sequence[str]

davidbrochart avatar Sep 06 '22 11:09 davidbrochart

@davidbrochart How JSON response should look like?

smth like this?

{
    "other_data": 1000000,
    "data_to_paginate": ["a", "b", "c"]
}

uriyyo avatar Sep 06 '22 12:09 uriyyo

Yes, exactly.

davidbrochart avatar Sep 06 '22 12:09 davidbrochart

@davidbrochart It looks ugly now(

I will try to provide a better API for such purposes.

from string import ascii_lowercase
from typing import Sequence, Type, TypeVar, Optional, Generic, cast

from fastapi import FastAPI

from fastapi_pagination.bases import AbstractPage, AbstractParams
from fastapi_pagination import add_pagination, paginate, Params

app = FastAPI()

T = TypeVar("T")
P = TypeVar("P", bound=AbstractPage)


class CustomResponse(AbstractPage[T], Generic[T]):
    other_data: Optional[int] = None
    data_to_paginate: Sequence[T]

    __params_type__ = Params

    @classmethod
    def create(
        cls: Type[P],
        items: Sequence[T],
        total: int,
        params: AbstractParams,
    ) -> P:
        return cls(data_to_paginate=items)


DATA = [*ascii_lowercase]


@app.get("/", response_model=CustomResponse[str])
def route():
    response = cast(CustomResponse[str], paginate(DATA))

    response.other_data = 42  # Add additional data to custom response

    return response


add_pagination(app)


if __name__ == '__main__':
    import uvicorn

    uvicorn.run(app)

image

uriyyo avatar Sep 06 '22 12:09 uriyyo

@davidbrochart As for me ideal API should look like this:

from string import ascii_lowercase
from typing import Sequence, Type, TypeVar, Optional, Generic, Dict, Any

from fastapi import FastAPI

from fastapi_pagination.bases import AbstractPage, AbstractParams
from fastapi_pagination import add_pagination, paginate, Params

app = FastAPI()

T = TypeVar("T")
P = TypeVar("P", bound=AbstractPage)


class CustomResponse(AbstractPage[T], Generic[T]):
    other_data: Optional[int] = None
    data_to_paginate: Sequence[T]

    __params_type__ = Params

    @classmethod
    def create(
        cls: Type[P],
        items: Sequence[T],
        total: int,
        params: AbstractParams,
        additional_data: Optional[Dict[str, Any]] = None,
    ) -> P:
        return cls(
            data_to_paginate=items,
            **(additional_data or {}),
        )


DATA = [*ascii_lowercase]


@app.get("/", response_model=CustomResponse[str])
def route():
    return paginate(
        DATA,
        additional_data={
            "other_data": 42,
        },
    )


add_pagination(app)

if __name__ == '__main__':
    import uvicorn

    uvicorn.run(app)

So basically you can add additional fields that will be passed when the response model will be instantiated:

    return paginate(
        DATA,
        additional_data={
            "other_data": 42,
        },
    )

What do you think about such API? Maybe you have other ideas?

uriyyo avatar Sep 06 '22 12:09 uriyyo

My original ResponseType example was intentionally simple:

class ResponseType(BaseModel):
    other_data: int
    data_to_paginate: Sequence[str]

But actually my real use-case is more complex. Ideally this model could be as big as we want, and the data to paginate could even be nested deep inside, so we don't want to add back all the other data around the data to paginate "by hand". It would be great if paginate() could return the same data structure, with the data to paginate replaced by the paginated data. Do you think it is possible?

davidbrochart avatar Sep 06 '22 13:09 davidbrochart

I think yes.

Could you please show how API should look like in your opinion?

uriyyo avatar Sep 06 '22 13:09 uriyyo

I'm not sure, maybe something like this:

class CustomResponse(AbstractPage[T], Generic[T]):
    __root__: ResponseType

    __params_type__ = Params

    @classmethod
    def create(
        cls: Type[P],
        items: Sequence[T],
        total: int,
        params: AbstractParams,
    ) -> P:
        data = get_response()
        data.data_to_paginate = items
        return cls(__root__=data)

But I'm probably missing something.

davidbrochart avatar Sep 06 '22 13:09 davidbrochart

Thanks for your suggestion. I will try to think about a good API for such feature.

Currently, as a workaround you can access paginated items and pass them to your custom schema:

from string import ascii_lowercase
from typing import Sequence, cast

from fastapi import FastAPI, Depends
from pydantic import BaseModel

from fastapi_pagination import add_pagination, paginate, Params, Page

app = FastAPI()


class ResponseType(BaseModel):
    other_data: int
    data_to_paginate: Sequence[str]


DATA = [*ascii_lowercase]


@app.get("/", response_model=ResponseType)
def route(params: Params = Depends()):
    data_to_paginate = cast(Page[str], paginate(DATA, params)).items

    return ResponseType(
        other_data=42,
        data_to_paginate=data_to_paginate,
    )


add_pagination(app)

if __name__ == '__main__':
    import uvicorn

    uvicorn.run(app)

uriyyo avatar Sep 06 '22 14:09 uriyyo

Thanks, but I'm not sure I understand. This doesn't return a Page response, right? For instance, there is no link header.

davidbrochart avatar Sep 06 '22 15:09 davidbrochart

Hi @davidbrochart,

Now it should be easier to implement this feature. Example:

from typing import Any, TypeVar, Generic, Sequence, Type

from fastapi import FastAPI
from pydantic import BaseModel, Field

from fastapi_pagination import paginate, Params
from fastapi_pagination.api import pagination_items, add_pagination
from fastapi_pagination.bases import AbstractPage, AbstractParams

app = FastAPI()
add_pagination(app)

T = TypeVar("T")
C = TypeVar("C")


class PageWithInnerItems(AbstractPage[T], Generic[T]):
    __root__: T
    __params_type__ = Params

    @classmethod
    def create(
            cls: Type[C],
            items: Sequence[T],
            params: AbstractParams,
            **kwargs: Any,
    ) -> C:
        return cls(__root__=kwargs)


class ResponseType(BaseModel):
    other_data: int
    data_to_paginate: Sequence[str] = Field(default_factory=pagination_items)


@app.get(
    "/",
    response_model=PageWithInnerItems[ResponseType],
)
async def route() -> Any:
    data = [*range(100)]

    return paginate(data, additional_data={"other_data": 1000000})


if __name__ == '__main__':
    import uvicorn

    uvicorn.run(app)

uriyyo avatar Nov 27 '22 18:11 uriyyo

Hi @davidbrochart, I am closing this issue, please reopen it if you have more questions.

uriyyo avatar Dec 07 '22 11:12 uriyyo

Thanks a lot for the work, I had to switch to something else but I will probably need these new features in the future.

davidbrochart avatar Dec 07 '22 12:12 davidbrochart