Skip to content

Support initializing PowerBiClient with an existing access token - #13

Open
wakinniranye31 wants to merge 1 commit into
areed1192:masterfrom
wakinniranye31:support-existing-access-token-8
Open

wakinniranye31 wants to merge 1 commit into
areed1192:masterfrom
wakinniranye31:support-existing-access-token-8

Conversation

@wakinniranye31

Copy link
Copy Markdown

Closes #8.

Problem

There was no way to construct a PowerBiClient from a token acquired outside this library (e.g. via a service principal client-credentials flow, a managed identity, or an app that already handles its own auth). The only path was the interactive/confidential-client MSAL flow baked into PowerBiAuth.

Change

  • PowerBiClient.__init__ and PowerBiAuth.__init__ gain an optional access_token parameter. client_id, client_secret, redirect_uri, and scope are now optional too — either access_token or all four of those must be supplied, enforced with a ValueError at construction time.
  • When access_token is provided, PowerBiAuth skips creating the msal.ConfidentialClientApplication entirely (self.client_app = None), and login() becomes a no-op, since there's nothing left to authenticate.
  • Everything downstream (PowerBiSession.build_headers(), all the service classes) already only reads self.client.access_token, so no other changes were needed to make requests work.

Testing

Added TestPowerBiClientWithAccessToken in tests/test_client.py:

  • constructing with just access_token skips the MSAL app and stores the token directly,
  • calling login() afterward is a no-op and doesn't touch the token,
  • constructing with neither access_token nor full credentials raises ValueError.

Ran the full suite locally: python -m unittest discover -s tests -v → 21 passed (18 existing + 3 new), all green.

Checklist (per CONTRIBUTING.md)

  • All existing tests pass
  • New public parameters have type hints and docstrings
  • CHANGELOG.md updated under [Unreleased]
  • Sample file added (samples/use_client_with_access_token.py)

Note on #7

I initially looked at #7 (add a Date ColumnDataTypes member) intending to fix both in one pass, but the Power BI push-dataset REST API only documents DateTime for temporal columns — there's no separate Date type in the Column object schema, and sibling library pbipy's equivalent enum doesn't have one either. Adding a DATE = "Date" member would let users build a column the service will likely reject, so I left that one alone and focused this PR on #8 instead, which is a real gap. Happy to comment on #7 with these findings if useful.

@areed1192

Copy link
Copy Markdown
Owner

Hey @wakinniranye31, thanks for putting this together, the use case is totally valid and I appreciate the detailed write-up. Before we merge though I'd like to ask for a few changes:

  1. Introduce a TokenProvider interface instead of accepting a raw string. Rather than access_token: str, I'd like to see a small abstract base class with a single get_token() -> str method. The idea is that every time the library needs to make a request it calls token_provider.get_token() rather than reading a stored string. That way the provider can internally decide whether its token is still valid and go fetch a fresh one if not, without the library needing to know anything about how that works. For the simple case where someone just has a plain string already, we'd ship a built-in StaticTokenProvider that just holds the string and always returns it. This is the same pattern Azure SDK and Google Auth use, and it means we're not painting ourselves into a corner on token expiry or rotation down the road.
  2. Update the type hints to use str | None. Since we're targeting Python 3.10 and above, the new optional parameters should be typed as str | None = None rather than str = None.
  3. Add the same ValueError guard to PowerBiAuth. You have the validation in PowerBiClient.__init__ which is great, but if someone constructs PowerBiAuth directly without passing anything they'll get a confusing error somewhere downstream. Would be good to mirror that same check in PowerBiAuth as well.
  4. Clean up the sample file. The hardcoded fake JWT ("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6....") looks close enough to a real token that it might confuse people. Just replace it with something clearly fake like "".

Happy to talk through any of this if it's unclear, especially the TokenProvider piece!

