Week 1 · Ingest anything › Day 5
Day 5. SharePoint, OneDrive, Google Drive, SFTP and object storage
By the end of today you can
- Pull files from SharePoint/OneDrive with Microsoft Graph and from Google Drive with its API
- Pull from SFTP with paramiko and handle partial uploads
- Land into object storage (S3, Azure Blob/ADLS, or MinIO locally)
- Pick the right transfer tool for a given company
1. Why this matters
"Shared folder" in a modern company usually means SharePoint/OneDrive (Microsoft shops), Google Drive (Google shops), an SFTP server (vendors, hotel PMS, payroll, banks) or a bucket (S3, Azure Blob/ADLS Gen2, GCS). Each has its own way of listing files, detecting changes and failing.
2. SharePoint and OneDrive via Graph
Same app-registration pattern as Day 4, permission Sites.Selected (scoped to one site, preferred) or Files.Read.All. Every library is a drive; you list children and download content. The delta endpoint returns only what changed since a token you saved: a server-side watermark.
site = requests.get("https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/Finance", headers=H).json()
drive = requests.get(f"https://graph.microsoft.com/v1.0/sites/{site['id']}/drive", headers=H).json()
url = saved_delta_link or f"https://graph.microsoft.com/v1.0/drives/{drive['id']}/root:/Exports/QSR:/delta"
while url:
page = requests.get(url, headers=H, timeout=30).json()
for item in page["value"]:
if "file" in item and "deleted" not in item:
content = requests.get(f"https://graph.microsoft.com/v1.0/drives/{drive['id']}/items/{item['id']}/content",
headers=H, timeout=60).content
# land(content, name=item["name"], etag=item["eTag"], modified=item["lastModifiedDateTime"])
url = page.get("@odata.nextLink")
if "@odata.deltaLink" in page:
saved_delta_link = page["@odata.deltaLink"] # persist this; next run starts from it
3. Google Drive
Use a service account, share the folder with the service account's email, and query with files.list(q="'<folderId>' in parents and trashed=false and modifiedTime > '...'"). Google Sheets need files.export to CSV/XLSX; regular files use files.get_media. The changes API is Drive's equivalent of delta.
4. SFTP
Vendors push to (or let you pull from) SFTP. Two classic failure modes: the partial upload (you read while they write) and host-key surprises (the vendor rebuilt their server).
import paramiko, stat
def pull_sftp(host, user, key_path, remote_dir, local_dir, known_hosts="~/.ssh/known_hosts"):
client = paramiko.SSHClient()
client.load_host_keys(os.path.expanduser(known_hosts)) # reject unknown hosts: no AutoAddPolicy in prod
client.connect(host, username=user, key_filename=key_path, timeout=30)
sftp = client.open_sftp()
for a in sftp.listdir_attr(remote_dir):
if stat.S_ISREG(a.st_mode) and a.filename.endswith(".txt"): # vendor renames .partial -> .txt when done
sftp.get(f"{remote_dir}/{a.filename}", str(local_dir / a.filename))
client.close()
Agree a completion signal with the vendor: rename on finish (.partial → .txt, as in the course data), a _SUCCESS or .done marker file, or a row count in a trailer line. Without one, use the size-stable check from Day 3.
5. Object storage as your raw zone
From here on, think of the raw zone as a bucket, even if it's a local folder today. Buckets are cheap (cents per GB-month), durable and every tool reads them. Locally you can run MinIO (S3-compatible) in Docker; in Azure you'd use ADLS Gen2; fsspec/s3fs/adlfs let one Python path string work against all of them.
import fsspec
fs = fsspec.filesystem("s3", key="minio", secret="minio123", client_kwargs={"endpoint_url": "http://localhost:9000"})
fs.put("data/raw/hotel/business_date=2026-09-14/pms_bookings_20260914__ab12cd34.txt",
"raw/hotel/business_date=2026-09-14/pms_bookings_20260914__ab12cd34.txt")
Transfer tools you should know exist
rclone syncs between 70+ storage backends (SharePoint, Drive, S3, SFTP) from one CLI: great for a cheap stack. AzCopy for Azure. Azure Data Factory / Fabric pipelines copy activities for Microsoft-only shops. dlt has filesystem and sftp sources that do listing, incremental state and loading in a few lines (Day 6).
Maths and principle
Principle: change detection is a set difference between what exists now and what you've seen.
Equation: \(\Delta_t = S_t \setminus S_{t-1}\) for new items, plus \(\{x \in S_t \cap S_{t-1} : \text{etag}_t(x) \neq \text{etag}_{t-1}(x)\}\) for changed ones. Delta APIs compute this on the server so you don't list a million files.
Analogy: a diff in git. Where it breaks: deletes. A file removed from the source is in \(S_{t-1} \setminus S_t\); decide whether that means "retract the data" or "nothing" (usually nothing for raw; flag it).
Try it: yesterday 400 files, today 403 listed, 2 etags changed, 1 file deleted. How many to download? (4 new + 2 changed = 6.)
The 5-year lens
Least privilege is your friend: ask for Sites.Selected on one SharePoint site, not tenant-wide file access. Security teams approve narrow requests in days and wide ones in months. And keep vendor SFTP credentials per vendor, rotated, with the key in a vault, because the day a vendor changes servers is the day you learn whether you can still find the key.
Lab
- Run MinIO locally:
docker run -p 9000:9000 -p 9001:9001 -e MINIO_ROOT_USER=minio -e MINIO_ROOT_PASSWORD=minio123 quay.io/minio/minio server /data --console-address :9001. Create bucketraw. (No Docker? Use a local folder andfsspec.filesystem("file"), same code.) - Run a local SFTP server:
docker run -p 2222:22 -v $PWD/data/landing/sftp/hotel:/home/pms/upload atmoz/sftp pms:pass:1001. Pull with paramiko (password auth is fine locally). - Write
sources/sftp.py: skip.partial, land to the bucket with the Day 3 manifest. Confirm the late file is skipped until you rename it. - Make the raw path a URL setting (
P2D_RAW_URL=s3://raworfile://./data/raw) and usefsspeceverywhere. Run the email and folder landers against MinIO without code changes. - Write a one-page
docs/transfer-choices.md: for a Microsoft 365 company, a Google company and a no-licence company, which tool would you use for each source and why.
Ship
feat(ingest): sftp source and fsspec raw zone (local or S3-compatible).
Check yourself
1. A vendor uploads a 2 GB file to SFTP over 10 minutes. What protects you from reading half of it?
The writer has to tell you it's done, or you infer it from a stable size over time.
2. Which Graph permission is best for a pipeline that reads one SharePoint site?
Narrow scope is safer and gets approved faster.
3. Why write the raw zone through fsspec URLs?
Storage becomes configuration, which matters when you move from laptop to cloud.