Skip to main content

GraphBridge is a lightweight Microsoft Graph client that uses app-only (Azure AD) authentication and streamlines SharePoint site/list operations: metadata retrieval, feature-based queries, CRUD, upsert, and field key encoding/decoding.

Project description

GraphBridge — Lightweight Microsoft Graph (SharePoint Lists) client

A small Python helper to work with Microsoft Graph—specifically SharePoint Lists—using app-only authentication (Azure AD / Entra ID via ClientSecretCredential). It exposes high-level classes to authenticate (GbAuth), resolve a SharePoint Site (GbSite), and read/write Lists (GbList).


Features

  • 🔐 App-only auth via azure-identity (ClientSecretCredential)
  • 🧭 SharePoint site resolution (siteId) through Graph
  • 📋 List read with automatic pagination
  • ✍️ CRUD helpers: create, update, delete
  • 🔁 Smart upsert/sync via upload(ids, rows, force, delete)
  • 🧩 Client-side filtering with get_items_by_features
  • 🔤 Utilities to map column names with spaces/punctuation (encode_row / decode_row)

Requirements

  • Python ≥ 3.10 (uses | unions and list[dict] style hints)

  • An Entra ID (Azure AD) app with Client ID/Secret and Graph application permissions:

    • Read: Sites.Read.All
    • Write: Sites.ReadWrite.All
  • Dependencies:

    pip install azure-identity requests
    

Admin consent is required for application permissions before calls will succeed.


Installation

If the file lives inside your project:

# grapbridge.py in your project
from grapbridge import GbAuth, GbSite, GbList

(There’s no separate package—treat it as an internal module.)


Quick start

import os
from grapbridge import GbAuth, GbSite, GbList

# 1) Authentication (read from env is recommended)
TENANT_ID = os.environ["AZURE_TENANT_ID"]
CLIENT_ID = os.environ["AZURE_CLIENT_ID"]
CLIENT_SECRET = os.environ["AZURE_CLIENT_SECRET"]

auth = GbAuth(tenant_id=TENANT_ID, client_id=CLIENT_ID, client_secret=CLIENT_SECRET)

# 2) SharePoint Site
site = GbSite(
    gb_auth=auth,
    hostname="contoso.sharepoint.com",
    site_path="/sites/Finance"  # include the leading slash
)

print("Site ID:", site.site_id)

# 3) List
gl = GbList(
    gb_site=site,
    list_name="Project Tracker"
)

# Read everything (fields, IDs, columns)
rows = gl.list_rows          # -> [ {<field>: <value>, ...}, ... ]
ids  = gl.list_ids           # -> ["1","2",...]
cols = gl.list_fields        # -> list of field names

print("Items count:", len(rows))
print("First 3 rows:", rows[:3])

Key concepts

  • Hostname: your SharePoint tenant domain, e.g., contoso.sharepoint.com.
  • Site path: path with a leading slash, e.g., /sites/Finance.
  • List name: the display title of the list, e.g., Project Tracker.
  • Graph base: all calls go to https://graph.microsoft.com/v1.0/... with Authorization: Bearer <token>.

High-level API

GbAuth

App-only auth via Azure AD.

GbAuth(tenant_id: str, client_id: str, client_secret: str)

# Handy properties (lazy, cached)
auth.credential  # ClientSecretCredential
auth.token       # JWT access token string
auth.headers     # {"Authorization": "Bearer <token>"}

May raise

  • ValueError / TypeError for invalid inputs
  • RuntimeError if token acquisition fails

GbSite(GbAuth)

Represents a SharePoint site.

GbSite(hostname: str, site_path: str, gb_auth: GbAuth | None = None, ...)

site.site_url   # Graph site endpoint
site.site_data  # dict (lazy; GET /sites/{hostname}:{site_path})
site.site_id    # site id

You can provide a GbAuth instance (recommended) or pass credentials directly.


GbList(GbSite)

List operations.

GbList(list_name: str, gb_site: GbSite | None = None, ...)

gl.list_url     # Graph list endpoint
gl.list_data    # list metadata (lazy)
gl.list_id      # list id

Reading

gl.list_items       # First page only (expand=fields) – no pagination
gl.list_items_all   # Property with automatic pagination (@odata.nextLink)
gl.list_rows        # [item["fields"] for item in list_items_all]
gl.list_ids         # ["1","2",...]
gl.list_fields      # keys from the first row (or [])

list_items_all uses $top=200 internally and will fetch all pages. It’s a property (no parenthesis) and may perform multiple HTTP requests.

Writing (CRUD)

# CREATE: accepts a dict or a list of dicts (fields)
gl.create(rows={"Title": "New", "Status": "Active"})
gl.create(rows=[{"Title": "A"}, {"Title": "B"}])

# UPDATE: id(s) + dict/list of dicts (1:1)
gl.update(ids="12",           rows={"Status": "Closed"})
gl.update(ids=["12","15"],    rows=[{"Status":"Closed"}, {"Status":"Open"}])

# DELETE: single id or a collection
gl.delete(ids="12")
gl.delete(ids={"12","13","14"})  # set/list/tuple are fine

Return shape (general) All mutating methods return a result object with successes / failures. Example for update:

{
  "successes": [
    {"id": "12", "success": true, "updated_row": {"id": "12", "Title": "X", "...": "..."}}
  ],
  "failures": [
    {"id": "15", "success": false, "error": "Error updating: 404 ..."}
  ]
}

Advanced upsert/sync

gl.upload(
  ids=["1","2","9"],    # logical keys to keep
  rows=[
    {"Title":"Row 1"},
    {"Title":"Row 2"},
    {"Title":"Row 9"}
  ],
  force=False,          # True = replace (delete+create) if ID exists
  delete=True           # True = delete items not in ids
)