PowerBiClient and PowerBiAuth now accept a token_provider argument
(a TokenProvider instance) instead of a raw access_token string.
client_id/client_secret/redirect_uri/scope become optional and the
whole MSAL/OAuth login() flow is skipped when a provider is supplied
- there's nothing left to authenticate.

Every request now goes through PowerBiAuth.get_token(), which either
delegates to the external TokenProvider or (for the existing MSAL
flow) validates/refreshes as before and returns the current token.
PowerBiSession.build_headers() calls this once per request rather
than reading a cached string, so a custom TokenProvider can rotate
or refresh its token transparently - the library never assumes the
token it saw last time is still valid.

Includes a built-in StaticTokenProvider for the common case of
already having a plain token string with no rotation logic needed.
Same pattern used by the Azure SDK and Google Auth libraries.

Addresses review feedback on this PR:
- Replaced the raw access_token: str parameter with a TokenProvider
  interface (this commit).
- New parameters are typed str | None = None / list[str] | None = None
  (project targets Python 3.10+).
- PowerBiAuth.__init__ now raises the same ValueError guard as
  PowerBiClient.__init__ when constructed directly with neither a
  token_provider nor a full set of credentials.
- samples/use_client_with_access_token.py no longer uses a
  JWT-shaped placeholder; replaced with an unambiguous
  'REPLACE_WITH_YOUR_ACCESS_TOKEN' string.

Rebased onto upstream's token-expiration handling
(f6ca626) and access-token-validation-before-request (8cc7163)
changes, which build_headers()/get_token() now compose with rather
than duplicate.

Tests: added TestPowerBiClientWithTokenProvider (including a
RotatingTokenProvider case proving get_token() is called fresh each
time, not cached) and TestPowerBiAuthWithTokenProvider covering the
direct-construction guard. Full suite: 135 passed.
@wakinniranye31
wakinniranye31 force-pushed the support-existing-access-token-8 branch from 331ca36 to 876a702 Compare July 25, 2026 14:41
@wakinniranye31

Copy link
Copy Markdown
Author

Thanks for the detailed review, @areed1192 — all four addressed:

  1. TokenProvider interface: added powerbi/token_provider.py with a TokenProvider ABC (get_token() -> str) and a built-in StaticTokenProvider for the plain-string case. PowerBiClient/PowerBiAuth now take token_provider: TokenProvider | None = None instead of a raw access_token: str. PowerBiAuth.get_token() delegates to the provider when one is supplied, or falls back to the existing MSAL validate/refresh logic otherwise — and PowerBiSession.build_headers() now calls get_token() once per request instead of reading a cached string, so a custom provider (e.g. one that refreshes on its own schedule) is called fresh every time, never cached by the library. Added a RotatingTokenProvider test case specifically to prove that.

  2. Type hints: the new parameters are now str | None = None / list[str] | None = None.

  3. PowerBiAuth guard: mirrored the same ValueError check in PowerBiAuth.__init__ — constructing it directly with neither token_provider nor a full credential set now fails immediately with the same message PowerBiClient gives, instead of surfacing a confusing error downstream.

  4. Sample file: replaced the JWT-shaped placeholder with "REPLACE_WITH_YOUR_ACCESS_TOKEN".

One extra thing worth flagging: while rebasing onto master I noticed your recent token-expiration-handling and access-token-validation commits (f6ca626, 8cc7163) touch the exact same code path this PR modifies (build_headers() calling into token validation). I folded get_token() into that flow rather than duplicating it — build_headers() now calls self.client.get_token() as its single source of truth for "give me a valid token right now," which covers both the MSAL-refresh case your commits added and the external-provider case this PR adds. Wanted to call that out explicitly in case you'd rather it be structured differently.

Ran the full suite locally after rebasing: 135 passed. Also ran black/flake8 against the changed files — clean, aside from two pre-existing long lines I left untouched since they're outside this PR's diff.

Happy to adjust the TokenProvider shape further if you had something more specific in mind than the Azure-SDK/Google-Auth-style interface I went with.

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.

Power BI Client with existing token

2 participants