#!/usr/bin/env python3
"""Export a Tableau workbook inventory (name, project, owner, dates) to CSV.

Uses the Tableau REST API with a personal access token (PAT). Read-only calls:
  POST /api/{version}/auth/signin
  GET  /api/{version}/sites/{site-id}/workbooks   (paged)
  GET  /api/{version}/sites/{site-id}/users       (paged, to turn owner ids into names)
  POST /api/{version}/auth/signout

Configuration comes from environment variables, nothing is hard coded:
  TABLEAU_SERVER_URL        e.g. https://tableau.example.com  (Tableau Cloud: include the pod,
                            e.g. https://prod-ca-a.online.tableau.com)
  TABLEAU_PAT_NAME          name of the personal access token
  TABLEAU_PAT_SECRET        secret of the personal access token
  TABLEAU_SITE_CONTENT_URL  site content URL; leave empty for the default site on Tableau Server
  TABLEAU_API_VERSION       optional, defaults to 3.29 (the version used in Tableau's own
                            examples). Use a version your server supports.

Requires Python 3 and the requests package (pip install requests).

Usage:
  python3 tableau-inventory.py workbooks.csv

Status: written against the Tableau REST API reference. Parsing was tested only against
a mock server that returns XML shaped like the documented responses. It has not been
run against a real Tableau Server or Tableau Cloud site. Try it on a test site first.
"""

import csv
import os
import sys
import xml.etree.ElementTree as ET

import requests

NS = {"t": "http://tableau.com/api"}
PAGE_SIZE = 1000  # documented maximum for pageSize


def env(name, default=None, required=False):
    value = os.environ.get(name, default)
    if required and not value:
        sys.exit(f"Missing environment variable: {name}")
    return value


def sign_in(base, name, secret, site_content_url):
    body = (
        "<tsRequest>"
        f'<credentials personalAccessTokenName="{xml_attr(name)}" '
        f'personalAccessTokenSecret="{xml_attr(secret)}">'
        f'<site contentUrl="{xml_attr(site_content_url)}" />'
        "</credentials></tsRequest>"
    )
    r = requests.post(
        f"{base}/auth/signin",
        data=body.encode("utf-8"),
        headers={"Content-Type": "application/xml"},
        timeout=60,
    )
    r.raise_for_status()
    root = ET.fromstring(r.content)
    creds = root.find("t:credentials", NS)
    site = creds.find("t:site", NS)
    return creds.get("token"), site.get("id")


def xml_attr(value):
    return (
        value.replace("&", "&amp;")
        .replace('"', "&quot;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
    )


def paged(session, url, item_path):
    """Yield child elements for every page. item_path is e.g. 't:workbooks/t:workbook'."""
    page = 1
    while True:
        r = session.get(
            url, params={"pageSize": PAGE_SIZE, "pageNumber": page}, timeout=120
        )
        r.raise_for_status()
        root = ET.fromstring(r.content)
        for item in root.findall(item_path, NS):
            yield item
        pg = root.find("t:pagination", NS)
        total = int(pg.get("totalAvailable"))
        if page * PAGE_SIZE >= total:
            return
        page += 1


def main():
    if len(sys.argv) != 2:
        sys.exit("Usage: python3 tableau-inventory.py output.csv")
    out_path = sys.argv[1]

    server = env("TABLEAU_SERVER_URL", required=True).rstrip("/")
    pat_name = env("TABLEAU_PAT_NAME", required=True)
    pat_secret = env("TABLEAU_PAT_SECRET", required=True)
    site_url = env("TABLEAU_SITE_CONTENT_URL", "")
    version = env("TABLEAU_API_VERSION", "3.29")
    base = f"{server}/api/{version}"

    token, site_id = sign_in(base, pat_name, pat_secret, site_url)
    session = requests.Session()
    session.headers["X-Tableau-Auth"] = token

    try:
        # The workbook list gives the owner as an id only, so fetch users to get names.
        owners = {}
        for u in paged(session, f"{base}/sites/{site_id}/users", "t:users/t:user"):
            owners[u.get("id")] = u.get("fullName") or u.get("name") or ""

        rows = []
        for wb in paged(
            session, f"{base}/sites/{site_id}/workbooks", "t:workbooks/t:workbook"
        ):
            project = wb.find("t:project", NS)
            owner = wb.find("t:owner", NS)
            owner_id = owner.get("id") if owner is not None else ""
            rows.append(
                {
                    "workbook": wb.get("name"),
                    "project": project.get("name") if project is not None else "",
                    "owner": owners.get(owner_id, owner_id),
                    "created at": wb.get("createdAt"),
                    "last published (updatedAt)": wb.get("updatedAt"),
                    "workbook id": wb.get("id"),
                    "content url": wb.get("contentUrl"),
                }
            )
    finally:
        session.post(f"{base}/auth/signout", timeout=60)

    rows.sort(key=lambda r: (r["project"], r["workbook"]))
    with open(out_path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()) if rows else [])
        writer.writeheader()
        writer.writerows(rows)
    print(f"Wrote {len(rows)} workbooks to {out_path}")


if __name__ == "__main__":
    main()
