API Reference¶
A practical tour of what the library gives you: the two clients, authentication, the query patterns that make it ergonomic, and the power-user features (proxy, custom SSL, timeouts, concurrent batching, CSV export, throttling).
The two clients¶
ClientContext |
GraphClient |
|
|---|---|---|
| Target API | SharePoint REST API v1 | Microsoft Graph API |
| Use for | SharePoint lists, files, folders, search, site admin, permissions | Outlook, OneDrive, Teams, OneNote, Planner, Users, Groups |
from office365.graph_client import GraphClient
from office365.sharepoint.client_context import ClientContext
graph = GraphClient(tenant="contoso.onmicrosoft.com").with_client_secret("client_id", "client_secret")
ctx = ClientContext("https://contoso.sharepoint.com/sites/team").with_client_secret(
"contoso.onmicrosoft.com", "client_id", "client_secret"
)
Authentication¶
Every modern flow is built in — client secret, certificate, username/password, device code, and interactive (plus on-prem NTLM and cookies for SharePoint):
| Flow | GraphClient |
ClientContext |
|---|---|---|
| Client secret | with_client_secret(client_id, secret) |
with_client_secret(tenant, client_id, secret) |
| Certificate | with_certificate(client_id, thumbprint, key) |
with_client_certificate(tenant, client_id, thumbprint, cert_path) |
| Username / password | with_username_and_password(client_id, user, password) |
with_username_and_password(tenant, client_id, user, password) |
| Device code | with_device_flow(client_id) |
with_device_flow(tenant, client_id, scopes) |
| Interactive | with_token_interactive(client_id) |
with_interactive(tenant, client_id, scopes) |
| On-prem (NTLM) | — | with_user_credentials(username, password) |
| Cookies / custom | with_token_callback(...) |
with_cookies(...), with_access_token(...) |
See the auth examples for full, runnable flows.
Query patterns¶
Filtering¶
OData $filter works identically on both clients:
guests = client.users.filter("userType eq 'Guest'").get().execute_query()
files = ctx.web.lists.get_by_title("Documents").items.filter("FSObjType eq 0").get().execute_query()
For complex SharePoint queries, use a CAML query instead:
from office365.sharepoint.listitems.caml.query import CamlQuery
qry = CamlQuery()
qry.ViewXml = "<View><Query><Where><Eq><FieldRef Name='Status'/><Value Type='Text'>Active</Value></Eq></Where></Query></View>"
items = ctx.web.lists.get_by_title("Tasks").get_items(qry).execute_query()
Paging¶
get_all() follows server-driven paging (@odata.nextLink) so you never write
paging code:
SharePoint collections expose paged(page_size, page_loaded) and
get_all(page_size, page_loaded):
Batching¶
Queue operations and submit them as a single OData $batch request — and run
several batches concurrently to cut wall time on large imports:
target_list = ctx.web.lists.get_by_title("Documents")
for i in range(100):
target_list.add_item({"Title": f"Item {i}"})
ctx.execute_batch(items_per_batch=100) # one $batch request
ctx.execute_batch(concurrency=5) # 5 batches in flight, retries on 429
Graph batches run their sub-requests in parallel by default. When order
matters — read-after-write on the same resource, delete-then-create, parent/child
ordering — pass sequential=True to chain them with Graph's
dependsOn:
Use it only for ordering-dependent side effects (parallel is faster for
independent requests). A batch can't reference another sub-request's response,
so sequential doesn't enable create-then-use-id flows — those need two
round-trips. sequential=True implies concurrency=1 (Graph sequences within a
single $batch).
Throttling & retries¶
Microsoft 365 throttles with HTTP 429 + a Retry-After header; Graph also
reports x-ms-throttle-limit-percentage, x-ms-resource-unit,
x-ms-throttle-scope and x-ms-throttle-information. The library honors
Retry-After and otherwise falls back to exponential backoff with jitter:
result = client.users.top(10).get().execute_query_retry() # per query
client.execute_batch(concurrency=5) # retries throttled sub-requests
Retry any callable with the same policy — call it, or decorate one:
Pace a fleet proactively — the shared rate limiter reads the throttle signals
(Retry-After, Graph's x-ms-throttle-limit-percentage, SharePoint's health
score) on every response and gates the group before it hits 429:
Mark background work as low priority so it's throttled before user-visible calls (Graph doesn't change the limits — low is throttled first, high last):
See Throttling for the signals, the quota reference and the best practices.
Limits¶
Service limits are first-class: declare them on the model with @limit, enforce
them at a call site, and discover them from any resource or tool.
from office365.limits import catalog, limit, limits_of, verify_limits
from office365.sharepoint.thresholds import Limits
@limit(Limits.LIST_VIEW, arg="page_size") # declare + enforce (warn by default)
def get_items(page_size=None): ...
lst.get_items(page_size=6000) # warns; the limit is discoverable
lst.declared_limits() # the limits declared on the class
verify_limits(lst.get_items, page_size=6000).ok # False
catalog("sharepoint") # every registered SharePoint limit
- Mechanics —
office365.limitsre-exportsLimit(a static threshold or a rate quota), the@limitdecorator, and the guardrails (exceeds/warn_if_exceeds/ensure_within/hint). - Catalogs —
sharepoint.thresholds.Limits; Graph quotas are declared on the resource classes (DirectoryObject,CallRecord, …). - Registry — products register their catalogs (
register_catalog);@limiton a class registers it;catalog(product)aggregates them. - Migration — assessment findings carry the authoritative
Limit(AssessmentIssue.limit) alongside the product-specificrisk_code.
See Service limits for the SharePoint catalog.
Error handling¶
Every failed request raises a ClientRequestException, dispatched to the most
specific registered type. Classification is locale-independent: SharePoint
encodes the error as "<HRESULT>, <dotnet-type>" in error.code, so the library
matches the numeric HRESULT and/or the embedded .NET type name — never the
translated message. Graph's symbolic codes (resourceLocked, itemNotFound, …)
are matched too.
from office365.sharepoint.exceptions import (
SharePointException, # catch-all for any Microsoft.SharePoint.* error
SPFileCheckOutException, # HTTP 423 — a releasable check-out
SPDuplicateValuesFoundException,
SPListDataValidationException,
SPQueryThrottledException,
)
from office365.runtime.exceptions import FileLockedException
try:
ctx.web.lists.get_by_title("Tasks").add_item({"Title": "x"}).execute_query()
except SPListDataValidationException as exc:
print(exc.message)
except SharePointException as exc: # any other SharePoint error
print(exc.code, exc.hresult, exc.error_type, exc.request_id)
DuplicatedObjectException(HTTP 400 /ConflictingObjects),ObjectNotFoundException(404 /ResourceNotFound) andFileLockedException(shared coauthoring lock, HTTP 423) are runtime-level and shared with Graph.SharePointExceptionis the catch-all base for product-specific errors; everySP*type subclasses it, soexcept SharePointExceptioncatches them all.- HTTP 423 is disambiguated:
SPFileCheckOutException(an explicit check-out, releasable withFile.checkin()/File.undocheckout()) is typed separately from the sharedFileLockedException(open in Office, not breakable via the API). exc.hresult/exc.error_typeexpose the parsed SharePoint code, whileexc.request_id,exc.server_guidandexc.duration_mscarry correlation and server diagnostics.
Errors with a recovery path expose it in GUIDANCE, appended to str(exc):
SPFileCheckOutException, FileLockedException, and SPQueryThrottledException
(page or index a column — see Large lists).
Power features¶
Proxy, custom SSL, timeouts¶
Configure the transport once — every request (including internal ones like the form digest) inherits it:
ctx = (
ClientContext("https://contoso.sharepoint.com/sites/team")
.with_client_certificate("tenant", "client_id", "thumbprint")
.with_transport(
proxies={"https": "http://proxy:8080"},
verify="/path/to/ca-bundle.pem", # or False for testing only
timeout=30,
)
)
For MSAL authentication traffic to
login.microsoftonline.com, set theHTTPS_PROXYenvironment variable instead — MSAL reads it directly.
CSV export¶
Collections export straight to CSV (or a pandas DataFrame with the pandas
extra):
with open("users.csv", "w", newline="") as f:
client.users.get_all().select(["displayName", "userPrincipalName"]).to_csv(f).execute_query()
DataFrame import / bulk load¶
Every collection exposes the same adapters over one shared projection: export via
to_records()/to_dataframe()/to_csv()/to_ndjson()/to_excel() (or the
generic export_to(..., format=...), e.g. format="json"), and import via the
streaming from_* family (from_records, from_dataframe, from_csv,
from_json, from_ndjson, from_excel, …).
The naming is consistent: from_* returns a streaming ImportResult (bounded,
resumable, idempotent); queue_* (queue_records/queue_dataframe) is the
deferred queue-all path (run with execute_query()).
List.from_dataframe() provisions the typed columns once (inferred from the
dtypes, or from an explicit schema), then queues, executes, and discards each
chunk — so memory stays bounded no matter the row count. Pick the execution
terminal — the driver itself carries no execution knobs:
import pandas as pd
lst = ctx.web.lists.ensure_list("Housing").execute_query()
lst.from_dataframe(df).execute_query() # sequential
lst.from_dataframe(pd.read_csv("housing.csv", chunksize=2000)) \
.execute_batch(items_per_batch=100, concurrency=5) # batched
For full control, iterate the driver and drive execution yourself:
for _ in lst.from_dataframe(pd.read_csv("housing.csv", chunksize=2000)):
ctx.execute_batch(items_per_batch=100, concurrency=5)
concurrency>1 runs batches in parallel with per-sub-request throttling retries
(honoring Retry-After).
For long-running jobs, pass a checkpoint: the committed cursor is persisted
atomically after each chunk, so an interrupted run resumes by skipping the
already-committed chunks (progress continues from that offset). on_error="collect"
records a failing chunk (in ImportStats.errors and the checkpoint's failures)
and continues instead of aborting:
lst.from_dataframe(pd.read_csv("housing.csv", chunksize=2000),
checkpoint="housing.run.json",
on_error="collect") \
.execute_batch(items_per_batch=100, concurrency=5)
checkpoint accepts a path (FileCheckpointStore), an ImportCheckpoint or
None (MemoryCheckpointStore), or any CheckpointStore — pluggable
persistence, MSAL-cache style. The driver exposes ImportResult.resumed_from
(rows already committed) and ImportResult.checkpoint (live cursor), and
ImportStats.resumed_from / stats.summary() report the resumed offset.
The generic entry point is collection.from_records(batches) for any
ClientObjectCollection. See examples/sharepoint/lists/from_dataframe.py
and from_dataframe_large.py, and the examples/entraid DataFrame export for
the Graph side.
Idempotent imports (skip / upsert)¶
Pass a natural key (one or more columns) to make the import duplicate-proof:
a SHA-256 hash of those columns is stored in key_field (created if missing),
the existing keys are loaded once, and a re-run either skips already-present
rows (on_conflict="skip") or updates them (on_conflict="upsert"):
lst.from_dataframe(pd.read_csv("housing.csv", chunksize=2000),
key=["region", "date"], # natural key -> MigrationKey hash
on_conflict="upsert") \
.execute_batch(items_per_batch=100, concurrency=5)
This works alongside checkpoint (resume) — the checkpoint skips committed
chunks for speed, and the key makes the import idempotent on a fresh run, on a
resumed run, and when a chunk is replayed. The existing keys are loaded once per
run (fresh or resumed; key values + Id only, paged). The checkpoint stores
a source signature (format, chunk size, key columns): if it changes between runs
(e.g. a different chunksize), the chunk skip is discarded and the source is
re-scanned — the key keeps it duplicate-free.
Pass enforce_unique=True to mark the key column unique on the list (guards
against a create race), and dry_run=True to preview the create/update/skip plan
without writing anything.
The import/export surface lives on RecordCollection (the base of every typed
EntityCollection), so the same API works on any collection. List mirrors it as
a facade: List.from_dataframe/from_records/from_file (streaming),
List.queue_dataframe/queue_records (deferred), List.export_to/to_dataframe
(record export), and List.export (a .zip package export — per-item JSON +
optional content — distinct from the record export):
collection.export_to(f, format="csv").execute_query() # unified record export
collection.export_to("out.csv", page_size=2000).execute_query() # streamed (bounded)
collection.from_dataframe(df, key=["id"], on_conflict="upsert") # unified streaming import
collection.from_records(batches, checkpoint="run.json") # stream record batches
lst.queue_dataframe(df).execute_query() # deferred (queue-all)
lst.export_to(f, format="csv").execute_query() # list -> records
lst.export(zip_file, include_content=True).execute_query() # list -> .zip package
result = lst.from_dataframe(df, key=["id"])
result.run(concurrency=5) # == execute_batch
report = result.verify(df, key=["id"]) # counts + missing keys
See the data pipeline guide for the full model, formats, idempotency, typed columns, the file bridge and verification.
The format registry (office365.runtime.converters.registry) maps a format name
to its reader/writer, so adding a format is a registration — the named
to_csv/from_dataframe conveniences are thin wrappers over it. Built-in
formats: csv, tsv, json, ndjson, excel, parquet, orc, feather,
dataframe; databases stream through from_sql/from_duckdb (and to_sql/
to_duckdb). Optional dependencies are extras: [pandas], [excel],
[parquet], [sql], [duckdb]. Readers and writers accept a path, a
PathLike or an open file object.
Idempotency¶
Two idempotent layers, one model — a re-run never duplicates:
- Metadata (
ensure_*) — get-or-create a resource (field, list, content type, term, folder, user …), deferred; run withexecute_query(). Passon_conflict="update"to reconcile an existing definition (the metadata analogue of upsert):
lst.ensure_field("Status", FieldType.Text).execute_query() # create if missing
lst.ensure_fields({"Region": FieldType.Text, "Amount": FieldType.Number}) # -> list[Field]
lst.ensure_field("Status", FieldType.Choice, on_conflict="update") # reconcile
All client-side ensure_* share the office365.runtime.queries.get_or_create
primitives (get_or_create/create_or_get), so error classification and the
deferred queueing behave the same everywhere. (Some ensure_* — e.g.
ensure_site_pages_library, ensure_user — are server-side operations and are
already idempotent.)
- Data (
from_records(key=…, on_conflict=…)) — get-or-create/update records by a natural key (see Idempotent imports above).
Distinct from both: ClientObject.ensure_property/ensure_properties is a
client-side lazy load (fetch a property if not already loaded), not a
server-side get-or-create.
Learn more¶
- Products — start with the SharePoint area, then browse the rest from the left navigation.
- Auth examples — all authentication flows.
- README — installation, the full auth matrix, and per-service guides.