RFC-0010: Microsoft Graph Backend (OneDrive, SharePoint, Teams Files)¶
Status¶
Accepted
Summary¶
Add a GraphBackend that implements the AsyncBackend ABC against the
Microsoft Graph v1.0 API, covering OneDrive (personal and business),
SharePoint document libraries, and Teams files (which are SharePoint
document libraries under the hood). The backend targets a single
drive_id per instance and addresses items by path. It uses httpx as
the HTTP transport, msal for token acquisition, and native async I/O
throughout.
Motivation¶
Microsoft positions Graph as the unified file API across OneDrive, SharePoint, and Teams. A Graph-first backend gives us one auth model, one permission model, one path/item-id scheme, and one module to maintain instead of three. Legacy SharePoint REST is a fallback only if a specific Store operation is genuinely unavailable on Graph — and all Store operations we target are available on Graph.
Users on Microsoft 365 tenants currently have no supported way to plug
remote-store into their existing OneDrive or SharePoint document
libraries without wrapping the raw REST API themselves. Closing that
gap is the point of this RFC.
Goals¶
- One backend covering OneDrive personal, OneDrive for business, SharePoint document libraries, and Teams files.
- Path-based addressing against a single
drive_id. - Both daemon (client-credentials) and interactive (device-code) auth flows in v1.
- Native async implementation per ADR-0012.
- Honest capability declarations. Where Graph's semantics differ from the ideal Store contract, the backend says so rather than pretending.
- Round-trip large-file transfers via resumable upload sessions with retry and resume on chunk failure.
Non-goals¶
- Item-id addressing (
/drive/items/{id}). Deferred from v1. The spec records the deferral explicitly so a future RFC can add it without ambiguity. - Legacy SharePoint REST API. Not targeted. Graph covers the operations we need.
- Mail, calendar, Teams messages, groups, users, or any non-file Graph surface.
- Managed-identity and workload-identity auth. Supported via the token-provider protocol (user supplies their own callable), but not packaged in v1.
- Cross-drive operations. Each
GraphBackendis scoped to one drive. Copying across drives is out of scope for the first cut.
Proposal¶
New backend¶
Module: remote_store.aio.backends._graph (sub-package; async-native)
Name: "graph"
Optional extra: pip install "remote-store[graph]"
Dependencies: the graph extra's pinned set lives in pyproject.toml; see ADR-0021 for the SDK choice.
Spec: sdd/specs/044-graph-backend.md (GR-001 through GR-058,
grouped by topic, with later additions slotted under the section
they belong to rather than appended at the end — IDs are
allocation-order, not section-order)
The backend name is "graph" rather than "onedrive" or "sharepoint"
because a single instance can target any of those services depending on
the drive_id resolved at construction. The name reflects the unified
API, not the user-visible product.
SDK decision¶
Evaluated honestly against the narrow surface we need (a small set of endpoints, async-native, custom polling and upload-session logic):
| Option | Verdict |
|---|---|
httpx + msal direct REST |
Chosen. Narrow dependency footprint, full control of the request layer, async-native fit, reuses the httpx dependency already used by the HTTP backend. |
msgraph-sdk (Kiota) |
Heavyweight transitive deps (Kiota runtime, azure-identity). Generated abstractions add little value over the thin surface we touch. Async-only in a way that complicates the sync-wrapper path. Revisit if the surface ever broadens materially. |
Office365-REST-Python-Client |
Mixes Graph with legacy SharePoint REST patterns; not a clean fit. |
ADR-0021 locks this decision.
Auth model¶
Dual flows in v1, both implemented in a small GraphAuth helper:
- Client-credentials (app-only). Tenant admin consents
Files.ReadWrite.Alland/orSites.ReadWrite.Allapplication permissions on the app registration. Used by daemon services. - Device-code (interactive). The user completes login in a browser. Used by CLIs, notebooks, and demo scripts.
The backend itself depends on a token-provider callable, not on
GraphAuth. Two shapes are supported:
Callable[[], str]Callable[[], Awaitable[str]]
Users with their own auth plumbing (managed identity, corporate broker, custom refresh) supply any callable matching one of those shapes. MSAL token caching uses a persistent backing file; the cache path, mechanism, and override rules are specified by GR-007 in 044-graph-backend.md (single source of truth).
Authorization headers are redacted anywhere request or response metadata surfaces in logs, error messages, or debug dumps (AF-008).
ADR-0022 locks the auth model.
Addressing¶
Single drive_id per backend instance, required at construction.
Identity-stable: it never changes for the lifetime of the backend,
which is important for ext.cache safety (the cache key derives from
backend identity).
An optional base_path constructor parameter (added during
implementation; GR-058) scopes the backend to a drive subfolder:
keys resolve to {base_path}/{key} under the drive root and keys
returned by listing stay base_path-relative, mirroring
SFTPBackend.base_path.
Path-only. Store paths are /-rooted POSIX strings. The backend
translates:
path→/drives/{drive_id}/root:{encoded_path}:for metadata endpoints.- Content reads go through
@microsoft.graph.downloadUrl(see below). - Content writes go through
/content(small) orcreateUploadSession(large).
Path segments are percent-encoded per RFC 3986 before substitution.
Graph is fussy about spaces, #, ?, +, and trailing dots in
segment names; the spec enumerates the encoding rules (GR-010).
GraphUtils.resolve_drive_id helper¶
Users who have a drive URL or site URL rather than a raw drive_id
call a namespaced helper that resolves the three canonical shapes:
- OneDrive personal / for business.
/me/drive→drive.id. TheGraphAuthprincipal determines whose drive. - SharePoint document library.
site_url→site_id→/sites/{site_id}/drives→ pick by name. - Teams channel files. Team/channel →
filesFolder→drive_id.
The helper is exposed as GraphUtils.resolve_drive_id(...) (and the
async counterpart GraphUtils.aresolve_drive_id(...)), mirroring the
SFTPUtils namespace pattern in backends/_sftp.py. It is a
one-shot translation used at application wiring time; the resolved
drive_id is then passed to GraphBackend and stored. The backend
does not repeat the resolution on each call.
Async posture¶
The backend implements AsyncBackend natively (ADR-0012). All I/O
operations are async def, backed by httpx.AsyncClient. The
pattern parallel for this implementation is AsyncAzureBackend
(src/remote_store/aio/backends/_azure.py), not the sync
AzureBackend — sync Azure wraps the sync Azure SDK, which is the
wrong reference shape for a native-async backend.
open_atomic() has no async equivalent on AsyncBackend (ASYNC-062);
GraphBackend therefore implements only write_atomic (GR-040) and
ships no Graph-specific open_atomic surface.
The sync-side story is out of scope for this RFC. Sync callers
reach any AsyncBackend through the existing AsyncBackendSyncAdapter
(ADR-0025, ASYNC-080..093), which also synthesises open_atomic via
spool-and-flush over write_atomic (ASYNC-085). Graph plugs into
that bridge unchanged; no Graph-specific sync code is part of this
proposal.
Async monitor-URL polling¶
Graph's copy operation responds with 202 Accepted and a Location
header pointing to a monitor URL. The client polls that URL until the
operation completes or fails. Move is synchronous in most cases but
can also go async; both reuse the same poller.
The polling logic lives backend-local in
src/remote_store/aio/backends/_graph/monitor.py (a module inside
the Graph sub-package alongside backend.py / http.py /
transfer.py / auth.py), or inline in backend.py while it
stays small. ADR-0023 records the reality check: an earlier draft
proposed a shared backends/_async_monitor.py on the premise that
Azure cross-account copy would reuse it, but AsyncAzureBackend.copy
ships in v0.27.0 without any polling, and there is no second consumer
today. The contract — interval, backoff, timeout, transient-5xx
handling, cancellation, status_parser — is in ADR-0023 and
referenced by the spec (GR-026). If a second backend genuinely needs
the same shape, a follow-up ADR supersedes ADR-0023 and the function
moves to a shared location.
Capability matrix¶
Honest capability declarations are central to this backend's design — several capabilities are
explicitly withheld with rationale (for example, SEEKABLE_READ is withheld because Graph
streams are forward-only; ATOMIC_MOVE because Graph move may be asynchronous). See GR-003
in sdd/specs/044-graph-backend.md for the complete declaration and per-capability rationale.
Error mapping¶
Graph returns structured error bodies with a code field under
error. The mapping uses HTTP status plus code, not string
matching — no fragile string parsing. backend is set to "graph"
on every mapped error. See GR-028 through GR-034 plus GR-045 / GR-046
/ GR-054 / GR-055 in sdd/specs/044-graph-backend.md for the
complete mapping table.
Throttling¶
Graph throttling is mapped to BackendUnavailable with the
Retry-After header value honoured by the in-backend retry loop
before the next attempt (it is not carried on the raised error; see
GR-034 / GR-048). No new RateLimitError is introduced; the existing RetryPolicy
extension handles the backoff. Because httpx has no native retry,
the backend itself honours the full five-field RetryPolicy
(max_attempts, backoff_base, backoff_max, jitter, timeout)
in-backend. The spec 025 retry-policy spec gains RET-015 describing
this mapping.
Resource locked¶
423 Locked / resourceLocked maps to a new ResourceLocked error
type (ADR-0024). Not retried by the default policy; callers decide
their own cadence.
Upload session¶
Files larger than 4 MiB go through a resumable upload session:
POST createUploadSession→ session URL withexpirationDateTime.- Chunks uploaded as
PUT {sessionUrl}withContent-Range. - Chunk size is a multiple of 320 KiB (Graph's documented alignment requirement), capped at a backend-configurable maximum.
- On chunk failure (5xx or network error), retry the same chunk
according to
RetryPolicy. Do not restart the session. - On
401mid-session, re-acquire the token via the provider and retry the chunk. Do not restart the session (session URL is pre-authorised). - On
PUTresponses containingnextExpectedRanges, resume from the server's expected range rather than trusting the client's view. - Session URLs live for a bounded time (Graph documents ~several hours). Session expiry mid-upload surfaces as an error; the retry handling is documented in the spec.
- On caller cancellation or unrecoverable failure, the backend
issues
DELETE {sessionUrl}as a best-effort cleanup.
Range download via downloadUrl¶
Graph returns item metadata containing an @microsoft.graph.downloadUrl
— a short-lived unauthenticated pre-signed URL. The /content
endpoint returns a 302 redirect to this URL, and only the URL
reliably honours the Range header.
An internal _read_bytes(path, start, length) helper (private; not
a public Store method — see GR-015) issues a GET with Range:
bytes=<start>-<end> directly to the download URL (no Authorization
header; the URL is pre-signed). If the URL expires mid-read
(403 / 401 from the pre-signed host), the backend re-fetches the
item metadata to obtain a fresh download URL and resumes the read
from the next unread byte using another Range request. The retry
budget is bounded by RetryPolicy.
Module layout¶
Referenced here for the implementation-phase work — the spec does
not hard-wire file names but does hard-wire responsibilities and
location. GraphBackend is async-native, so it sits under
aio/backends/ (matching aio/backends/_azure.py and
aio/backends/_memory.py), not under the sync backends/ package.
The component count (backend, HTTP wrapper, transfer drivers,
monitor poller, auth helper, utils) makes a sub-package preferable
to the sibling-file form used by smaller aio/backends/ modules.
src/remote_store/aio/backends/_graph/
__init__.py # re-exports GraphBackend, GraphAuth, GraphUtils
backend.py # GraphBackend (AsyncBackend implementation)
http.py # httpx client wrapper, error mapper, pagination
transfer.py # upload-session driver, range-download driver
monitor.py # backend-local monitor-URL poller (ADR-0023)
auth.py # GraphAuth helper
utils.py # GraphUtils namespace (resolve_drive_id, …)
User onboarding¶
Graph onboarding is the single largest UX hurdle for this backend, so the implementation phase ships a dedicated guide. The guide covers:
- OAuth flow decision. Daemon service → client-credentials. Interactive user → device-code. If you are not sure, start with device-code.
- App registration. Walkthrough of registering an application in
Microsoft Entra (formerly Azure AD), configuring redirect URIs for
device-code (
https://login.microsoftonline.com/common/oauth2/nativeclient), creating a client secret for client-credentials, and enabling the right permissions. - Permissions (scopes).
Files.ReadWrite.AllandSites.ReadWrite.Allas the typical baseline. Read-only variants exist for read-only workloads. Application vs delegated permission types. - Admin consent. Client-credentials requires a tenant admin to grant admin consent on the application permissions. Direct link to the admin-consent URL construction.
- Token cache location. Where the cache file lives, how to change it, how to clear it.
- Common errors.
AADSTS65001(consent missing),AADSTS700016(app not found in tenant),AADSTS50076(MFA required), and the 403accessDeniedcase where scopes are correct but Graph denies access because the target drive is outside the principal's permissions. GraphUtils.resolve_drive_idusage. Example snippets for OneDrive, SharePoint, and Teams.
Documentation deliverables (implementation phase)¶
Tracked here so the implementation run does not lose them:
guides/backends/graph.md— primary backend guide (usage, configuration, capability notes).guides/backends/graph-setup.md— initial setup walkthrough modelled ondocs-src/guides/backends/azure-hns-setup.md: Microsoft Entra app registration, redirect URIs, client-secret vs certificate, admin-consent URL construction, commonAADSTS*errors and their fixes. Onboarding is the largest UX hurdle for this backend and merits a dedicated step-by-step doc separate from the usage guide.examples/graph-backend.mdor the corresponding module docstring rendered bygen_pages.py.FEATURES.mdrow for Graph (capabilities, extras, status); the capability columns must match GR-003, includingWRITE_RESULT_NATIVEand the explicit absence ofUSER_METADATA.__all__↔index.mdparity (ID-173): every public symbol added by this work (GraphBackend,GraphAuth,GraphUtils,ResourceLocked) must appear in the rendered API reference. Thecheck_api_docs.pyparity check is a hard CI gate; the implementation PR ships the docs entries in the same commit as the__all__additions.- README backends line and Quick Start snippet (optional).
- Docstrings on
GraphBackend,GraphAuth,GraphUtils(including each@staticmethod), and public helpers.
Test plan¶
This plan is the Graph-specific overlay on the contract-expanding feature Definition of Done (000-process.md § Feature-type Definition of Done — owned by BK-237). The DoD checklist takes precedence on any overlap; the items below are additions, not substitutes.
The plan uses the kind/stage axes from ADR-0028. Graph is an HTTP backend — the full Stage 1 (replay), Stage 3 (live) demotion path applies. Graph has no Stage 2 (no Docker emulator exists for the Microsoft Graph surface), and that gap is explicit: Stage 3 is the authoritative tier; Stage 1 replay is what runs in default CI.
- Stage 1 — unit (
respxdirect).respx-stubbedhttpx.AsyncClientcovering every operation, every error-code mapping, pagination across multiple pages, async copy polling (success + failure), upload-session chunking (small, exact boundary, large, retry, resume, abort), and@microsoft.graph.downloadUrlrange reads (including URL expiry mid-read). These exercise the request-construction layer; they do not exercise the live wire format. - Stage 1 — replay (
graph_replayfixture). Per the HTTP-backend recipe in ADR-0028, agraph_replayStage 1 fixture exercises the realGraphBackendcode path with the HTTP transport stubbed by a recorded cassette. Refresh follows the explicitpytest --stage=3 --recordrecipe (TEST-009 cassette-refresh policy) — CI does not silently re-record. Prerequisite work for the impl PR-set (not free against today's spine): the existing replay machinery is Azure-hardcoded and has to be generalised before Graph can plug in. Concretely: tests/backends/conformance/conftest.py:vcr_cassette_dircurrently returnsCASSETTE_DIR_AZUREunconditionally; TEST-007 mandatescassettes/<backend>/, so a per-backend dispatch is needed (Graph getscassettes/graph/, not "alongside" Azure).- The id-alias map,
_AZURE_REAL_FIXTURE_IDSset, and missing-cassette → skip hook intests/backends/fixtures/registry.pyrecognise onlyazure_*ids;graph_replayneeds the same recognition. - Cassette scrub layer (
tests/backends/fixtures/_cassettes.py) is Azure-specific (x-ms-*, SharedKey, connection-string, Azurite). Graph usesAuthorization: Bearerplus pre-signed@microsoft.graph.downloadUrlhosts — without a Graph-aware scrub list, a bearer token would survive and leak into committed cassettes. This is a security-critical prerequisite, not a polish item. scripts/record_cassettes.py_BACKENDScarries onlyazuretoday; agraphentry is part of the same work.httpxstreaming-replay path is unproven. Azure async needed a bespokeAsyncioRequestsTransportshim because vcrpy 8.1.1's aiohttp stub cannot stream a response body (azure_replay_async.py:9-23); whether vcrpy can capture/replayhttpx.AsyncClient.stream()for GR-012 (chunked reads) and GR-015 (Range-over-downloadUrl) is open.respxhas no record-from-live mode and is unit-only.
If the generic spine + scrub + httpx-streaming-replay package
cannot land alongside the Graph backend, the Stage-1-replay scope
shrinks to the operations that don't require streaming and the
conformance matrix runs against Graph at Stage 3 only — call this
out explicitly in the impl PR.
- Stage 3 — live (graph_live fixture). Gated by the
RS_TEST_LIVE_GRAPH=1 opt-in plus the three credential env
vars GRAPH_CLIENT_ID, GRAPH_TENANT_ID (consumers),
GRAPH_DRIVE_ID. The shipped tier is device-code / consumer — no
client secret; the MSAL token cache the first interactive sign-in
writes keeps later runs non-interactive. Missing opt-in skips
cleanly; opt-in with a missing credential var fails loud. This
mirrors the Azure live-test pattern at
tests/backends/fixtures/fixtures.toml and _live_env.py —
credential presence alone is deliberately not enough to opt into
live runs. Stage 3 is the authoritative tier for any behaviour that
depends on Graph service semantics (chunk alignment, real
throttling with authentic Retry-After, real 423 resourceLocked,
real 507/quota responses); Stage 3 discoveries get cassetted
back into the graph_replay fixture so the next default CI run
catches the regression at Stage 1 cost.
- WriteResult conformance. TestWriteResultConformance in
tests/backends/conformance/test_atomic.py is sync-fixtured today
(via fixture_params(Capability.WRITE), default is_async=False);
it cannot host a native-async backend without modification, and
the async sister in test_async_extended.py covers only WR-013.
Pick one and ship it with the impl PR-set: (a) land an async
TestWriteResultConformance parametrised over async fixtures so
WR-001a / 004 / 005 / 012 / 013 exist for AsyncBackend before
Graph plugs in, or (b) register a sync fixture entry that wraps
GraphBackend in AsyncBackendSyncAdapter to consume the existing
sync suite. Either way, both the small-file and upload-session
paths populate the rich fields from the driveItem response
(GR-018, GR-019).
- USER_METADATA strict-gate test — non-empty metadata= raises
CapabilityNotSupported per WR-010; empty mapping and None are
no-ops (GR-003).
- Capability matrix test asserting that declared capabilities
match the matrix in GR-003 and that unsupported capabilities raise
CapabilityNotSupported where applicable.
- Round-trip test writing a 10 MiB file via upload session,
reading it back via Range to validate byte-equality across the
large-file path. (This is the largest payload the conformance
matrix carries today — ~1 MiB is the prior precedent — and is
deliberately Stage-3-only on the cost side; Stage 1 cassettes
record a single representative round-trip.)
- Dafny-oracle ripple. ADR-0024 ships a Dafny
Error.ResourceLocked(path: string, backend: string) variant
plus dispatch in tests/backends/dafny/_helpers.py::_raise_if_err.
The dafny_oracle and dafny_oracle_async fixtures
(fixtures.toml:83-99) participate in the conformance spine, so
the variant addition and dispatcher update are conformance-fixture-
affecting changes — bundled with the impl PR per ADR-0024
(Consequences: "Ships as a coupled bundle").
- e2e chain. tests/e2e/test_async_streaming_integrity.py
builds its async chain by hand in the test body; there is no
registration seam. Wiring Graph in requires (a) adding two-layer-
gate credential plumbing to tests/e2e/conftest.py (today wires
only Docker-service settings), (b) editing the chain construction
to insert a conditional Graph hop alongside the existing
if _async_azure_available(): branch (e.g.
AsyncMemory(seed) → AsyncAzure → AsyncGraph → SyncWrapped(Local) →
AsyncMemory(sink) when both Azurite and Graph live credentials
are reachable), and (c) handling the LAZY_READ chunk-exemption
if Graph's range-fallback path (GR-015) materialises during the
test. The integrity assertion (SHA-256 identical across hops) and
the lazy-read chunking assertion (count > 1, max_chunk < file_size)
then cover the streaming contract Graph is required to honour. The
Graph hop is conditional on the same two-layer gate as the live
fixture and skips cleanly otherwise. Non-trivial — not a "plug in".
Every spec ID in GR-NNN is traceable to at least one test via
@pytest.mark.spec("GR-NNN") per 000-process.md Rule 2.
Alternatives Considered¶
Option A — msgraph-sdk¶
Rejected. See ADR-0021.
Option B — Legacy SharePoint REST API¶
Rejected. Would require separate code paths for OneDrive vs SharePoint and does not cover Teams files as a first-class target. Graph replaces all of these with a single unified surface.
Option C — One backend per product (OneDrive, SharePoint, Teams)¶
Rejected. The underlying storage model in Graph is identical — they
are all drives identified by a drive_id. Splitting them into
separate backends would triple the maintenance surface for zero
semantic benefit.
Option D — Item-id addressing in v1¶
Rejected for v1. Store paths are the user-facing addressing model across every other backend; introducing a second mode in the same backend adds complexity without an urgent use case. Explicitly deferred in GR-011 so the deferral is tracked.
Impact¶
- Public API. Adds
GraphBackend,GraphAuth, andGraphUtils(namespace class carryingresolve_drive_id, mirroringSFTPUtils) underremote_store.aio.backends._graph, re-exported fromremote_store.aio.backendsbehind a guarded import (the pattern used by the other async-native backends insrc/remote_store/aio/backends/__init__.py). AddsResourceLockedto the top-level error exports. - Backwards compatibility. Purely additive. No existing behaviour
changes except the new
ResourceLockederror class — which is unreachable from backends other than Graph. - Performance. Native async throughout. The sync wrapper pays the event-loop overhead that all async-to-sync bridges pay; this matches ADR-0012's design for async-native backends.
- Testing.
respxbecomes a test-only dependency if not already pulled in by the HTTP backend tests. Integration tests need a real Microsoft 365 tenant; gated by env vars.
Ripple-check¶
Per sdd/CLAUDE-REFERENCE.md, this RFC touches:
- Backends. New
graphbackend.FEATURES.mdrow added in the implementation phase. - Extras. New
graphextra inpyproject.toml, which holds the pinned dependency set; ADR-0021 records the SDK choice. - Spec 005 (errors). Amended at RFC acceptance to add ERR-013
ResourceLocked. The runtime class (remote_store._errors.ResourceLocked) and Dafny variant ship with the backend implementation per the ID-127 bundled-sub-task note insdd/BACKLOG.md. - Spec 025 (retry). Amended at RFC acceptance to add RET-015 Graph retry mapping.
- Spec 045 (
WriteResult). Graph honours WR-001..WR-013 unchanged;WRITE_RESULT_NATIVEis declared (GR-003),source="native"on both small-file and upload-session paths (GR-018, GR-019). No amendment to spec 045 required. - Capabilities.
WRITE_RESULT_NATIVEdeclared;USER_METADATAwithheld with rationale (GR-003). No new capability defined. - ADRs. ADR-0021, ADR-0022, ADR-0023, ADR-0024 all accepted at RFC acceptance.
Open Questions¶
None blocking. Secondary items deferred to post-v1:
- Item-id addressing (tracked in GR-011).
- Managed-identity / workload-identity auth (supported via token-provider protocol today; first-class packaging deferred).
- Surfacing
file.hashesintoext.integrity(plumbed throughFileInfo.extrain v1 per GR-049; wired up when the extension gains a Graph fast-path).
References¶
- Spec:
sdd/specs/044-graph-backend.md - ADRs:
sdd/adrs/0021-graph-sdk-choice.md,sdd/adrs/0022-graph-auth-model.md,sdd/adrs/0023-async-monitor-polling.md,sdd/adrs/0024-resource-locked-error.md - Backend contract:
sdd/specs/003-backend-adapter-contract.md - Error model:
sdd/specs/005-error-model.md - Retry policy:
sdd/specs/025-retry-policy.md - Async API:
sdd/adrs/0012-async-store-backend-api.md - Seekable read:
sdd/adrs/0017-seekable-read-on-store-api.md - Azure backend (pattern reference):
sdd/rfcs/rfc-0001-azure-backend.md,sdd/specs/012-azure-backend.md - Microsoft Graph v1.0: https://learn.microsoft.com/graph/api/overview
- Graph drives and items: https://learn.microsoft.com/graph/api/resources/onedrive
- Upload sessions: https://learn.microsoft.com/graph/api/driveitem-createuploadsession
- MSAL Python: https://learn.microsoft.com/entra/msal/python/