Skip to content

Support for cursor based pagination - #161

Draft
funelie wants to merge 19 commits into
mainfrom
cursor-based-pagination
Draft

funelie wants to merge 19 commits into
mainfrom
cursor-based-pagination

Conversation

@funelie

@funelie funelie commented Sep 14, 2026

Copy link
Copy Markdown

Add support for cursor based pagination based on RFC 9865

Resolves #113

@funelie
funelie marked this pull request as draft September 14, 2026 08:03
@funelie
funelie force-pushed the cursor-based-pagination branch from ddb4721 to 75adf75 Compare September 14, 2026 13:23
Comment thread scim2_models/messages/list_response.py Outdated
Comment thread doc/changelog.rst Outdated
"required_error",
"Field 'total_results' is required but value is missing or null",
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to RFC7644 §3.4.2 totalResults is required:

totalResults The total number of results returned by the list or
query operation. The value may be larger than the number of
resources returned, such as when returning a single page (see
Section 3.4.2.4) of results where multiple pages are available.
REQUIRED.

But according to RFC9865 §2 totalResults is optional:

As described in Section 3.4.1 of [RFC7644], service providers should return an accurate value for totalResults, which is the total number of resources for all pages. Service providers implementing cursor pagination that are unable to estimate totalResults MAY choose to omit the totalResults attribute.

We should probably enforce total_results only when nextCursor or previousCursor are present. What do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably enforce total_results only when nextCursor or previousCursor are present. What do you think?

Actually, according to RFC9865 §2, both nextCursor and previousCursor are optional, so they are probably not very reliable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The §1 of RFC9865 says:

These changes are invoked when using the "cursor" parameter when making SCIM search requests using GET or POST methods.

That does not really help since it would mean that we would have to remember what was passed in the request to know how to validate the response 🤔

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the other hand, a bit later we can read:

The cursor value MUST be empty or omitted for the first request of a cursor-paginated query.

So there are situations where cursor can be absent, so that don't seem to be a good discriminant.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I think I know what we can do. We can maybe read the pagination.cursor parameter from the ServiceProviderConfiguration object newly passed in context["scim_spc"].

So if pagination.cursor is True, total_result is optional, and it is mandatory otherwise.

Comment thread doc/changelog.rst Outdated
@funelie
funelie force-pushed the cursor-based-pagination branch from b72cb45 to 3067d54 Compare September 16, 2026 09:40
# Conflicts:
#	doc/changelog.rst
Comment on lines +179 to +191
@model_validator(mode="wrap")
@classmethod
def default_start_index(
cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
) -> Self:
"""Default to start_index 1 if no start_index or cursor is provided."""
obj = handler(value)
assert isinstance(obj, cls)

if obj.cursor is None and obj.start_index is None:
obj.start_index = 1

return obj

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which part of the RFC tells to set 1 as the default value?

@funelie funelie Sep 17, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RFC 7644 does in the definition of the index based pagination here https://www.rfc-editor.org/info/rfc7644/#section-3.4.2.4

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so this part:

   +------------+----------------------------+-------------------------+
   | Parameter  | Description                | Default                 |
   +------------+----------------------------+-------------------------+
   | startIndex | The 1-based index of the   | 1                       |
   |            | first query result.  A     |                         |
   |            | value less than 1 SHALL be |                         |
   |            | interpreted as 1.          |                         |

I am not so sure about this validator though, it injects a startIndex value on every payload, where users might want to explicitly omit it. This is what causes this:

https://github.com/python-scim/scim2-models/pull/161/changes#diff-4c59045d0df695f6b88b4dc47120e79bf9d29f0aa1f769d9590ae348cd04b020R43

I wonder if there is a way that the param takes the value 1 when unset only when it is read, but is not injected in payloads when it is unset. Maybe pydantic has something to offer in that direction?

index: Annotated[bool | None, Mutability.read_only, Required.true] = None
"""A Boolean value specifying whether or not the operation is supported."""

default_pagination_method: Annotated[str | None, Mutability.read_only] = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be a ExtensibleStringEnum instead of str. You can find several examples of usage in the lib.

"schemas": ["urn:ietf:params:scim:api:messages:2.0:SearchRequest"],
"cursor": "",
"count": 10,
"start_index": 1,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be camelCase in payloads (startIndex).

Comment on lines +65 to +66
default_page_size: Annotated[int | None, Mutability.read_only] = None
"""An integer value specifying the default page size."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can take the verbatim description from the RFC here (and same thing for the two other params with their own descriptions) without the OPTIONAL.

Positive integer value specifying the default number of results returned in a page when a count is not specified in the query.

Plus, the RFC enforces that the integer should be positive, pydantic has a native type for that

Comment on lines +193 to +200
@model_validator(mode="after")
def check_cursor_and_index(self, info: ValidationInfo) -> Self:
if self.cursor is not None and self.start_index is not None:
raise PydanticCustomError(
"index_and_cursor_error",
"'cursor' and 'start_index' are mutually exclusive",
)
return self

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the RFC enforces those values as mutually exclusive?
If not, maybe the decision of which method to use should be left to the server implementation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it doesn't explicitly, the RFC just states "When a service provider supports both index-based and cursor-based pagination, clients can use the 'startIndex' or 'cursor' query parameters to request a specific method. "

so if both start index and cursor are sent, it would be up to the server to fallback to its default/preferred method, the same as if none of the two are sent ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think so.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for cursor-based pagination

2 participants