Behavior of upload

  • If delete=True: remove existing items whose IDs are not in ids.

  • For each ID in ids:

    • If it exists:

      • force=Truereplace (delete + create). The new item gets a new Graph id; the result includes new_id.
      • force=Falseupdate (PATCH).
    • If it doesn’t existcreate.

Return (shape):

{
  "delete_results": {"successes": [...], "failures": [...] | null},
  "force_results": {
    "replaced": {"successes": [...], "failures": [...]},
    "updated":  {"successes": [...], "failures": [...]},
    "created":  {"successes": [...], "failures": [...]}
  }
}

Column names with spaces/punctuation

Microsoft Graph often encodes field keys like Project_x0020_Name for “Project Name”. GbList provides utilities to convert automatically:

# Encoding map (excerpt)
gl.encode_map   # {' ': '_x0020_', '/': '_x002f_', '(': '_x0028_', ')': '_x0029_', ...}

# Convert human keys -> Graph-encoded keys
row_api = gl.encode_row({"Project Name": "ABC", "Cost (USD)": 123.45})
# -> {"Project_x0020_Name": "ABC", "Cost_x0020__x0028_USD_x0029_": 123.45}

# Decode back (Graph payload -> human keys)
row_human = gl.decode_row(row_api)

Tip: if your column names have spaces/symbols, always pass data to create/update through encode_row(...).


Client-side filtering

# Returns the raw Graph “item” dicts (not just fields) that match
# AT LEAST one block (OR across dicts). Inside each block: AND.
matches = gl.get_items_by_features([
  {"fields": {"Status": "Active"}},  # nested: match inside "fields"
  {"id": "12"}                       # flat: match top-level keys of the item
])

only_fields = [i["fields"] for i in matches]
  • Supports one nesting level, e.g., {"fields": {"Column": "Value"}}.
  • Results are de-duplicated.

Note: iterates list_items_all (with expand=fields). To filter by list fields, use the nested "fields": {...} shape.


Practical snippets

Create multiple items with “human” column names

new_rows_human = [
  {"Project Name": "Mars", "Status": "Active"},
  {"Project Name": "Venus", "Status": "Pending"}
]
gl.create(rows=[gl.encode_row(r) for r in new_rows_human])

Bulk update

ids = ["101", "102", "103"]
patches = [{"Status": "Closed"} for _ in ids]
gl.update(ids=ids, rows=patches)

Keep your source of truth in sync (remove anything else)

source_ids = ["1","2","3"]
source_rows = [{"Title": "A"}, {"Title": "B"}, {"Title": "C"}]
gl.upload(ids=source_ids, rows=source_rows, force=False, delete=True)

Error handling

Methods may raise:

  • ValueError / TypeError for invalid inputs
  • RuntimeError for non-2xx HTTP responses (includes Graph message)

Example:

try:
    gl.update(ids="999", rows={"Status": "Closed"})
except (ValueError, TypeError, RuntimeError) as e:
    print("Operation failed:", e)

Best practices

  • Don’t hard-code secrets—use env vars:

    export AZURE_TENANT_ID=...
    export AZURE_CLIENT_ID=...
    export AZURE_CLIENT_SECRET=...
    
  • Ensure site_path starts with / (e.g., /sites/Finance).

  • For large datasets use gl.list_rows (auto-pagination).

  • For columns with spaces/symbols use encode_row / decode_row.

  • With upload(force=True), remember the Graph ID can change (see new_id).


Graph endpoints used (simplified)

  • Site: GET https://graph.microsoft.com/v1.0/sites/{hostname}:{site_path}

  • List (metadata): GET https://graph.microsoft.com/v1.0/sites/{siteId}/lists/{listNameEncoded}

  • Items:

    • Read: GET .../items?expand=fields
    • Create: POST .../items body: {"fields": {...}}
    • Update fields: PATCH .../items/{id}/fields body: {...}
    • Delete: DELETE .../items/{id}

FAQ

Q: My “Project Name” column doesn’t update. A: It’s likely a key-encoding issue. Use gl.encode_row({"Project Name": "..."}) before create/update.

Q: list_items doesn’t return everything. A: Use gl.list_items_all or gl.list_rows (auto-pagination).

Q: Which permissions do I need? A: App-only: Sites.Read.All to read, Sites.ReadWrite.All to write, and admin consent.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

graphbridge-0.0.4.tar.gz (17.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

graphbridge-0.0.4-py3-none-any.whl (13.2 kB view details)

Uploaded Python 3

File details

Details for the file graphbridge-0.0.4.tar.gz.

File metadata

  • Download URL: graphbridge-0.0.4.tar.gz
  • Upload date:
  • Size: 17.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for graphbridge-0.0.4.tar.gz
Algorithm Hash digest
SHA256 f98790606ed2e77af26ad31266ed8c68a091b3e9337ff166e1eb0a13aed327ef
MD5 727d3423c4d842e6f086208a324ca66c
BLAKE2b-256 67ea5815601745dac783230079bde55e6103449d3e7eebeccd46b13db0ee339d

See more details on using hashes here.

File details

Details for the file graphbridge-0.0.4-py3-none-any.whl.

File metadata

  • Download URL: graphbridge-0.0.4-py3-none-any.whl
  • Upload date:
  • Size: 13.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for graphbridge-0.0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 8d04022c031e75c15e01b6a164cd866300c2ac6097b34d59c2bc2fc4033d6642
MD5 526e24c0da8dff4250777fb2310eb2cc
BLAKE2b-256 6ae3ad386ff0a1a0874575b830a550c7c0b44b4fd8e84ab572e85ec82c5551b0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page