From ca3d21bca506ffb7a6628325553caf17b0a85e95 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 23 Jul 2026 23:13:57 +0530 Subject: [PATCH 1/2] fix!: match the backend's standardized single-level pagination shape BREAKING CHANGE: the Authorizer backend removed the PaginatedRequest wrapper type entirely, standardizing GraphQL pagination on PaginationRequest directly everywhere - matching the proto/gRPC surface, which never had a double-wrapper in the first place. - ListClientsRequest/ListTrustedIssuersRequest/ListSAMLServiceProvidersRequest/ ListOrganizationsRequest/ListOrgMembersRequest/ListOrgDomainsRequest: their `pagination` field was typed PaginatedRequest (itself wrapping PaginationRequest) - now PaginationRequest directly. Breaking change for callers constructing these with a nested `pagination=`. - verification_requests/webhooks/email_templates (both AuthorizerAdminClient and AsyncAuthorizerAdminClient): took PaginatedRequest as their whole request parameter and hardcoded `$data: PaginatedRequest` in the GraphQL query strings - now take PaginationRequest directly, `$data: PaginationRequest`. - _proto.py's build_message(): removed a dead-code-now branch that specifically detected and collapsed the double-nested {"pagination": {"pagination": {...}}} shape for REST/gRPC callers - unreachable once no caller can produce that shape anymore. - PaginatedRequest is no longer exported from the package. Verified against a locally built authorizer image running the backend fix: mypy and ruff check clean, all 112 unit tests pass, all 63 live integration tests pass across all three transports (graphql, rest, grpc). --- src/authorizer/__init__.py | 2 -- src/authorizer/_proto.py | 12 ------------ src/authorizer/_queries.py | 6 +++--- src/authorizer/admin_client.py | 6 +++--- src/authorizer/async_admin_client.py | 6 +++--- src/authorizer/types.py | 17 ++++++----------- tests/integration/test_live.py | 18 +++++------------- 7 files changed, 20 insertions(+), 47 deletions(-) diff --git a/src/authorizer/__init__.py b/src/authorizer/__init__.py index 276a8cf..f5c936c 100644 --- a/src/authorizer/__init__.py +++ b/src/authorizer/__init__.py @@ -96,7 +96,6 @@ OrgSAMLConnection, OrgSAMLConnectionRequest, OtpMfaSetupRequest, - PaginatedRequest, Pagination, PaginationRequest, Permission, @@ -262,7 +261,6 @@ "OrgSAMLConnection", "OrgSAMLConnectionRequest", "OtpMfaSetupRequest", - "PaginatedRequest", "Pagination", "PaginationRequest", "Permission", diff --git a/src/authorizer/_proto.py b/src/authorizer/_proto.py index 28d9537..08dbc49 100644 --- a/src/authorizer/_proto.py +++ b/src/authorizer/_proto.py @@ -70,18 +70,6 @@ def _wrap(value: Any, descriptor: Any) -> Any: if field.message_type.full_name == "authorizer.v1.AppData": # SDK contract: free-form maps are flat; wrap into the proto AppData. out[key] = {"value": val} if isinstance(val, dict) else val - elif ( - field.message_type.full_name == "authorizer.v1.PaginationRequest" - and isinstance(val, dict) - and set(val) == {"pagination"} - and isinstance(val["pagination"], dict) - ): - # Several GraphQL ListXRequest inputs type `pagination` as - # PaginatedRequest (itself wrapping PaginationRequest) rather than - # PaginationRequest directly — a pre-existing schema quirk. Proto - # flattens straight to PaginationRequest; collapse the redundant - # wrapper so REST/gRPC callers get the same page/limit as GraphQL. - out[key] = _wrap(val["pagination"], field.message_type) elif isinstance(val, list): out[key] = [_wrap(v, field.message_type) for v in val] else: diff --git a/src/authorizer/_queries.py b/src/authorizer/_queries.py index 97d0878..e006b71 100644 --- a/src/authorizer/_queries.py +++ b/src/authorizer/_queries.py @@ -199,7 +199,7 @@ "{ _delete_user(params: $data) { message } }" ) ADMIN_VERIFICATION_REQUESTS = ( - "query adminVerificationRequests($data: PaginatedRequest) " + "query adminVerificationRequests($data: PaginationRequest) " f"{{ _verification_requests(params: $data) {{ {PAGINATION_FRAGMENT} " f"verification_requests {{ {VERIFICATION_REQUEST_FRAGMENT} }} }} }}" ) @@ -232,7 +232,7 @@ f"{{ _webhook(params: $data) {{ {WEBHOOK_FRAGMENT} }} }}" ) ADMIN_WEBHOOKS = ( - "query adminWebhooks($data: PaginatedRequest) " + "query adminWebhooks($data: PaginationRequest) " f"{{ _webhooks(params: $data) {{ {PAGINATION_FRAGMENT} webhooks {{ {WEBHOOK_FRAGMENT} }} }} }}" ) ADMIN_WEBHOOK_LOGS = ( @@ -257,7 +257,7 @@ "{ _delete_email_template(params: $data) { message } }" ) ADMIN_EMAIL_TEMPLATES = ( - "query adminEmailTemplates($data: PaginatedRequest) " + "query adminEmailTemplates($data: PaginationRequest) " f"{{ _email_templates(params: $data) {{ {PAGINATION_FRAGMENT} " f"email_templates {{ {EMAIL_TEMPLATE_FRAGMENT} }} }} }}" ) diff --git a/src/authorizer/admin_client.py b/src/authorizer/admin_client.py index acfb48c..1fc8eb7 100644 --- a/src/authorizer/admin_client.py +++ b/src/authorizer/admin_client.py @@ -156,7 +156,7 @@ def delete_user(self, req: t.DeleteUserRequest) -> t.GenericResponse: return t.GenericResponse.from_dict(res or {}) def verification_requests( - self, req: t.PaginatedRequest | None = None + self, req: t.PaginationRequest | None = None ) -> t.VerificationRequestsResponse: res = self._invoke( "verification_requests", d.ADMIN["verification_requests"], @@ -195,7 +195,7 @@ def get_webhook(self, req: t.WebhookRequest) -> t.Webhook: res = self._invoke("get_webhook", d.ADMIN["get_webhook"], req.to_dict()) return t.Webhook.from_dict(res or {}) - def webhooks(self, req: t.PaginatedRequest | None = None) -> t.WebhooksResponse: + def webhooks(self, req: t.PaginationRequest | None = None) -> t.WebhooksResponse: res = self._invoke("webhooks", d.ADMIN["webhooks"], req.to_dict() if req else None) return t.WebhooksResponse.from_dict(res or {}) @@ -221,7 +221,7 @@ def delete_email_template(self, req: t.DeleteEmailTemplateRequest) -> t.GenericR res = self._invoke("delete_email_template", d.ADMIN["delete_email_template"], req.to_dict()) return t.GenericResponse.from_dict(res or {}) - def email_templates(self, req: t.PaginatedRequest | None = None) -> t.EmailTemplatesResponse: + def email_templates(self, req: t.PaginationRequest | None = None) -> t.EmailTemplatesResponse: res = self._invoke( "email_templates", d.ADMIN["email_templates"], req.to_dict() if req else None ) diff --git a/src/authorizer/async_admin_client.py b/src/authorizer/async_admin_client.py index bcb5064..f7c65f6 100644 --- a/src/authorizer/async_admin_client.py +++ b/src/authorizer/async_admin_client.py @@ -155,7 +155,7 @@ async def delete_user(self, req: t.DeleteUserRequest) -> t.GenericResponse: return t.GenericResponse.from_dict(res or {}) async def verification_requests( - self, req: t.PaginatedRequest | None = None + self, req: t.PaginationRequest | None = None ) -> t.VerificationRequestsResponse: res = await self._invoke( "verification_requests", d.ADMIN["verification_requests"], @@ -194,7 +194,7 @@ async def get_webhook(self, req: t.WebhookRequest) -> t.Webhook: res = await self._invoke("get_webhook", d.ADMIN["get_webhook"], req.to_dict()) return t.Webhook.from_dict(res or {}) - async def webhooks(self, req: t.PaginatedRequest | None = None) -> t.WebhooksResponse: + async def webhooks(self, req: t.PaginationRequest | None = None) -> t.WebhooksResponse: res = await self._invoke("webhooks", d.ADMIN["webhooks"], req.to_dict() if req else None) return t.WebhooksResponse.from_dict(res or {}) @@ -229,7 +229,7 @@ async def delete_email_template(self, req: t.DeleteEmailTemplateRequest) -> t.Ge return t.GenericResponse.from_dict(res or {}) async def email_templates( - self, req: t.PaginatedRequest | None = None + self, req: t.PaginationRequest | None = None ) -> t.EmailTemplatesResponse: res = await self._invoke( "email_templates", d.ADMIN["email_templates"], req.to_dict() if req else None diff --git a/src/authorizer/types.py b/src/authorizer/types.py index 01fe8f6..0ca2cc2 100644 --- a/src/authorizer/types.py +++ b/src/authorizer/types.py @@ -611,11 +611,6 @@ class PaginationRequest(_Request): page_token: str | None = None -@dataclass -class PaginatedRequest(_Request): - pagination: PaginationRequest | None = None - - @dataclass class ListUsersRequest(_Request): """Request for :meth:`AuthorizerAdminClient.users`. @@ -818,7 +813,7 @@ class ClientRequest(_Request): @dataclass class ListClientsRequest(_Request): - pagination: PaginatedRequest | None = None + pagination: PaginationRequest | None = None # -- trusted issuers --------------------------------------------------------- # @@ -859,7 +854,7 @@ class TrustedIssuerRequest(_Request): @dataclass class ListTrustedIssuersRequest(_Request): service_account_id: str | None = None - pagination: PaginatedRequest | None = None + pagination: PaginationRequest | None = None # -- organizations ------------------------------------------------------------ # @@ -885,7 +880,7 @@ class OrganizationRequest(_Request): @dataclass class ListOrganizationsRequest(_Request): - pagination: PaginatedRequest | None = None + pagination: PaginationRequest | None = None @dataclass @@ -905,7 +900,7 @@ class RemoveOrgMemberRequest(_Request): @dataclass class ListOrgMembersRequest(_Request): org_id: str - pagination: PaginatedRequest | None = None + pagination: PaginationRequest | None = None # -- org SSO connections ------------------------------------------------------ # @@ -1035,7 +1030,7 @@ class SAMLServiceProviderRequest(_Request): @dataclass class ListSAMLServiceProvidersRequest(_Request): org_id: str - pagination: PaginatedRequest | None = None + pagination: PaginationRequest | None = None @dataclass @@ -1088,7 +1083,7 @@ class AddVerifiedOrgDomainRequest(_Request): @dataclass class ListOrgDomainsRequest(_Request): org_id: str - pagination: PaginatedRequest | None = None + pagination: PaginationRequest | None = None @dataclass diff --git a/tests/integration/test_live.py b/tests/integration/test_live.py index b103cd9..3939c19 100644 --- a/tests/integration/test_live.py +++ b/tests/integration/test_live.py @@ -296,7 +296,7 @@ async def test_async_login_and_update_profile(protocol: str) -> None: # Admin client — available over all protocols (sync) # --------------------------------------------------------------------------- # def test_admin_users(admin: AuthorizerAdminClient) -> None: - page = admin.users(t.PaginatedRequest(pagination=t.PaginationRequest(page=1, limit=10))) + page = admin.users(t.ListUsersRequest(pagination=t.PaginationRequest(page=1, limit=10))) assert page.pagination.page == 1 assert page.pagination.limit == 10 # int64 string coerced to int over REST assert isinstance(page.pagination.limit, int) @@ -304,9 +304,7 @@ def test_admin_users(admin: AuthorizerAdminClient) -> None: def test_admin_verification_requests(admin: AuthorizerAdminClient) -> None: - res = admin.verification_requests( - t.PaginatedRequest(pagination=t.PaginationRequest(page=1, limit=10)) - ) + res = admin.verification_requests(t.PaginationRequest(page=1, limit=10)) assert isinstance(res.verification_requests, list) @@ -331,17 +329,13 @@ def test_admin_webhook_lifecycle(admin: AuthorizerAdminClient, protocol: str) -> created_id = "" try: # drop any leftover webhook for this event from an interrupted prior run - for w in admin.webhooks( - t.PaginatedRequest(pagination=t.PaginationRequest(page=1, limit=100)) - ).webhooks: + for w in admin.webhooks(t.PaginationRequest(page=1, limit=100)).webhooks: if w.event_name == event and w.id: admin.delete_webhook(t.WebhookRequest(id=w.id)) admin.add_webhook( t.AddWebhookRequest(event_name=event, endpoint=endpoint, enabled=False) ) - page = admin.webhooks( - t.PaginatedRequest(pagination=t.PaginationRequest(page=1, limit=100)) - ) + page = admin.webhooks(t.PaginationRequest(page=1, limit=100)) assert isinstance(page.webhooks, list) match = next((w for w in page.webhooks if w.endpoint == endpoint), None) assert match is not None and match.id @@ -397,9 +391,7 @@ def test_admin_meta_rest_grpc(protocol: str) -> None: async def test_async_admin_users(protocol: str) -> None: c = AsyncAuthorizerAdminClient(URL, ADMIN_SECRET, protocol=protocol, grpc_endpoint=GRPC) try: - page = await c.users( - t.PaginatedRequest(pagination=t.PaginationRequest(page=1, limit=5)) - ) + page = await c.users(t.ListUsersRequest(pagination=t.PaginationRequest(page=1, limit=5))) assert page.pagination.page == 1 assert isinstance(page.pagination.limit, int) finally: From 1bfc6527c6f8016c7dc13080a9f6e1a3eaa2e487 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Fri, 24 Jul 2026 04:17:12 +0530 Subject: [PATCH 2/2] chore(ci): bump test image to 2.4.0-rc.9 Picks up the backend's pagination schema fix this PR targets. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3269eb8..f1c3214 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: run: | docker run -d --name authorizer \ -p 8080:8080 -p 9091:9091 \ - quay.io/authorizer/authorizer:2.4.0-rc.7 \ + quay.io/authorizer/authorizer:2.4.0-rc.9 \ --database-type=sqlite --database-url=test.db \ --jwt-type=HS256 --jwt-secret=test \ --admin-secret=admin --client-id=ci-client --client-secret=secret \