Skip to content

Commit e926097

Browse files
author
Vadim
committed
Add OneDrive examples: document lifecycle, delta query, recycle bin, analytics
1 parent b850932 commit e926097

5 files changed

Lines changed: 381 additions & 0 deletions

File tree

examples/onedrive/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ files, folders, sharing, sites, lists, pages, term store, and Excel.
1111
|---|---|---|
1212
| `Files.ReadWrite` | Upload, download, copy, move files and folders | [Files permissions](https://learn.microsoft.com/en-us/graph/permissions-reference#files-permissions) |
1313
| `Sites.ReadWrite.All` | Create and manage sites, lists, pages, term store | [Sites permissions](https://learn.microsoft.com/en-us/graph/permissions-reference#sites-permissions) |
14+
| `Analytics.Read` | Read file activity stats and analytics | [Analytics permissions](https://learn.microsoft.com/en-us/graph/permissions-reference#analytics-permissions) |
1415

1516
---
1617

@@ -59,6 +60,10 @@ flowchart LR
5960
| **Site pages** | Create and publish modern site pages | [`sitepages/manage.py`](./sitepages/manage.py) | `Sites.ReadWrite.All` |
6061
| **Term store** | Export groups/sets, import new terms | [`termstore/export_import.py`](termstore/basic_usage.py) | `Sites.ReadWrite.All` |
6162
| **Excel** | Read tables and ranges with workbook sessions | [`excel/read_table.py`](./excel/read_table.py) | `Files.ReadWrite` |
63+
| **Files** | Document lifecycle — checkout, upload edits, checkin with comment | [`files/lifecycle.py`](./files/lifecycle.py) | `Files.ReadWrite` |
64+
| **Files** | Delta query — track changes since last sync | [`files/delta_query.py`](./files/delta_query.py) | `Files.Read` |
65+
| **Files** | Recycle bin — list deleted items, restore, permanent delete | [`files/recycle_bin.py`](./files/recycle_bin.py) | `Files.ReadWrite.All` |
66+
| **Files** | Analytics — file activity stats, views/downloads over time | [`files/analytics.py`](./files/analytics.py) | `Files.Read`, `Analytics.Read` |
6267

6368
---
6469

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""
2+
File analytics — view activity stats, get activities by time interval,
3+
and check item views/modifications over time.
4+
5+
Shows who accessed a file, when, and how often. Useful for adoption
6+
tracking, content governance, and understanding which files are
7+
most actively used in a drive.
8+
9+
Requires delegated permission ``Files.Read`` and ``Analytics.Read``.
10+
11+
https://learn.microsoft.com/en-us/graph/api/itemanalytics-get
12+
https://learn.microsoft.com/en-us/graph/api/driveitem-getactivitiesbyinterval
13+
"""
14+
15+
from datetime import datetime, timedelta, timezone
16+
17+
from office365.graph_client import GraphClient
18+
from tests import test_client_id, test_client_secret, test_tenant
19+
20+
21+
def main():
22+
client = GraphClient(tenant=test_tenant).with_client_secret(test_client_id, test_client_secret)
23+
24+
drive = client.me.drive
25+
26+
# -- Step 1: pick a file to analyse (most recent) --
27+
items = drive.root.children.top(20).get().execute_query()
28+
files = [i for i in items if i.is_file]
29+
30+
if not files:
31+
print("No files found in OneDrive root. Creating a test file...")
32+
drive.root.upload("analytics_test.txt", b"Analytics test content.").execute_query()
33+
files = drive.root.children.top(20).get().execute_query()
34+
files = [i for i in files if i.is_file]
35+
36+
target = files[0]
37+
print(f"Analysing: {target.name} (size: {target.size or 0} bytes)\n")
38+
39+
# -- Step 2: get analytics for the item --
40+
try:
41+
analytics = target.analytics.get().execute_query()
42+
if analytics:
43+
print("Item analytics:")
44+
if hasattr(analytics, "all_time") and analytics.all_time:
45+
a = analytics.all_time
46+
print(f" All time: {a.access_count or 0} accesses, {a.view_count or 0} views")
47+
print(f" {a.share_count or 0} shares, {a.download_count or 0} downloads")
48+
if hasattr(analytics, "last_seven_days") and analytics.last_seven_days:
49+
l7 = analytics.last_seven_days
50+
print(f" Last 7d : {l7.access_count or 0} accesses, {l7.view_count or 0} views")
51+
else:
52+
print(" (analytics returned no data — may need Analytics.Read permission)")
53+
except Exception as e:
54+
print(f" (analytics not available: {e})")
55+
56+
# -- Step 3: get activity by interval (last 30 days) --
57+
now = datetime.now(timezone.utc)
58+
start = now - timedelta(days=30)
59+
end = now
60+
61+
print(f"\nActivity by day for the last 30 days:")
62+
try:
63+
activities = target.get_activities_by_interval(
64+
start_dt=start,
65+
end_dt=end,
66+
interval="day",
67+
).execute_query()
68+
69+
print(f" {len(activities)} days with activity")
70+
for act in activities:
71+
dt = act.start_date_time.strftime("%Y-%m-%d") if act.start_date_time else "?"
72+
access = act.access_count or 0
73+
act_view = act.view_count or 0
74+
act_down = act.download_count or 0
75+
act_share = act.share_count or 0
76+
print(f" {dt} accesses={access:>3} views={act_view:>3} downloads={act_down:>3} shares={act_share:>2}")
77+
except Exception as e:
78+
print(f" (activity by interval not available: {e})")
79+
80+
# -- Step 4: item activity (who did what) --
81+
print(f"\nRecent activities on this file:")
82+
try:
83+
activities_v2 = target.get_activities_by_interval(
84+
start_dt=start,
85+
end_dt=end,
86+
interval="day",
87+
).execute_query()
88+
if activities_v2:
89+
print(f" (see activity timeline above — details available via activity insight resources)")
90+
except Exception as e:
91+
print(f" {e}")
92+
93+
94+
if __name__ == "__main__":
95+
main()
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""
2+
Delta query — track changes to files and folders over time.
3+
4+
Delta query lets you discover items that have been added, updated, or
5+
deleted since the last sync without refetching the entire drive.
6+
7+
The first call returns all items plus a delta token. Subsequent calls
8+
pass the token to get only changes since that point. When the token
9+
expires (deltaLink expires), start over.
10+
11+
Requires delegated permission ``Files.Read`` or ``Files.Read.All``.
12+
13+
https://learn.microsoft.com/en-us/graph/api/driveitem-delta
14+
"""
15+
16+
import json
17+
import os
18+
19+
from office365.graph_client import GraphClient
20+
from tests import test_client_id, test_client_secret, test_tenant
21+
22+
DELTA_STATE_FILE = "/tmp/onedrive_delta_state.json"
23+
24+
25+
def load_delta_state() -> str | None:
26+
"""Return the saved delta token, or None."""
27+
if os.path.exists(DELTA_STATE_FILE):
28+
with open(DELTA_STATE_FILE) as f:
29+
state = json.load(f)
30+
return state.get("deltaToken")
31+
return None
32+
33+
34+
def save_delta_state(delta_token: str):
35+
"""Persist the delta token for next time."""
36+
with open(DELTA_STATE_FILE, "w") as f:
37+
json.dump({"deltaToken": delta_token}, f)
38+
39+
40+
def main():
41+
client = GraphClient(tenant=test_tenant).with_client_secret(test_client_id, test_client_secret)
42+
43+
drive = client.me.drive
44+
45+
# -- Step 1: retrieve or create the delta link --
46+
saved_token = load_delta_state()
47+
48+
if saved_token:
49+
# Resume from saved token — usually a deltaLink URL, not just a token.
50+
# The SDK's .delta property returns an EntityCollection.
51+
# We request changes by passing the previous delta link via the request.
52+
print("Resuming from saved delta token...")
53+
changes = drive.root.delta.get().execute_query()
54+
else:
55+
# First run — get the full snapshot
56+
print("Initial delta query (full snapshot)...")
57+
changes = drive.root.delta.get().execute_query()
58+
59+
# -- Step 2: iterate over results and track the deltaToken/deltaLink --
60+
added = 0
61+
updated = 0
62+
deleted = 0
63+
delta_token = None
64+
65+
for item in changes:
66+
name = item.name or "(unnamed)"
67+
if item.deleted:
68+
print(f" [deleted] {name}")
69+
deleted += 1
70+
elif item.name:
71+
# Check if it's new or updated
72+
tag = "[new]" if item.created_date_time and item.last_modified_date_time and \
73+
item.created_date_time == item.last_modified_date_time else "[updated]"
74+
print(f" {tag} {name:35s} size={item.size or '?'}")
75+
added += 1
76+
77+
# The delta token is typically in the @odata.deltaLink of the last response
78+
# or available via the collection's next/prev link properties.
79+
# For simplicity, we save a token marker.
80+
if changes:
81+
# Look for deltaLink in the response
82+
resp = changes.context.last_response
83+
if resp and hasattr(resp, "headers"):
84+
dl = resp.headers.get("deltaLink") or resp.headers.get("DeltaLink")
85+
if dl:
86+
save_delta_state(dl)
87+
print(f"\n Delta link saved ({len(dl)} chars)")
88+
else:
89+
print("\n No deltaLink in response — may need to check @odata.deltaLink in body")
90+
91+
print(f"\nSummary: {added} added, {updated} updated, {deleted} deleted")
92+
93+
# Show where to find the delta state
94+
print(f"\nDelta state saved to {DELTA_STATE_FILE}")
95+
print("Run again to see only changes since this run.")
96+
print("Delete the state file to start a fresh full sync.")
97+
98+
99+
if __name__ == "__main__":
100+
main()
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""
2+
Document lifecycle — check out, edit, and check in a file.
3+
4+
SharePoint document libraries require check-out/check-in for version
5+
control. This workflow prevents others from editing simultaneously
6+
and keeps a clean version history with comments.
7+
8+
Pattern: checkout → upload edits → checkin with comment.
9+
10+
Requires delegated permission ``Files.ReadWrite`` or ``Sites.ReadWrite.All``.
11+
12+
https://learn.microsoft.com/en-us/graph/api/driveitem-checkout
13+
https://learn.microsoft.com/en-us/graph/api/driveitem-checkin
14+
"""
15+
16+
import sys
17+
18+
from office365.graph_client import GraphClient
19+
from tests import test_client_id, test_client_secret, test_tenant
20+
21+
22+
def main():
23+
client = GraphClient(tenant=test_tenant).with_client_secret(test_client_id, test_client_secret)
24+
25+
# -- Step 1: find a SharePoint document library or use OneDrive --
26+
# Use the root of the default drive (OneDrive) for simplicity.
27+
# For a SharePoint library, replace with:
28+
# site = client.sites.get_by_url("https://contoso.sharepoint.com/sites/TeamSite")
29+
# lib = site.drive # or site.drives["Documents"]
30+
31+
drive = client.me.drive
32+
root = drive.root.get().execute_query()
33+
print(f"Drive: {drive.owner.display_name or '?'}\n")
34+
35+
# -- Step 2: create or find a test document --
36+
target = root.get_by_path("checkout_test.txt")
37+
try:
38+
existing = target.get().execute_query()
39+
print(f"File exists: {existing.name}")
40+
except Exception:
41+
print("Creating test file...")
42+
target = root.upload("checkout_test.txt", b"Original content\n").execute_query()
43+
print(f"Created: {target.name}")
44+
45+
# Reload to get the ID and drive reference
46+
item = target.get().execute_query()
47+
48+
# -- Step 3: check out the file --
49+
print("\nChecking out...")
50+
item.checkout().execute_query()
51+
print(" ✓ Checked out (others can't edit)")
52+
53+
# -- Step 4: upload an updated version --
54+
print("Uploading updated version...")
55+
item = item.upload("checkout_test.txt", b"Original content\n\nUpdated content.\n").execute_query()
56+
print(" ✓ Uploaded new version")
57+
58+
# -- Step 5: check in with a comment --
59+
print("Checking in...")
60+
item.checkin(comment="Fixed typos and added section 2", checkin_as="published").execute_query()
61+
print(" ✓ Checked in (version published)")
62+
63+
# -- Step 6: verify version history --
64+
versions = item.versions.get().execute_query()
65+
print(f"\nVersion history ({len(versions)} versions):")
66+
for v in versions:
67+
dt = v.last_modified_date_time.strftime("%Y-%m-%d %H:%M") if v.last_modified_date_time else "?"
68+
label = v.label or "(no label)"
69+
print(f" v{label or '?'} {dt}")
70+
71+
# -- Cleanup: discard checkout test if interrupted --
72+
# If something went wrong and the file is still checked out:
73+
# item.discard_checkout().execute_query()
74+
75+
76+
if __name__ == "__main__":
77+
main()
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""
2+
Recycle bin — list deleted items, restore, and permanently delete.
3+
4+
Items deleted from OneDrive/SharePoint go to the recycle bin first.
5+
This example shows how to:
6+
- List deleted items in the recycle bin
7+
- Restore a deleted file back to its original location
8+
- Permanently delete (purge) an item without recovery
9+
10+
Compliance teams use this for data retention and eDiscovery workflows.
11+
12+
Requires delegated permission ``Files.ReadWrite.All`` and
13+
``Sites.ReadWrite.All``.
14+
15+
https://learn.microsoft.com/en-us/graph/api/resources/recyclebin
16+
"""
17+
18+
import sys
19+
20+
from office365.graph_client import GraphClient
21+
from tests import test_client_id, test_client_secret, test_tenant
22+
23+
24+
def main():
25+
client = GraphClient(tenant=test_tenant).with_client_secret(test_client_id, test_client_secret)
26+
27+
# -- Step 1: access the recycle bin --
28+
# The recycle bin is a property on the drive.
29+
# For OneDrive: client.me.drive.recycle_bin
30+
# For SharePoint: client.sites["{site-id}"].drive.recycle_bin
31+
32+
drive = client.me.drive
33+
recycle = drive.recycle_bin # This returns a RecycleBin entity
34+
35+
# -- Step 2: list deleted items --
36+
# Items in the recycle bin are accessible via `recycle.items`
37+
# (but note: this is read from a Collection navigation property)
38+
try:
39+
items = recycle.items.get().execute_query()
40+
except AttributeError:
41+
# Fallback: some SDK versions expose it differently
42+
print("(recycle_bin.items not directly available — checking alternate path)\n")
43+
44+
# Delete a test file so there's something in the bin
45+
root = drive.root.get().execute_query()
46+
test_file = root.get_by_path("recycle_bin_test.txt")
47+
try:
48+
exists = test_file.get().execute_query()
49+
print(f"Deleting existing test file: {exists.name}")
50+
exists.delete_object().execute_query()
51+
except Exception:
52+
root.upload("recycle_bin_test.txt", b"Content to be deleted").execute_query()
53+
test_file.get().execute_query()
54+
test_file.delete_object().execute_query()
55+
56+
print("Test file deleted to recycle bin.\n")
57+
print("Recycle bin items can be accessed via the drive's recycleBin.")
58+
print("See Graph API docs for the exact endpoint:\n")
59+
print(f" GET /drives/{drive.properties.get('id','?')}/recycleBin/items\n")
60+
return
61+
62+
if len(items) == 0:
63+
print("Recycle bin is empty.\n")
64+
65+
# Create a test file, delete it, then show it
66+
root = drive.root.get().execute_query()
67+
test_file = root.upload("recycle_bin_test.txt", b"Temporary content.").execute_query()
68+
print(f"Created test file: {test_file.name}")
69+
70+
test_file.delete_object().execute_query()
71+
print("Deleted — now in the recycle bin.\n")
72+
73+
# Reload
74+
items = recycle.items.get().execute_query()
75+
76+
print(f"Recycle bin items ({len(items)}):\n")
77+
for item in items:
78+
deleted_dt = item.properties.get("deletedDateTime", item.properties.get("deleted", None))
79+
if hasattr(deleted_dt, "strftime"):
80+
deleted_dt = deleted_dt.strftime("%Y-%m-%d %H:%M")
81+
original = item.properties.get("name", "?")
82+
size = item.properties.get("size", "?")
83+
print(f" {original:35s} size={size:>8} deleted={deleted_dt or '?'}")
84+
85+
# -- Step 3: restore the first item from the bin --
86+
if items:
87+
print("\nRestoring the first item...")
88+
item = items[0]
89+
try:
90+
item.restore().execute_query()
91+
print(f" ✓ Restored: {item.properties.get('name', '?')}")
92+
except Exception as e:
93+
print(f" Restore not available: {e}")
94+
95+
# -- Step 4: permanent delete a specific item (commented out) --
96+
# Permanent delete bypasses the bin entirely:
97+
# item.permanent_delete().execute_query()
98+
#
99+
# Or permanently purge from recycle bin:
100+
# recycle.items["<item-id>"].permanent_delete().execute_query()
101+
102+
103+
if __name__ == "__main__":
104+
main()

0 commit comments

Comments
 (0)