Repository navigation
Conversation
…false success switch_org() treated any status=='OK' response as a completed switch, but the server overloads that status for two different outcomes: a real switch (data has pk/org_name/organization_slug) and an SSO re-authentication challenge (data['idp'] populated) for members of SSO-protected orgs who aren't the owner/admin. The challenge case never actually updates the server-side active org, so treating it as success silently left callers on their previous org while locally claiming to have switched. Now raises with the idp names and a pointer to sso_login(org_name=...) as the alternative, instead of logging a false "Switched to organization" and setting self.session.org_name to a value the server never accepted.
_switch_org wrote _last_switched_org_token unconditionally after the POST, so a 403/500 or an SSO-challenge response (200 + data['idp']) got recorded as a completed switch. That poisoned memo made _maybe_switch_org short-circuit later, skipping switch_org() -- the only implementation that validates the response and raises on an SSO challenge. Now _switch_org returns without writing the memo on a non-2xx status or an idp challenge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This was referenced Aug 18, 2026
…ogic ArrowUploader._switch_org and GraphistryClient.switch_org each POSTed to the same org-switch endpoint with their own copy of the HTTP-status, body-status, and SSO-idp-challenge checks. Extract the shared logic into switch_org_request() in graphistry/utils/requests.py, raising OrgSwitchError / OrgSwitchIdpChallenge. Each caller keeps its own success/failure policy (ArrowUploader logs+swallows, GraphistryClient raises) since that divergence is intentional API contract, not duplication. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TNGYwRNwAFQdYDucbwgtUm
…quests.py Type-hygiene guard flagged missing __init__ return annotations and a bare dict for OrgSwitchIdpChallenge's idp param. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GgDZr3AgU9uKz5EWcVZZVY
vaimdev
force-pushed
the
fix/site-wide-org-dataset-error
branch
from
August 28, 2026 08:21
75c3b4f to
f043c72
Compare
Guard baseline already sat at max cast() count; cast() would've pushed it over. Use a localized type: ignore instead, per DEVELOP.md guidance. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GgDZr3AgU9uKz5EWcVZZVY
vaimdev
force-pushed
the
fix/site-wide-org-dataset-error
branch
from
August 28, 2026 08:50
f043c72 to
4a9d63e
Compare
Adds direct unit tests for graphistry/utils/requests.py's switch_org_request (success, HTTP error, non-OK body w/ detail, unparseable JSON, SSO idp challenge) and ArrowUploader._switch_org (no-op guards, dedup cache, OrgSwitchError/OrgSwitchIdpChallenge/generic-exception swallow paths), raising changed-line coverage on the switch-org refactor. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GgDZr3AgU9uKz5EWcVZZVY
mock.patch("graphistry.utils.requests.requests.post") failed under the
full suite (AttributeError: module 'graphistry' has no attribute 'utils')
because some other test's import juggling leaves the parent package's
'utils' attribute unset. mock.patch.object against the imported module
sidesteps the string-resolution path entirely.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgDZr3AgU9uKz5EWcVZZVY
vaimdev
force-pushed
the
fix/site-wide-org-dataset-error
branch
from
September 2, 2026 09:02
95a9c9a to
510625a
Compare
…ent-only) Cache per-org verified JWTs (exp-aware) in ClientSession so switch_org() can no-op or swap in a cached token instead of re-triggering SSO, without any server-side changes. Multi-org: each org's token cached independently, so switching back to an org verified under an earlier (now-inactive) token still skips re-challenge as long as that token hasn't expired. - client_session.py: _verified_org_tokens dict (org -> (token, exp)), is_org_verified / mark_org_verified / get_verified_token, JWT exp read (no signature verification, token already trusted). - pygraphistry.py: switch_org() fast paths (same-token skip, cached-token swap-in); sso_login()/_handle_auth_url() record verification immediately on token issuance instead of waiting on a follow-up switch call. - arrow_uploader.py: _switch_org() uses the same cache API. - Tests added/migrated in test_pygraphistry.py, test_client_session.py, tests/utils/test_requests.py. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UnsBpuxHHTvC2bAUNaCfED
vaimdev
force-pushed
the
fix/site-wide-org-dataset-error
branch
from
September 2, 2026 09:06
510625a to
f24fb45
Compare
The SSO token endpoint answers with a non-OK body status ("State is
invalid") on every poll until the browser login completes, so the poll
loop's blanket `except Exception` was load-bearing. But it also swallowed
permanent failures -- an org mismatch burned the full sso_timeout and then
reported a misleading "run sso_get_token()" message.
Type the retryable cases instead:
- New SsoPendingException, raised by ArrowUploader.sso_get_token for a
non-OK body status and for an unrecognized body with no 'status' key
(proxy error page, DRF detail). Nothing before the token is parsed can
be classified as permanent.
- The poll loop waits on SsoPendingException uncapped until sso_timeout,
retries RequestException/ValueError up to SSO_POLL_MAX_CONSECUTIVE_ERRORS
(ValueError covers both json and simplejson decode errors), and lets
everything else -- org mismatch, missing JWT in an OK response --
propagate on the first poll.
Also fix switch_org's cached-token fast path clearing _is_authenticated:
it went through the api_token() setter, whose side effect forced a
needless refresh() on the next authenticate(); that refresh minted a token
not verified for the org, reinstating the round trip the fast path exists
to skip. Assign the session field directly, and carry the org grant onto
the refreshed token in refresh() so the fast path stays hit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvLMs6S5UosfKf2YB2sCRS
vaimdev
added a commit
that referenced
this pull request
Sep 15, 2026
B1 follow-up to #1721: refresh() carried the org grant from the pre-refresh token onto the newly reissued one, but the server resets the active org on every reissue -- so the carried grant let _maybe_switch_org() skip the required /switch/ POST, and plot() silently uploaded to the default org while session.org_name still showed the requested one. Deleting the carry-grant lines alone regresses further: switch_org()'s same-user fast path (added by 5cdd1d6 for B3) would then find the stale cache entry left over from before the rotation and swap that superseded, pre-refresh token back in as active -- no real switch request, and refresh()'s own return value reverted to the old token. Fix: ClientSession.forget_verified_org() drops the cache entry for the active org once refresh() confirms the token actually rotated (skipped on a no-op refresh, so it doesn't discard a still-valid verification), forcing a real switch_org() call with the new token. Tests: test_refresh_rotated_token_does_not_skip_switch pins the mocked-switch_org_request behavior; test_refresh_rotated_token_hits_real_switch_endpoint is the HTTP-level version (only requests.post mocked, real ArrowUploader.refresh()/switch_org_request() code paths run) -- both fail against the pre-fix code. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B1Pvaw1aZQvtraEWkEvdyt
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changelog
Summary
After SSO login on a site-wide SSO server, the session could end up bound to the reserved
SITEorg (or to no org at all), and subsequent dataset uploads failed because nothing can be created underSITE. This PR fixes that, and along the way hardens the surrounding org-switch and SSO-polling code that the fix depends on.Changes
1. SSO org resolution (
ArrowUploader.sso_get_token)Four-layer fallback for deciding which org the session is bound to after SSO:
active_organization: use it, but treat the reservedSITEsentinel as unbound (with a warning that the IdP org claim is likely mis-mapped server-side). Caller-suppliedorg_namemust match or be absent.org_name, server silent: preserve caller intent; membership validates lazily on later requests._personal_org_from_api()looks up the user's personal org viaGET /api/v2/my/organizations/filtered onis_personal. Replaces the earlier JWT-username-slugify reconstruction, which couldn't reproduce server-side slug-collision suffixing.2. Unified org-switch request (
graphistry/utils/requests.py)GraphistryClient.switch_organdArrowUploader._switch_orghad duplicate POST-and-validate code. Newswitch_org_request()is the single source of truth and raises:OrgSwitchError— non-2xx HTTP status or non-OK body status.OrgSwitchIdpChallenge— server quirk: for a plain member (not owner/admin) of an org with its own IdP, the switch endpoint returnsstatus: OKwithout actually switching and instead returns an SSO challenge indata['idp']. Previously this was silently treated as success, leaving the caller on their previous org.3. Per-org verified-token cache (
ClientSession)Replaces the single
_last_switched_org_tokentuple with_verified_org_tokens: Dict[org_name -> (token, exp)]plusis_org_verified()/mark_org_verified()/get_verified_token(). Entries self-expire based on the JWTexpclaim (read without signature verification — the token is already trusted, having come from the server via the auth flow just completed)._maybe_switch_orgnever re-triggers an IdP challenge.switch_org()gains two fast paths: already verified under the current token → skip the round trip; a still-unexpired older token was verified for that org → swap it back in as active.refresh()carries the org grant forward to the newly issued token.4. SSO polling robustness (
GraphistryClient.sso_login)New
SsoPendingExceptiondistinguishes "browser login not finished yet" from real failures. The poll loop now:sso_timeout;requests.RequestException/ JSONValueErrorup toSSO_POLL_MAX_CONSECUTIVE_ERRORS(3) consecutive times before giving up;SsoStateInvalidExceptionand other permanent errors immediately.The per-poll log is demoted from
errortodebugso it stops printing once a second during a normal login.5. Mid-switch org tracking
New
session.sso_requested_org_namerecords the org an in-flight SSO login was initiated for, sincesession.org_namestill holds the previous org until the switch is confirmed._sso_get_token()passes it explicitly toArrowUploader.6. Cleanup
Removed dead commented-out code (
sso_state/sso_auth_urlstubs,api_token_refresh_ms, old polling loop), return-type annotations and mypy fixes inutils/requests.py/switch_org.Tests
tests/utils/test_requests.py(new) —switch_org_requestsuccess, HTTP error, non-OK body, and IdP-challenge branches.test_arrow_uploader.py—_switch_orgmemoization/no-memoization on failure,SITEsentinel handling,_personal_org_from_apifallback,SsoPendingExceptionclassification.test_client_session.py— verified-token cache and JWTexpexpiry.test_pygraphistry.py—switch_orgfast paths, poll-loop failure classification, refresh carrying the org grant.🤖 Generated with Claude Code
https://claude.ai/code/session_01TyjxnzaqeB3SeirofUQhSj