{ "cells": [ { "cell_type": "markdown", "id": "a1b2c3d4", "metadata": {}, "source": [ "# GHG Audit Export" ] }, { "cell_type": "markdown", "id": "b2c3d4e5", "metadata": {}, "source": [ "**Table of contents**\n", "- Overview\n", "\n", "- Setup\n", "\n", " - Authentication Token\n", "\n", "- Trigger Audit Export\n", "\n", " - Output Description\n", "\n", "- Get Audit Export Status\n", "\n", " - Output Description\n", "\n", "- Download Audit Export\n", "\n", "- Related Links" ] }, { "cell_type": "markdown", "id": "c3d4e5f6", "metadata": {}, "source": [ "## Overview\n", "\n", "This notebook demonstrates how to use the GHG Audit Export APIs to retrieve a full audit record of API activity for your organization for a given date range.\n", "\n", "The export is generated asynchronously. The workflow is:\n", "\n", "1. **Trigger** — submit an export request with a date range using `POST /audit`.\n", "2. **Poll** — check the status of the request using `GET /audit/status` until the status is `COMPLETED` or `FAILED`.\n", "3. **Download** — retrieve the ZIP archive using `GET /audit/download` once the status is `COMPLETED`.\n", "\n", "All endpoints require an admin bearer token. If the authenticated user is not an admin, the service returns `403 Forbidden`." ] }, { "cell_type": "markdown", "id": "d4e5f6a7", "metadata": {}, "source": [ "## Setup\n", "\n", "Ensure that Python 3+ is installed on your system.\n", "\n", "Note: To run this notebook, add your credentials to `../../../auth/secrets.ini` and `../../../auth/config.ini`.\n", "\n", "Example `secrets.ini` format:\n", "\n", "```\n", "[EAPI]\n", "api.pat_token = \n", "api.tenant_id = \n", "```" ] }, { "cell_type": "code", "execution_count": null, "id": "e5f6a7b8", "metadata": {}, "outputs": [], "source": [ "# Install the prerequisite Python packages\n", "%pip install pandas configparser IPython requests" ] }, { "cell_type": "code", "execution_count": null, "id": "f6a7b8c9", "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import configparser\n", "import requests\n", "import json\n", "import time\n", "from IPython.display import display as display_summary" ] }, { "cell_type": "markdown", "id": "a7b8c9d0", "metadata": {}, "source": [ "### Authentication Token\n", "\n", "Run the following code snippet to generate a Bearer Token using your PAT token. These admin endpoints require an authenticated admin user." ] }, { "cell_type": "code", "execution_count": null, "id": "b8c9d0e1", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Authentication Success\n" ] } ], "source": [ "config = configparser.RawConfigParser()\n", "config.read(['../../../auth/secrets.ini','../../../auth/config.ini'])\n", "\n", "EAPI_PAT_TOKEN = config.get('EAPI', 'api.pat_token')\n", "EAPI_TENANT_ID = config.get('EAPI', 'api.tenant_id')\n", "\n", "EAPI_AUTH_ENDPOINT = config.get('EAPI', 'api.auth_endpoint')\n", "EAPI_BASE_URL = config.get('EAPI', 'api.base_url')\n", "\n", "EAPI_AUTH_CLIENT_ID = 'saascore-' + EAPI_TENANT_ID\n", "EAPI_CLIENT_ID = 'ghgemissions-' + EAPI_TENANT_ID\n", "\n", "auth_request_headers: dict = {}\n", "auth_request_headers[\"X-IBM-Client-Id\"] = EAPI_AUTH_CLIENT_ID\n", "auth_request_headers[\"X-IBM-Envizi-Pat\"] = EAPI_PAT_TOKEN\n", "\n", "verify = True\n", "\n", "auth_url = f\"{EAPI_AUTH_ENDPOINT}\"\n", " \n", "response = requests.post(url = auth_url,\n", " headers = auth_request_headers,\n", " verify = verify\n", " )\n", "if response.status_code == 200:\n", " jwt_token = response.text\n", " print(\"Authentication Success\")\n", "else: \n", " print(\"Authentication Failed\")\n", " print(response.text)" ] }, { "cell_type": "markdown", "id": "c9d0e1f2", "metadata": {}, "source": [ "## Trigger Audit Export\n", "\n", "Use `POST /audit` to queue an audit export job for a given date range.\n", "\n", "The request body accepts the following parameters:\n", "\n", "- `fromDate` (required): Start date of the audit period (inclusive), in `YYYY-MM-DD` format.\n", "- `toDate` (required): End date of the audit period (inclusive), in `YYYY-MM-DD` format.\n", "- `apiName` (optional): Filter the export to a specific API endpoint — partial match on endpoint path.\n", "\n", "The endpoint returns:\n", "\n", "- `202 Accepted` for a fresh request, or a retry after a previous failure.\n", "- `409 Conflict` if an identical request is already in progress or has already completed." ] }, { "cell_type": "code", "execution_count": null, "id": "d0e1f2a3", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Status Code: 202\n", "requestId: tenant-uuid/20250101/20250331\n" ] } ], "source": [ "AUDIT_ENDPOINT = f\"{EAPI_BASE_URL}/audit\"\n", "\n", "payload = {\n", " 'fromDate': '2025-01-01',\n", " 'toDate': '2025-03-31'\n", " # Optional: uncomment to filter by a specific API endpoint\n", " # 'apiName': '/location'\n", "}\n", "\n", "request_headers: dict = {}\n", "request_headers['Content-Type'] = 'application/json'\n", "request_headers['x-ibm-client-id'] = EAPI_CLIENT_ID\n", "request_headers['Authorization'] = 'Bearer ' + jwt_token\n", "\n", "response = requests.post(\n", " AUDIT_ENDPOINT,\n", " headers=request_headers,\n", " data=json.dumps(payload)\n", ")\n", "\n", "print(f'Status Code: {response.status_code}')\n", "\n", "if response.status_code == 202:\n", " response_json = response.json()\n", " request_id = response_json.get('requestId')\n", " print(f'requestId: {request_id}')\n", " display_summary(pd.json_normalize(response_json))\n", "elif response.status_code == 409:\n", " print('Conflict: an identical request is already in progress or completed.')\n", " print(response.text)\n", "else:\n", " print(response.text)" ] }, { "cell_type": "markdown", "id": "e1f2a3b4", "metadata": {}, "source": [ "### Output Description\n", "\n", "requestId - Unique identifier for the export request. Pass this to the status and download endpoints.\n", "\n", "status - Current processing status. On acceptance this will be `QUEUED`.\n", "\n", "message - Human-readable result message.\n", "\n", "submittedAt - ISO-8601 timestamp indicating when the request was submitted.\n", "\n", "links - Hypermedia links for the status check and download endpoints.\n", "\n", "Possible HTTP status codes:\n", "\n", "- `202` - Request accepted — fresh request or retry after a previous failure\n", "- `400` - Bad request — missing or invalid parameters\n", "- `403` - Forbidden — admin token required\n", "- `409` - Conflict — request is already in progress or already completed\n", "- `500` - Internal server error" ] }, { "cell_type": "markdown", "id": "f2a3b4c5", "metadata": {}, "source": [ "## Get Audit Export Status\n", "\n", "Use `GET /audit/status` to check the current processing status of an audit export request.\n", "\n", "Pass the `requestId` returned by `POST /audit` as a query parameter. The endpoint always returns `HTTP 200` for a valid `requestId`; the `status` field in the response body indicates the actual job outcome.\n", "\n", "The tenant extracted from the token must match the tenant embedded in the `requestId`.\n", "\n", "Possible `status` values:\n", "\n", "| Status | Description |\n", "|---|---|\n", "| `QUEUED` | Request accepted and waiting to be processed |\n", "| `IN_PROGRESS` | Report is currently being generated |\n", "| `COMPLETED` | Report is ready for download |\n", "| `FAILED` | Report generation failed — see `message` for details |" ] }, { "cell_type": "code", "execution_count": null, "id": "a3b4c5d6", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Polling status...\n", "Status: QUEUED\n", "Status: IN_PROGRESS\n", "Status: COMPLETED\n", "Export is ready for download.\n" ] } ], "source": [ "AUDIT_STATUS_ENDPOINT = f\"{EAPI_BASE_URL}/audit/status\"\n", "\n", "request_headers: dict = {}\n", "request_headers['Content-Type'] = 'application/json'\n", "request_headers['x-ibm-client-id'] = EAPI_CLIENT_ID\n", "request_headers['Authorization'] = 'Bearer ' + jwt_token\n", "\n", "print('Polling status...')\n", "\n", "while True:\n", " response = requests.get(\n", " AUDIT_STATUS_ENDPOINT,\n", " headers=request_headers,\n", " params={'requestId': request_id}\n", " )\n", "\n", " if response.status_code != 200:\n", " print(f'Unexpected status code: {response.status_code}')\n", " print(response.text)\n", " break\n", "\n", " status_json = response.json()\n", " current_status = status_json.get('status')\n", " print(f'Status: {current_status}')\n", "\n", " if current_status == 'COMPLETED':\n", " print('Export is ready for download.')\n", " display_summary(pd.json_normalize(status_json))\n", " break\n", " elif current_status == 'FAILED':\n", " print(f'Export failed: {status_json.get(\"message\")}')\n", " break\n", " else:\n", " # QUEUED or IN_PROGRESS — wait before polling again\n", " time.sleep(10)" ] }, { "cell_type": "markdown", "id": "b4c5d6e7", "metadata": {}, "source": [ "### Output Description\n", "\n", "requestId - The unique identifier of the export request.\n", "\n", "status - Current job status: `QUEUED`, `IN_PROGRESS`, `COMPLETED`, or `FAILED`.\n", "\n", "submittedAt - ISO-8601 timestamp when the request was submitted. Present for `QUEUED`, `IN_PROGRESS`, and `FAILED` states.\n", "\n", "completedAt - ISO-8601 timestamp when the export completed. Present only when `status` is `COMPLETED`.\n", "\n", "message - Human-readable detail. Present when `status` is `FAILED` — describes why the export could not be generated.\n", "\n", "links.download - Download URL. Present only when `status` is `COMPLETED`.\n", "\n", "Possible HTTP status codes:\n", "\n", "- `200` - Status returned — always HTTP 200 for a valid `requestId`\n", "- `400` - Invalid or missing `requestId`, or `requestId` belongs to a different tenant\n", "- `403` - Forbidden — admin token required" ] }, { "cell_type": "markdown", "id": "c5d6e7f8", "metadata": {}, "source": [ "## Download Audit Export\n", "\n", "Use `GET /audit/download` to download the completed audit export. This endpoint is only available when the export status is `COMPLETED`.\n", "\n", "The response is a ZIP archive (`application/octet-stream`) containing a single CSV file named `audit-export.csv`.\n", "\n", "The tenant extracted from the token must match the tenant embedded in the `requestId`." ] }, { "cell_type": "code", "execution_count": null, "id": "d6e7f8a9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Status Code: 200\n", "Export saved to: audit-export.zip\n" ] } ], "source": [ "AUDIT_DOWNLOAD_ENDPOINT = f\"{EAPI_BASE_URL}/audit/download\"\n", "\n", "request_headers: dict = {}\n", "request_headers['x-ibm-client-id'] = EAPI_CLIENT_ID\n", "request_headers['Authorization'] = 'Bearer ' + jwt_token\n", "\n", "response = requests.get(\n", " AUDIT_DOWNLOAD_ENDPOINT,\n", " headers=request_headers,\n", " params={'requestId': request_id}\n", ")\n", "\n", "print(f'Status Code: {response.status_code}')\n", "\n", "if response.status_code == 200:\n", " output_file = 'audit-export.zip'\n", " with open(output_file, 'wb') as f:\n", " f.write(response.content)\n", " print(f'Export saved to: {output_file}')\n", "elif response.status_code == 409:\n", " print('Report is still processing or generation failed. Check status before downloading.')\n", "else:\n", " print(response.text)" ] }, { "cell_type": "markdown", "id": "e7f8a9b0", "metadata": {}, "source": [ "The downloaded ZIP archive contains a single file: `audit-export.csv`.\n", "\n", "Possible HTTP status codes:\n", "\n", "- `200` - ZIP file returned successfully\n", "- `400` - Invalid `requestId` for this tenant\n", "- `403` - Forbidden — admin token required\n", "- `409` - Report still processing, or generation failed" ] }, { "cell_type": "markdown", "id": "f8a9b0c1", "metadata": {}, "source": [ "## Related Links\n", "\n", "[Emissions API Developer Guide](https://developer.ibm.com/apis/catalog/ghgemissions--ibm-envizi-emissions-api/Introduction)" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbformat_minor": 5, "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.10" } }, "nbformat": 4, "nbformat_minor": 5 }