Pipeline to Dashboardnihalsulaiman.com

Week 1 · Ingest anything › Day 5

Day 5. SharePoint, OneDrive, Google Drive, SFTP and object storage

⏱ 4h · learn 60m · lab 140m · ship 25m · review 15m🏷 hospitality (SFTP PMS extracts)Before: Day 4

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

  1. 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 bucket raw. (No Docker? Use a local folder and fsspec.filesystem("file"), same code.)
  2. 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).
  3. 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.
  4. Make the raw path a URL setting (P2D_RAW_URL=s3://raw or file://./data/raw) and use fsspec everywhere. Run the email and folder landers against MinIO without code changes.
  5. 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?

2. Which Graph permission is best for a pipeline that reads one SharePoint site?

3. Why write the raw zone through fsspec URLs?

My notes