From a3bce9106dca28bded63e37955df7f6cd09bb396 Mon Sep 17 00:00:00 2001 From: dbasilfid <117305439+dbasilfid@users.noreply.github.com> Date: Tue, 30 Dec 2025 19:25:28 -0500 Subject: [PATCH 1/2] Add new api-examples/ directory structure with REST API publishing validation notebooks for Python and TypeScript. New structure: - api-examples/ - README.md (overview of REST API examples) - publishing/ - README.md (publishing-specific concepts) - python/validate-rest-api-examples.ipynb - typescript/validate-typescript-rest-api-example.nnb Features: - Python notebook: batch publishing, streaming, updates, production retry logic - TypeScript notebook: type-safe patterns, native fetch, error handling - Comprehensive documentation at each level - Updated main README with "REST API Examples" section These notebooks validate code examples from the official Fiddler REST API documentation and serve as production-ready reference implementations for language-agnostic REST API integration. --- README.md | 53 ++ api-examples/README.md | 62 ++ api-examples/publishing/README.md | 53 ++ .../python/validate-rest-api-examples.ipynb | 607 ++++++++++++++++++ .../validate-typescript-rest-api-example.nnb | 46 ++ 5 files changed, 821 insertions(+) create mode 100644 api-examples/README.md create mode 100644 api-examples/publishing/README.md create mode 100644 api-examples/publishing/python/validate-rest-api-examples.ipynb create mode 100644 api-examples/publishing/typescript/validate-typescript-rest-api-example.nnb diff --git a/README.md b/README.md index ce7378c55..6628b9b04 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,59 @@ This repo contains the example notebooks listed below. You can launch them in a * [Ranking Model - Monitoring](https://github.com/fiddler-labs/fiddler-examples/blob/main/quickstart/latest/Fiddler_Quickstart_Ranking_Model.ipynb) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/fiddler-labs/fiddler-examples/blob/main/quickstart/latest/Fiddler_Quickstart_Ranking_Model.ipynb) * [Regression Model - Monitoring](https://github.com/fiddler-labs/fiddler-examples/blob/main/quickstart/latest/Fiddler_Quickstart_Regression_Model.ipynb) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/fiddler-labs/fiddler-examples/blob/main/quickstart/latest/Fiddler_Quickstart_Regression_Model.ipynb) +## REST API Examples + +The [`api-examples`](./api-examples) directory contains validation notebooks and reference implementations for working with the Fiddler REST API. These examples are ideal for language-agnostic integration and production deployments. + +### Event Publishing + +* **[Python Publishing Examples](https://github.com/fiddler-labs/fiddler-examples/blob/main/api-examples/publishing/python/validate-rest-api-examples.ipynb)** - Comprehensive REST API usage in Python + * Batch publishing (file upload + publish with Job tracking) + * Streaming publishing (direct events with queue submission) + * Event updates (PATCH operations) + * Production-ready publisher with exponential backoff retry logic + +* **[TypeScript Publishing Examples](https://github.com/fiddler-labs/fiddler-examples/blob/main/api-examples/publishing/typescript/validate-typescript-rest-api-example.nnb)** - TypeScript REST API patterns + * Native `fetch` API implementation + * Type-safe interfaces for Fiddler API v3.0 + * Production-ready publisher class with retry logic + * Comprehensive error handling + +**Key Concepts:** +- Fiddler automatically selects processing mode based on data source (files → batch with Job tracking, JSON/lists → streaming with queue submission) +- Only PRODUCTION events can be updated (PRE_PRODUCTION events are immutable) +- All operations return HTTP 202 Accepted for consistency + +See the [api-examples README](./api-examples/README.md) for setup instructions and additional details. + + + +## REST API Examples + +The [`api-examples`](./api-examples) directory contains validation notebooks and reference implementations for working with the Fiddler REST API. These examples are ideal for language-agnostic integration and production deployments. + +### Event Publishing + +* **[Python Publishing Examples](https://github.com/fiddler-labs/fiddler-examples/blob/main/api-examples/publishing/python/validate-rest-api-examples.ipynb)** - Comprehensive REST API usage in Python + * Batch publishing (file upload + publish with Job tracking) + * Streaming publishing (direct events with queue submission) + * Event updates (PATCH operations) + * Production-ready publisher with exponential backoff retry logic + +* **[TypeScript Publishing Examples](https://github.com/fiddler-labs/fiddler-examples/blob/main/api-examples/publishing/typescript/validate-typescript-rest-api-example.nnb)** - TypeScript REST API patterns + * Native `fetch` API implementation + * Type-safe interfaces for Fiddler API v3.0 + * Production-ready publisher class with retry logic + * Comprehensive error handling + +**Key Concepts:** +- Fiddler automatically selects processing mode based on data source (files → batch with Job tracking, JSON/lists → streaming with queue submission) +- Only PRODUCTION events can be updated (PRE_PRODUCTION events are immutable) +- All operations return HTTP 202 Accepted for consistency + +See the [api-examples README](./api-examples/README.md) for setup instructions and additional details. + + ## Fiddler Utils Package - Admin Automation Library The [`fiddler_utils`](./fiddler_utils) package is an admin automation library designed to reduce code duplication across utility scripts and notebooks. While **not part of the official Fiddler SDK**, it is available for both Fiddler field engineers and customers to use and extend. diff --git a/api-examples/README.md b/api-examples/README.md new file mode 100644 index 000000000..8905392d1 --- /dev/null +++ b/api-examples/README.md @@ -0,0 +1,62 @@ +# Fiddler REST API Examples + +This directory contains examples and validation notebooks for working with the Fiddler REST API. + +## Directory Structure + +- **publishing/** - Event publishing examples (batch, streaming, updates) + - `python/` - Python REST API examples using `requests` library + - `typescript/` - TypeScript REST API examples using native `fetch` + +## About These Examples + +These notebooks serve dual purposes: + +1. **Validation** - Test and validate code examples from the official Fiddler documentation +2. **Reference** - Provide production-ready example code for REST API integration + +Unlike the quick start guides in `/quickstart/`, these examples focus on: +- REST API usage patterns and best practices +- Language-agnostic integration (not requiring the Python SDK) +- Production-ready error handling and retry logic +- Comprehensive coverage of API operations + +## Prerequisites + +### Python Examples +```bash +pip install requests ipykernel +``` + +### TypeScript Examples +For TypeScript notebooks, install the tslab kernel: +```bash +npm install -g tslab +tslab install +``` + +## Getting Started + +1. Set your environment variables: + ```bash + export FIDDLER_API_KEY="your-api-key" + export FIDDLER_ENDPOINT="https://your-instance.fiddler.ai" + export MODEL_ID="your-model-uuid" + ``` + +2. Choose your language and open the corresponding notebook +3. Run all cells to validate the examples + +## Documentation + +These examples validate code from the official Fiddler documentation: +- [Publishing via REST API](https://docs.fiddler.ai) - Quick-start guide +- [Advanced REST API Publishing](https://docs.fiddler.ai) - Production patterns + +## Contributing + +When adding new REST API examples: +1. Create language-specific subdirectories under the relevant topic (e.g., `publishing/python/`) +2. Include validation tests for documented code examples +3. Provide production-ready implementations with error handling +4. Update this README with links to the new examples diff --git a/api-examples/publishing/README.md b/api-examples/publishing/README.md new file mode 100644 index 000000000..24be125bf --- /dev/null +++ b/api-examples/publishing/README.md @@ -0,0 +1,53 @@ +# Event Publishing Examples + +Examples for publishing inference events to Fiddler using the REST API. + +## Available Examples + +### Python (`python/`) +- **validate-rest-api-examples.ipynb** - Comprehensive Python examples covering: + - Batch publishing (file upload + publish) + - Streaming publishing (direct events) + - Event updates (PATCH operations) + - Production-ready publisher with exponential backoff retry logic + +### TypeScript (`typescript/`) +- **validate-typescript-rest-api-example.nnb** - TypeScript examples demonstrating: + - File upload and batch publishing with Job tracking + - Streaming events with queue submission + - Event updates with immutability constraints + - Production-ready publisher class with retry logic + +## Key Concepts + +### Automatic Processing Mode Selection + +Fiddler automatically selects processing mode based on data source: + +- **Files (CSV, Parquet)** → Batch processing with Job tracking + - Returns `job_id` for monitoring via Jobs API + - Asynchronous background processing + - Best for large datasets + +- **JSON Arrays / Python Lists** → Streaming with queue submission + - Returns `event_ids` confirming queue submission + - Lower latency, queue-based processing + - Best for real-time or small batches + +### Environment Constraints + +**CRITICAL**: Only PRODUCTION events can be updated. Non-production events (PRE_PRODUCTION) are immutable and cannot be modified via batch OR streaming processes once published. + +## Documentation References + +These examples validate code from: +- [Publishing via REST API (Quick Start)](https://docs.fiddler.ai/docs/python-client-guides/publishing-production-data/publishing-via-rest-api) +- [Advanced REST API Publishing](https://docs.fiddler.ai/docs/python-client-guides/publishing-production-data/publishing-via-rest-api-advanced) + +## Running the Examples + +1. Set environment variables (API key, endpoint, model ID) +2. Open the notebook for your preferred language +3. Run all cells to execute the validation tests + +Each notebook is self-contained and demonstrates all publishing workflows end-to-end. diff --git a/api-examples/publishing/python/validate-rest-api-examples.ipynb b/api-examples/publishing/python/validate-rest-api-examples.ipynb new file mode 100644 index 000000000..dc4f4c7e9 --- /dev/null +++ b/api-examples/publishing/python/validate-rest-api-examples.ipynb @@ -0,0 +1,607 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# REST API Publishing Examples Validation (Python)\n", + "\n", + "This notebook validates Python code examples from the Fiddler REST API publishing guides:\n", + "- [Publishing via REST API](../python-client-guides/publishing-production-data/publishing-via-rest-api.md) (Quick-start guide)\n", + "- [Advanced REST API Publishing](../python-client-guides/publishing-production-data/publishing-via-rest-api-advanced.md) (Advanced guide)\n", + "\n", + "## Prerequisites\n", + "\n", + "### Environment Variables\n", + "Set these environment variables before running:\n", + "```bash\n", + "export FIDDLER_API_KEY=\"your-api-key\"\n", + "export FIDDLER_ENDPOINT=\"https://your-instance.fiddler.ai\"\n", + "export MODEL_ID=\"your-model-uuid\"\n", + "```\n", + "\n", + "### Python Dependencies\n", + "```bash\n", + "pip install requests ipykernel\n", + "```\n", + "\n", + "## Notebook Structure\n", + "\n", + "1. **Setup** - Environment variables and test data creation\n", + "2. **Quick-Start Examples** - Python examples from quick-start guide\n", + "3. **Advanced Examples** - Production-ready Python publisher with retry logic\n", + "4. **Cleanup** - Remove temporary files\n", + "\n", + "## Note on TypeScript Examples\n", + "\n", + "For TypeScript examples, see the dedicated [TypeScript REST API Validation Notebook](validate-typescript-rest-api-example.nnb).\n", + "\n", + "## Note on curl Examples\n", + "\n", + "The markdown documentation includes curl examples for language-agnostic reference. This notebook uses Python `requests` for clarity and simplicity." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Section 1: Setup\n", + "\n", + "Initialize environment variables and create test data." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u2713 Environment configured\n", + " Endpoint: https://preprod.cloud.fiddler.ai\n", + " Model ID: 811fcb34-fb3e-458d-a81f-b4ecd17ec1fd\n", + "\u2713 Created test CSV file: churn_events.csv\n" + ] + } + ], + "source": [ + "import json", + "import os", + "from datetime import datetime, timezone", + "from typing import Dict, List", + "", + "import requests", + "", + "# Load environment variables", + "api_key = os.environ.get(\"FIDDLER_API_KEY\")", + "fiddler_endpoint = os.environ.get(\"FIDDLER_ENDPOINT\")", + "model_id = os.environ.get(\"MODEL_ID\")", + "", + "# Validate environment", + "if not all([api_key, fiddler_endpoint, model_id]):", + " raise ValueError(", + " \"Missing required environment variables. Set: \"", + " \"FIDDLER_API_KEY, FIDDLER_ENDPOINT, MODEL_ID\"", + " )", + "", + "print(\"\u2713 Environment configured\")", + "print(f\" Endpoint: {fiddler_endpoint}\")", + "print(f\" Model ID: {model_id}\")", + "", + "# Create test CSV file for batch publishing with bank churn schema", + "csv_content = \"\"\"customer_id,creditscore,geography,gender,age,tenure,balance,numofproducts,hascrcard,isactivemember,estimatedsalary,predicted_churn,churn,timestamp", + "27c349a2,559,California,Male,52,2,0.0,1,1,0,129013.59,0.007447,no,2025-12-28T12:00:00Z", + "27c35cee,482,California,Male,55,5,97318.25,1,0,1,78416.14,0.804852,yes,2025-12-27T12:01:00Z", + "27c364f0,651,Florida,Female,46,4,89743.05,1,1,0,156425.57,0.012754,no,2025-12-29T12:02:00Z", + "\"\"\"", + "", + "with open(\"churn_events.csv\", \"w\") as f:", + " f.write(csv_content)", + "", + "print(\"\u2713 Created test CSV file: churn_events.csv\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Section 2: Quick-Start Examples\n", + "\n", + "Examples from [publishing-via-rest-api.md](../python-client-guides/publishing-production-data/publishing-via-rest-api.md)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.1 Batch Upload - Python Example\n", + "\n", + "Upload CSV file and save file_id for batch publishing." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u2713 File uploaded. File ID: 71573cc3-e29d-47ea-9ebc-3987312201ad\n" + ] + } + ], + "source": [ + "# Upload file", + "with open(\"churn_events.csv\", \"rb\") as f:", + " response = requests.post(", + " f\"{fiddler_endpoint}/v3/files/upload\",", + " headers={\"Authorization\": f\"Bearer {api_key}\"},", + " files={\"file\": f}", + " )", + "", + "file_id = response.json()[\"data\"][\"id\"]", + "print(f\"\u2713 File uploaded. File ID: {file_id}\")", + "", + "# Save for next step", + "with open(\"file_id.txt\", \"w\") as f:", + " f.write(file_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.2 Batch Publish - Python Example\n", + "\n", + "Publish events from the uploaded file." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u2713 Batch publish started. Job ID: cc2d9811-ff97-4527-930b-1999b22c0e1a\n" + ] + } + ], + "source": [ + "# Read file_id from previous upload", + "with open(\"file_id.txt\", \"r\") as f:", + " file_id = f.read().strip()", + "", + "payload = {", + " \"model_id\": model_id,", + " \"env_type\": \"PRODUCTION\",", + " \"source\": {", + " \"type\": \"FILE\",", + " \"file_id\": file_id", + " }", + "}", + "", + "response = requests.post(", + " f\"{fiddler_endpoint}/v3/events\",", + " headers={\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {api_key}\"},", + " data=json.dumps(payload)", + ")", + "", + "job_id = response.json()[\"data\"][\"job\"][\"id\"]", + "print(f\"\u2713 Batch publish started. Job ID: {job_id}\")", + "", + "# Save for reference", + "with open(\"job_id.txt\", \"w\") as f:", + " f.write(job_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.3 Streaming - Python Example\n", + "\n", + "Stream individual events directly to Fiddler." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u2713 Streaming publish completed. Published 1 event(s)\n" + ] + } + ], + "source": [ + "events = [", + " {", + " \"customer_id\": \"27c349a2\",", + " \"creditscore\": 559,", + " \"geography\": \"California\",", + " \"gender\": \"Male\",", + " \"age\": 52,", + " \"tenure\": 2,", + " \"balance\": 0.0,", + " \"numofproducts\": 1,", + " \"hascrcard\": 1,", + " \"isactivemember\": 0,", + " \"estimatedsalary\": 129013.59,", + " \"predicted_churn\": 0.007447,", + " \"churn\": \"no\",", + " \"timestamp\": datetime.now(timezone.utc).isoformat()", + " }", + "]", + "", + "payload = {", + " \"model_id\": model_id,", + " \"env_type\": \"PRODUCTION\",", + " \"source\": {", + " \"type\": \"EVENTS\",", + " \"events\": events", + " }", + "}", + "", + "response = requests.post(", + " f\"{fiddler_endpoint}/v3/events\",", + " headers={\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {api_key}\"},", + " data=json.dumps(payload)", + ")", + "", + "event_ids = response.json()[\"data\"][\"event_ids\"]", + "print(f\"\u2713 Streaming publish completed. Published {len(event_ids)} event(s)\")", + "", + "# Save first event_id for updates", + "with open(\"event_id.txt\", \"w\") as f:", + " f.write(event_ids[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.4 Updates - Python Example\n", + "\n", + "Update existing events using PATCH.\n", + "\n", + "**IMPORTANT:** Only PRODUCTION events can be updated. Non-production events (PRE_PRODUCTION) are immutable and cannot be modified via batch OR streaming processes." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u2713 Update completed. Response status: 202\n" + ] + } + ], + "source": [ + "updates = [", + " {", + " \"customer_id\": \"27c349a2\",", + " \"churn\": \"yes\"", + " }", + "]", + "", + "payload = {", + " \"model_id\": model_id,", + " \"env_type\": \"PRODUCTION\",", + " \"source\": {", + " \"type\": \"EVENTS\",", + " \"events\": updates", + " }", + "}", + "", + "response = requests.patch(", + " f\"{fiddler_endpoint}/v3/events\",", + " headers={\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {api_key}\"},", + " data=json.dumps(payload)", + ")", + "", + "print(f\"\u2713 Update completed. Response status: {response.status_code}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Section 3: Advanced Examples\n", + "\n", + "Production-ready Python publisher from [publishing-via-rest-api-advanced.md](../python-client-guides/publishing-production-data/publishing-via-rest-api-advanced.md)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.1 Python ProductionStreamingPublisher\n", + "\n", + "Production-grade publisher with exponential backoff retry logic." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u2713 ProductionStreamingPublisher class defined\n", + "\u2713 Successfully published 2 events with ProductionStreamingPublisher\n" + ] + } + ], + "source": [ + "from typing import Optional", + "", + "", + "class ProductionStreamingPublisher:", + " def __init__(", + " self,", + " api_key: str,", + " endpoint: str,", + " max_retries: int = 3,", + " initial_delay: float = 1.0,", + " backoff_multiplier: float = 2.0", + " ):", + " self.api_key = api_key", + " self.endpoint = endpoint", + " self.max_retries = max_retries", + " self.initial_delay = initial_delay", + " self.backoff_multiplier = backoff_multiplier", + "", + " def _get_headers(self, is_retry: bool = False) -> Dict[str, str]:", + " headers = {", + " \"Content-Type\": \"application/json\",", + " \"Authorization\": f\"Bearer {self.api_key}\"", + " }", + " if is_retry:", + " headers[\"X-Fiddler-Client-Retry\"] = \"true\"", + " return headers", + "", + " def _calculate_backoff(self, attempt: int) -> float:", + " return self.initial_delay * (self.backoff_multiplier ** attempt)", + "", + " def _is_retryable(self, status_code: int) -> bool:", + " return status_code in [429, 500, 502, 503, 504]", + "", + " def publish_events_with_retry(", + " self,", + " model_id: str,", + " events: List[Dict]", + " ) -> Optional[List[str]]:", + " \"\"\"Publish events with exponential backoff retry logic.\"\"\"", + "", + " if not events:", + " raise ValueError(\"Events list cannot be empty\")", + "", + " if len(events) > 1000:", + " raise ValueError(f\"Batch size {len(events)} exceeds maximum 1000\")", + "", + " payload = {", + " \"model_id\": model_id,", + " \"env_type\": \"PRODUCTION\",", + " \"source\": {", + " \"type\": \"EVENTS\",", + " \"events\": events", + " }", + " }", + "", + " last_error = None", + "", + " for attempt in range(self.max_retries + 1):", + " try:", + " # Add delay and retry header for retries", + " if attempt > 0:", + " delay = self._calculate_backoff(attempt - 1)", + " print(f\"Retry attempt {attempt}/{self.max_retries} after {delay:.1f}s\")", + " time.sleep(delay)", + "", + " headers = self._get_headers(is_retry=attempt > 0)", + "", + " response = requests.post(", + " f\"{self.endpoint}/v3/events\",", + " headers=headers,", + " data=json.dumps(payload),", + " timeout=30", + " )", + "", + " # Streaming returns 202 Accepted (both batch and streaming use 202)", + " if response.status_code == 200 or response.status_code == 202:", + " result = response.json()", + " if attempt > 0:", + " print(f\"\u2713 Succeeded on retry attempt {attempt}\")", + " return result[\"data\"].get(\"event_ids\", [])", + "", + " # Check if retryable", + " if not self._is_retryable(response.status_code):", + " error_body = response.json()", + " raise Exception(", + " f\"Non-retryable error ({response.status_code}): \"", + " f\"{error_body.get('message', 'Unknown error')}\"", + " )", + "", + " error_body = response.json()", + " last_error = Exception(", + " f\"HTTP {response.status_code}: \"", + " f\"{error_body.get('message', 'Unknown error')}\"", + " )", + "", + " except requests.exceptions.RequestException as e:", + " last_error = e", + " # Network errors are retryable", + " if attempt == self.max_retries:", + " break", + "", + " raise Exception(", + " f\"Max retries ({self.max_retries}) exceeded. \"", + " f\"Last error: {str(last_error)}\"", + " )", + "", + " def publish_batch(", + " self,", + " model_id: str,", + " events: List[Dict],", + " batch_size: int = 1000", + " ) -> List[str]:", + " \"\"\"Publish large event lists in batches with retry.\"\"\"", + " all_event_ids = []", + "", + " for i in range(0, len(events), batch_size):", + " batch = events[i:i + batch_size]", + " print(f\"Publishing batch {i // batch_size + 1} ({len(batch)} events)\")", + "", + " event_ids = self.publish_events_with_retry(model_id, batch)", + " all_event_ids.extend(event_ids)", + "", + " print(f\"\u2713 Published {len(event_ids)} events\")", + "", + " return all_event_ids", + "", + "print(\"\u2713 ProductionStreamingPublisher class defined\")", + "", + "# Test the class with sample events", + "publisher = ProductionStreamingPublisher(", + " api_key=api_key,", + " endpoint=fiddler_endpoint,", + " max_retries=2,", + " initial_delay=1.0", + ")", + "", + "# Create test events with bank churn schema", + "test_events = [", + " {", + " \"customer_id\": \"27c349a2\",", + " \"creditscore\": 559,", + " \"geography\": \"California\",", + " \"gender\": \"Male\",", + " \"age\": 52,", + " \"tenure\": 2,", + " \"balance\": 0.0,", + " \"numofproducts\": 1,", + " \"hascrcard\": 1,", + " \"isactivemember\": 0,", + " \"estimatedsalary\": 129013.59,", + " \"predicted_churn\": 0.007447,", + " \"churn\": \"no\",", + " \"timestamp\": datetime.now(timezone.utc).isoformat()", + " },", + " {", + " \"customer_id\": \"27c35cee\",", + " \"creditscore\": 482,", + " \"geography\": \"California\",", + " \"gender\": \"Male\",", + " \"age\": 55,", + " \"tenure\": 5,", + " \"balance\": 97318.25,", + " \"numofproducts\": 1,", + " \"hascrcard\": 0,", + " \"isactivemember\": 1,", + " \"estimatedsalary\": 78416.14,", + " \"predicted_churn\": 0.804852,", + " \"churn\": \"yes\",", + " \"timestamp\": datetime.now(timezone.utc).isoformat()", + " }", + "]", + "", + "try:", + " event_ids = publisher.publish_events_with_retry(", + " model_id=model_id,", + " events=test_events", + " )", + " print(f\"\u2713 Successfully published {len(event_ids)} events with ProductionStreamingPublisher\")", + "except Exception as e:", + " print(f\"Publishing test: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Section 4: Cleanup\n", + "\n", + "Remove temporary files created during validation." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u2713 Removed churn_events.csv\n", + "\u2713 Removed file_id.txt\n", + "\u2713 Removed job_id.txt\n", + "\u2713 Removed event_id.txt\n", + "\n", + "=== Validation Complete ===\n", + "All Python code examples from REST API publishing guides have been tested.\n" + ] + } + ], + "source": [ + "import os", + "", + "temp_files = [", + " \"churn_events.csv\",", + " \"file_id.txt\",", + " \"job_id.txt\",", + " \"event_id.txt\"", + "]", + "", + "for file in temp_files:", + " if os.path.exists(file):", + " os.remove(file)", + " print(f\"\u2713 Removed {file}\")", + "", + "print(\"\\n=== Validation Complete ===\")", + "print(\"All Python code examples from REST API publishing guides have been tested.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/api-examples/publishing/typescript/validate-typescript-rest-api-example.nnb b/api-examples/publishing/typescript/validate-typescript-rest-api-example.nnb new file mode 100644 index 000000000..82f037693 --- /dev/null +++ b/api-examples/publishing/typescript/validate-typescript-rest-api-example.nnb @@ -0,0 +1,46 @@ +{ + "cells": [ + { + "language": "typescript", + "source": [ + "// Setup: Configuration and Types\n// Based on Fiddler REST API v3.0 specification\n\n// Environment configuration\nconst API_KEY = process.env.FIDDLER_API_KEY || \"\";\nconst FIDDLER_ENDPOINT = process.env.FIDDLER_ENDPOINT || \"\";\nconst MODEL_ID = process.env.MODEL_ID || \"\";\n\n// Type Definitions matching Fiddler API v3.0\ninterface FiddlerEvent {\n customer_id: string;\n creditscore: number;\n geography: string;\n gender: string;\n age: number;\n tenure: number;\n balance: number;\n numofproducts: number;\n hascrcard: number;\n isactivemember: number;\n estimatedsalary: number;\n predicted_churn: number;\n churn: string;\n timestamp: string;\n}\n\ninterface PublishPayload {\n model_id: string;\n env_type: \"PRODUCTION\" | \"PRE_PRODUCTION\";\n env_name?: string;\n source: FileSource | EventsSource;\n}\n\ninterface FileSource {\n type: \"FILE\";\n file_id: string;\n}\n\ninterface EventsSource {\n type: \"EVENTS\";\n events: FiddlerEvent[];\n}\n\n// API v3.0 Response Envelope\ninterface ApiResponse {\n api_version: string;\n kind: string;\n data: T;\n}\n\n// File Upload Response\ninterface FileUploadData {\n id: string;\n filename: string;\n type: string;\n status: string;\n created_at: string;\n updated_at?: string;\n}\n\n// Batch Publish Response\ninterface BatchPublishData {\n job: {\n id: string;\n name: string;\n };\n}\n\n// Streaming Publish Response\ninterface StreamingPublishData {\n source_type: string;\n event_ids: string[];\n}\n\nconsole.log(\"\u2713 Configuration and types loaded\");\nconsole.log(` Endpoint: ${FIDDLER_ENDPOINT}`);\nconsole.log(` Model ID: ${MODEL_ID}`);" + ], + "outputs": [] + }, + { + "language": "typescript", + "source": [ + "// Test 1: File Upload for Batch Publishing\n// Upload CSV file and get file_id for batch publish\n\nimport * as fs from 'fs';\nimport { Blob } from 'buffer';\n\n// Create test CSV with bank churn data\nconst csvContent = `customer_id,creditscore,geography,gender,age,tenure,balance,numofproducts,hascrcard,isactivemember,estimatedsalary,predicted_churn,churn,timestamp\n27c349a2,559,California,Male,52,2,0.0,1,1,0,129013.59,0.007447,no,2025-12-24T12:00:00Z\n27c35cee,482,California,Male,55,5,97318.25,1,0,1,78416.14,0.804852,yes,2025-12-25T12:01:00Z\n27c364f0,651,Florida,Female,46,4,89743.05,1,1,0,156425.57,0.012754,no,2025-12-23T12:02:00Z`;\n\nfs.writeFileSync('churn_events.csv', csvContent);\nconsole.log(\"\u2713 Created test CSV file\");\n\n// Upload file using native FormData (Node 18+)\nconst fileBuffer = fs.readFileSync('churn_events.csv');\nconst blob = new Blob([fileBuffer], { type: 'text/csv' });\nconst formData = new FormData();\nformData.append('file', blob, 'churn_events.csv');\n\nconst uploadResponse = await fetch(`${FIDDLER_ENDPOINT}/v3/files/upload`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${API_KEY}`\n },\n body: formData\n});\n\nif (!uploadResponse.ok) {\n const errorText = await uploadResponse.text();\n throw new Error(`File upload failed (${uploadResponse.status}): ${errorText}`);\n}\n\nconst uploadResult: ApiResponse = await uploadResponse.json();\nconst fileId = uploadResult.data.id;\n\nconsole.log(`\u2713 File uploaded successfully`);\nconsole.log(` File ID: ${fileId}`);\nconsole.log(` Filename: ${uploadResult.data.filename}`);\nconsole.log(` Status: ${uploadResult.data.status}`);" + ], + "outputs": [] + }, + { + "language": "typescript", + "source": [ + "// Test 2: Batch Publish Using Uploaded File\n// Publish events from the uploaded file (returns job_id)\n\nconst batchPayload: PublishPayload = {\n model_id: MODEL_ID,\n env_type: \"PRODUCTION\",\n source: {\n type: \"FILE\",\n file_id: fileId // From previous cell\n }\n};\n\nconst batchResponse = await fetch(`${FIDDLER_ENDPOINT}/v3/events`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${API_KEY}`\n },\n body: JSON.stringify(batchPayload)\n});\n\nif (!batchResponse.ok) {\n const errorText = await batchResponse.text();\n throw new Error(`Batch publish failed (${batchResponse.status}): ${errorText}`);\n}\n\n// Batch publish returns 202 Accepted with job ID\nif (batchResponse.status !== 202) {\n console.warn(`\u26a0\ufe0f Expected status 202, got ${batchResponse.status}`);\n}\n\nconst batchResult: ApiResponse = await batchResponse.json();\nconst jobId = batchResult.data.job.id;\n\nconsole.log(`\u2713 Batch publish started`);\nconsole.log(` Job ID: ${jobId}`);\nconsole.log(` Job Name: ${batchResult.data.job.name}`);" + ], + "outputs": [] + }, + { + "language": "typescript", + "source": [ + "// Test 3: Streaming Publish (Direct Events)\n// Publish events directly without file upload\n\nconst events: FiddlerEvent[] = [\n {\n customer_id: \"27c349a2\",\n creditscore: 559,\n geography: \"California\",\n gender: \"Male\",\n age: 52,\n tenure: 2,\n balance: 0.0,\n numofproducts: 1,\n hascrcard: 1,\n isactivemember: 0,\n estimatedsalary: 129013.59,\n predicted_churn: 0.007447,\n churn: \"no\",\n timestamp: new Date().toISOString()\n },\n {\n customer_id: \"27c35cee\",\n creditscore: 482,\n geography: \"California\",\n gender: \"Male\",\n age: 55,\n tenure: 5,\n balance: 97318.25,\n numofproducts: 1,\n hascrcard: 0,\n isactivemember: 1,\n estimatedsalary: 78416.14,\n predicted_churn: 0.804852,\n churn: \"yes\",\n timestamp: new Date().toISOString()\n }\n];\n\nconst streamingPayload: PublishPayload = {\n model_id: MODEL_ID,\n env_type: \"PRODUCTION\",\n source: {\n type: \"EVENTS\",\n events: events\n }\n};\n\nconst streamingResponse = await fetch(`${FIDDLER_ENDPOINT}/v3/events`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${API_KEY}`\n },\n body: JSON.stringify(streamingPayload)\n});\n\nif (!streamingResponse.ok) {\n const errorText = await streamingResponse.text();\n throw new Error(`Streaming publish failed (${streamingResponse.status}): ${errorText}`);\n}\n\n// Streaming publish returns 202 Accepted (like batch publish)\nif (streamingResponse.status !== 202) {\n console.warn(`\u26a0\ufe0f Expected status 202, got ${streamingResponse.status}`);\n}\n\nconst streamingResult: ApiResponse = await streamingResponse.json();\nconst eventIds = streamingResult.data.event_ids;\n\nconsole.log(`\u2713 Streaming publish completed`);\nconsole.log(` Published ${eventIds.length} event(s)`);\nconsole.log(` Event IDs: ${eventIds.join(', ')}`);" + ], + "outputs": [] + }, + { + "language": "typescript", + "source": [ + "// Test 4: Update Existing Events\n// Update events using PATCH\n//\n// IMPORTANT: Only PRODUCTION events can be updated.\n// Non-production events (PRE_PRODUCTION) are immutable and cannot be\n// modified via batch OR streaming processes.\n\nconst updates: Partial[] = [\n {\n customer_id: \"27c349a2\",\n churn: \"yes\" // Update prediction\n }\n];\n\nconst updatePayload = {\n model_id: MODEL_ID,\n env_type: \"PRODUCTION\",\n source: {\n type: \"EVENTS\",\n events: updates\n }\n};\n\nconst updateResponse = await fetch(`${FIDDLER_ENDPOINT}/v3/events`, {\n method: 'PATCH',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${API_KEY}`\n },\n body: JSON.stringify(updatePayload)\n});\n\nif (!updateResponse.ok) {\n const errorText = await updateResponse.text();\n throw new Error(`Update failed (${updateResponse.status}): ${errorText}`);\n}\n\nconsole.log(`\u2713 Update completed`);\nconsole.log(` Status: ${updateResponse.status}`);\n\n// Clean up test file\nfs.unlinkSync('churn_events.csv');\nconsole.log(`\\n\u2713 Cleanup completed - removed test CSV file`);" + ], + "outputs": [] + }, + { + "language": "typescript", + "source": [ + "// Production-Ready Publisher Class\n// Complete implementation with retry logic and error handling\n\nclass FiddlerPublisher {\n private apiKey: string;\n private endpoint: string;\n private maxRetries: number;\n private initialDelay: number;\n private backoffMultiplier: number;\n\n constructor(\n apiKey: string,\n endpoint: string,\n options: {\n maxRetries?: number;\n initialDelay?: number;\n backoffMultiplier?: number;\n } = {}\n ) {\n this.apiKey = apiKey;\n this.endpoint = endpoint;\n this.maxRetries = options.maxRetries ?? 3;\n this.initialDelay = options.initialDelay ?? 1000;\n this.backoffMultiplier = options.backoffMultiplier ?? 2;\n }\n\n private async delay(ms: number): Promise {\n return new Promise(resolve => setTimeout(resolve, ms));\n }\n\n private calculateBackoff(attempt: number): number {\n return this.initialDelay * Math.pow(this.backoffMultiplier, attempt);\n }\n\n private isRetryable(status: number): boolean {\n return [429, 500, 502, 503, 504].includes(status);\n }\n\n async publishEventsWithRetry(\n modelId: string,\n events: FiddlerEvent[],\n envType: \"PRODUCTION\" | \"PRE_PRODUCTION\" = \"PRODUCTION\"\n ): Promise {\n if (events.length === 0) {\n throw new Error(\"Events array cannot be empty\");\n }\n\n const payload: PublishPayload = {\n model_id: modelId,\n env_type: envType,\n source: {\n type: \"EVENTS\",\n events: events\n }\n };\n\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n try {\n // Add delay for retries\n if (attempt > 0) {\n const delayMs = this.calculateBackoff(attempt - 1);\n console.log(`Retry attempt ${attempt}/${this.maxRetries} after ${delayMs}ms`);\n await this.delay(delayMs);\n }\n\n const headers: Record = {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.apiKey}`\n };\n\n if (attempt > 0) {\n headers['X-Fiddler-Client-Retry'] = 'true';\n }\n\n const response = await fetch(`${this.endpoint}/v3/events`, {\n method: 'POST',\n headers: headers,\n body: JSON.stringify(payload)\n });\n\n // Streaming returns 202 Accepted (both batch and streaming use 202)\n if (response.status === 200 || response.status === 202) {\n const result: ApiResponse = await response.json();\n if (attempt > 0) {\n console.log(`\u2713 Succeeded on retry attempt ${attempt}`);\n }\n return result.data.event_ids;\n }\n\n // Check if error is retryable\n if (!this.isRetryable(response.status)) {\n const errorText = await response.text();\n throw new Error(\n `Non-retryable error (${response.status}): ${errorText}`\n );\n }\n\n const errorText = await response.text();\n lastError = new Error(`HTTP ${response.status}: ${errorText}`);\n\n } catch (error) {\n if (error instanceof Error && !error.message.includes('Non-retryable')) {\n lastError = error;\n if (attempt === this.maxRetries) {\n break;\n }\n } else {\n throw error;\n }\n }\n }\n\n throw new Error(\n `Max retries (${this.maxRetries}) exceeded. Last error: ${lastError?.message}`\n );\n }\n}\n\nconsole.log(\"\u2713 FiddlerPublisher class defined\");\n\n// Test the production publisher\nconst publisher = new FiddlerPublisher(API_KEY, FIDDLER_ENDPOINT, {\n maxRetries: 2,\n initialDelay: 1000\n});\n\nconst testEvents: FiddlerEvent[] = [\n {\n customer_id: \"test1_\" + Date.now(),\n creditscore: 650,\n geography: \"New York\",\n gender: \"Female\",\n age: 31,\n tenure: 8,\n balance: 50000.1,\n numofproducts: 1,\n hascrcard: 1,\n isactivemember: 1,\n estimatedsalary: 95000.00,\n predicted_churn: 0.15,\n churn: \"no\",\n timestamp: new Date().toISOString()\n }\n];\n\nconst publishedIds = await publisher.publishEventsWithRetry(MODEL_ID, testEvents);\nconsole.log(`\\n\u2713 Production publisher test completed`);\nconsole.log(` Published ${publishedIds.length} event(s)`);\nconsole.log(` Event IDs: ${publishedIds.join(', ')}`);" + ], + "outputs": [] + } + ] +} \ No newline at end of file From ff2a48118b6cb3d9401363ea4c4d9922fb253610 Mon Sep 17 00:00:00 2001 From: dbasilfid <117305439+dbasilfid@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:57:41 -0500 Subject: [PATCH 2/2] Clear notebook outputs --- .../python/validate-rest-api-examples.ipynb | 748 ++++++++---------- 1 file changed, 341 insertions(+), 407 deletions(-) diff --git a/api-examples/publishing/python/validate-rest-api-examples.ipynb b/api-examples/publishing/python/validate-rest-api-examples.ipynb index dc4f4c7e9..1c569378f 100644 --- a/api-examples/publishing/python/validate-rest-api-examples.ipynb +++ b/api-examples/publishing/python/validate-rest-api-examples.ipynb @@ -53,55 +53,44 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u2713 Environment configured\n", - " Endpoint: https://preprod.cloud.fiddler.ai\n", - " Model ID: 811fcb34-fb3e-458d-a81f-b4ecd17ec1fd\n", - "\u2713 Created test CSV file: churn_events.csv\n" - ] - } - ], + "outputs": [], "source": [ - "import json", - "import os", - "from datetime import datetime, timezone", - "from typing import Dict, List", - "", - "import requests", - "", - "# Load environment variables", - "api_key = os.environ.get(\"FIDDLER_API_KEY\")", - "fiddler_endpoint = os.environ.get(\"FIDDLER_ENDPOINT\")", - "model_id = os.environ.get(\"MODEL_ID\")", - "", - "# Validate environment", - "if not all([api_key, fiddler_endpoint, model_id]):", - " raise ValueError(", - " \"Missing required environment variables. Set: \"", - " \"FIDDLER_API_KEY, FIDDLER_ENDPOINT, MODEL_ID\"", - " )", - "", - "print(\"\u2713 Environment configured\")", - "print(f\" Endpoint: {fiddler_endpoint}\")", - "print(f\" Model ID: {model_id}\")", - "", - "# Create test CSV file for batch publishing with bank churn schema", - "csv_content = \"\"\"customer_id,creditscore,geography,gender,age,tenure,balance,numofproducts,hascrcard,isactivemember,estimatedsalary,predicted_churn,churn,timestamp", - "27c349a2,559,California,Male,52,2,0.0,1,1,0,129013.59,0.007447,no,2025-12-28T12:00:00Z", - "27c35cee,482,California,Male,55,5,97318.25,1,0,1,78416.14,0.804852,yes,2025-12-27T12:01:00Z", - "27c364f0,651,Florida,Female,46,4,89743.05,1,1,0,156425.57,0.012754,no,2025-12-29T12:02:00Z", - "\"\"\"", - "", - "with open(\"churn_events.csv\", \"w\") as f:", - " f.write(csv_content)", - "", - "print(\"\u2713 Created test CSV file: churn_events.csv\")" + "import json\n", + "import os\n", + "from datetime import datetime, timezone\n", + "from typing import Dict, List\n", + "\n", + "import requests\n", + "\n", + "# Load environment variables\n", + "api_key = os.environ.get(\"FIDDLER_API_KEY\")\n", + "fiddler_endpoint = os.environ.get(\"FIDDLER_ENDPOINT\")\n", + "model_id = os.environ.get(\"MODEL_ID\")\n", + "\n", + "# Validate environment\n", + "if not all([api_key, fiddler_endpoint, model_id]):\n", + " raise ValueError(\n", + " \"Missing required environment variables. Set: \"\n", + " \"FIDDLER_API_KEY, FIDDLER_ENDPOINT, MODEL_ID\"\n", + " )\n", + "\n", + "print(\"✓ Environment configured\")\n", + "print(f\" Endpoint: {fiddler_endpoint}\")\n", + "print(f\" Model ID: {model_id}\")\n", + "\n", + "# Create test CSV file for batch publishing with bank churn schema\n", + "csv_content = \"\"\"customer_id,creditscore,geography,gender,age,tenure,balance,numofproducts,hascrcard,isactivemember,estimatedsalary,predicted_churn,churn,timestamp\n", + "27c349a2,559,California,Male,52,2,0.0,1,1,0,129013.59,0.007447,no,2025-12-28T12:00:00Z\n", + "27c35cee,482,California,Male,55,5,97318.25,1,0,1,78416.14,0.804852,yes,2025-12-27T12:01:00Z\n", + "27c364f0,651,Florida,Female,46,4,89743.05,1,1,0,156425.57,0.012754,no,2025-12-29T12:02:00Z\n", + "\"\"\"\n", + "\n", + "with open(\"churn_events.csv\", \"w\") as f:\n", + " f.write(csv_content)\n", + "\n", + "print(\"✓ Created test CSV file: churn_events.csv\")" ] }, { @@ -125,31 +114,23 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u2713 File uploaded. File ID: 71573cc3-e29d-47ea-9ebc-3987312201ad\n" - ] - } - ], + "outputs": [], "source": [ - "# Upload file", - "with open(\"churn_events.csv\", \"rb\") as f:", - " response = requests.post(", - " f\"{fiddler_endpoint}/v3/files/upload\",", - " headers={\"Authorization\": f\"Bearer {api_key}\"},", - " files={\"file\": f}", - " )", - "", - "file_id = response.json()[\"data\"][\"id\"]", - "print(f\"\u2713 File uploaded. File ID: {file_id}\")", - "", - "# Save for next step", - "with open(\"file_id.txt\", \"w\") as f:", + "# Upload file\n", + "with open(\"churn_events.csv\", \"rb\") as f:\n", + " response = requests.post(\n", + " f\"{fiddler_endpoint}/v3/files/upload\",\n", + " headers={\"Authorization\": f\"Bearer {api_key}\"},\n", + " files={\"file\": f}\n", + " )\n", + "\n", + "file_id = response.json()[\"data\"][\"id\"]\n", + "print(f\"✓ File uploaded. File ID: {file_id}\")\n", + "\n", + "# Save for next step\n", + "with open(\"file_id.txt\", \"w\") as f:\n", " f.write(file_id)" ] }, @@ -164,42 +145,34 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u2713 Batch publish started. Job ID: cc2d9811-ff97-4527-930b-1999b22c0e1a\n" - ] - } - ], + "outputs": [], "source": [ - "# Read file_id from previous upload", - "with open(\"file_id.txt\", \"r\") as f:", - " file_id = f.read().strip()", - "", - "payload = {", - " \"model_id\": model_id,", - " \"env_type\": \"PRODUCTION\",", - " \"source\": {", - " \"type\": \"FILE\",", - " \"file_id\": file_id", - " }", - "}", - "", - "response = requests.post(", - " f\"{fiddler_endpoint}/v3/events\",", - " headers={\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {api_key}\"},", - " data=json.dumps(payload)", - ")", - "", - "job_id = response.json()[\"data\"][\"job\"][\"id\"]", - "print(f\"\u2713 Batch publish started. Job ID: {job_id}\")", - "", - "# Save for reference", - "with open(\"job_id.txt\", \"w\") as f:", + "# Read file_id from previous upload\n", + "with open(\"file_id.txt\", \"r\") as f:\n", + " file_id = f.read().strip()\n", + "\n", + "payload = {\n", + " \"model_id\": model_id,\n", + " \"env_type\": \"PRODUCTION\",\n", + " \"source\": {\n", + " \"type\": \"FILE\",\n", + " \"file_id\": file_id\n", + " }\n", + "}\n", + "\n", + "response = requests.post(\n", + " f\"{fiddler_endpoint}/v3/events\",\n", + " headers={\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {api_key}\"},\n", + " data=json.dumps(payload)\n", + ")\n", + "\n", + "job_id = response.json()[\"data\"][\"job\"][\"id\"]\n", + "print(f\"✓ Batch publish started. Job ID: {job_id}\")\n", + "\n", + "# Save for reference\n", + "with open(\"job_id.txt\", \"w\") as f:\n", " f.write(job_id)" ] }, @@ -214,57 +187,49 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u2713 Streaming publish completed. Published 1 event(s)\n" - ] - } - ], + "outputs": [], "source": [ - "events = [", - " {", - " \"customer_id\": \"27c349a2\",", - " \"creditscore\": 559,", - " \"geography\": \"California\",", - " \"gender\": \"Male\",", - " \"age\": 52,", - " \"tenure\": 2,", - " \"balance\": 0.0,", - " \"numofproducts\": 1,", - " \"hascrcard\": 1,", - " \"isactivemember\": 0,", - " \"estimatedsalary\": 129013.59,", - " \"predicted_churn\": 0.007447,", - " \"churn\": \"no\",", - " \"timestamp\": datetime.now(timezone.utc).isoformat()", - " }", - "]", - "", - "payload = {", - " \"model_id\": model_id,", - " \"env_type\": \"PRODUCTION\",", - " \"source\": {", - " \"type\": \"EVENTS\",", - " \"events\": events", - " }", - "}", - "", - "response = requests.post(", - " f\"{fiddler_endpoint}/v3/events\",", - " headers={\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {api_key}\"},", - " data=json.dumps(payload)", - ")", - "", - "event_ids = response.json()[\"data\"][\"event_ids\"]", - "print(f\"\u2713 Streaming publish completed. Published {len(event_ids)} event(s)\")", - "", - "# Save first event_id for updates", - "with open(\"event_id.txt\", \"w\") as f:", + "events = [\n", + " {\n", + " \"customer_id\": \"27c349a2\",\n", + " \"creditscore\": 559,\n", + " \"geography\": \"California\",\n", + " \"gender\": \"Male\",\n", + " \"age\": 52,\n", + " \"tenure\": 2,\n", + " \"balance\": 0.0,\n", + " \"numofproducts\": 1,\n", + " \"hascrcard\": 1,\n", + " \"isactivemember\": 0,\n", + " \"estimatedsalary\": 129013.59,\n", + " \"predicted_churn\": 0.007447,\n", + " \"churn\": \"no\",\n", + " \"timestamp\": datetime.now(timezone.utc).isoformat()\n", + " }\n", + "]\n", + "\n", + "payload = {\n", + " \"model_id\": model_id,\n", + " \"env_type\": \"PRODUCTION\",\n", + " \"source\": {\n", + " \"type\": \"EVENTS\",\n", + " \"events\": events\n", + " }\n", + "}\n", + "\n", + "response = requests.post(\n", + " f\"{fiddler_endpoint}/v3/events\",\n", + " headers={\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {api_key}\"},\n", + " data=json.dumps(payload)\n", + ")\n", + "\n", + "event_ids = response.json()[\"data\"][\"event_ids\"]\n", + "print(f\"✓ Streaming publish completed. Published {len(event_ids)} event(s)\")\n", + "\n", + "# Save first event_id for updates\n", + "with open(\"event_id.txt\", \"w\") as f:\n", " f.write(event_ids[0])" ] }, @@ -281,41 +246,33 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u2713 Update completed. Response status: 202\n" - ] - } - ], + "outputs": [], "source": [ - "updates = [", - " {", - " \"customer_id\": \"27c349a2\",", - " \"churn\": \"yes\"", - " }", - "]", - "", - "payload = {", - " \"model_id\": model_id,", - " \"env_type\": \"PRODUCTION\",", - " \"source\": {", - " \"type\": \"EVENTS\",", - " \"events\": updates", - " }", - "}", - "", - "response = requests.patch(", - " f\"{fiddler_endpoint}/v3/events\",", - " headers={\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {api_key}\"},", - " data=json.dumps(payload)", - ")", - "", - "print(f\"\u2713 Update completed. Response status: {response.status_code}\")" + "updates = [\n", + " {\n", + " \"customer_id\": \"27c349a2\",\n", + " \"churn\": \"yes\"\n", + " }\n", + "]\n", + "\n", + "payload = {\n", + " \"model_id\": model_id,\n", + " \"env_type\": \"PRODUCTION\",\n", + " \"source\": {\n", + " \"type\": \"EVENTS\",\n", + " \"events\": updates\n", + " }\n", + "}\n", + "\n", + "response = requests.patch(\n", + " f\"{fiddler_endpoint}/v3/events\",\n", + " headers={\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {api_key}\"},\n", + " data=json.dumps(payload)\n", + ")\n", + "\n", + "print(f\"✓ Update completed. Response status: {response.status_code}\")" ] }, { @@ -341,196 +298,187 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u2713 ProductionStreamingPublisher class defined\n", - "\u2713 Successfully published 2 events with ProductionStreamingPublisher\n" - ] - } - ], + "outputs": [], "source": [ - "from typing import Optional", - "", - "", - "class ProductionStreamingPublisher:", - " def __init__(", - " self,", - " api_key: str,", - " endpoint: str,", - " max_retries: int = 3,", - " initial_delay: float = 1.0,", - " backoff_multiplier: float = 2.0", - " ):", - " self.api_key = api_key", - " self.endpoint = endpoint", - " self.max_retries = max_retries", - " self.initial_delay = initial_delay", - " self.backoff_multiplier = backoff_multiplier", - "", - " def _get_headers(self, is_retry: bool = False) -> Dict[str, str]:", - " headers = {", - " \"Content-Type\": \"application/json\",", - " \"Authorization\": f\"Bearer {self.api_key}\"", - " }", - " if is_retry:", - " headers[\"X-Fiddler-Client-Retry\"] = \"true\"", - " return headers", - "", - " def _calculate_backoff(self, attempt: int) -> float:", - " return self.initial_delay * (self.backoff_multiplier ** attempt)", - "", - " def _is_retryable(self, status_code: int) -> bool:", - " return status_code in [429, 500, 502, 503, 504]", - "", - " def publish_events_with_retry(", - " self,", - " model_id: str,", - " events: List[Dict]", - " ) -> Optional[List[str]]:", - " \"\"\"Publish events with exponential backoff retry logic.\"\"\"", - "", - " if not events:", - " raise ValueError(\"Events list cannot be empty\")", - "", - " if len(events) > 1000:", - " raise ValueError(f\"Batch size {len(events)} exceeds maximum 1000\")", - "", - " payload = {", - " \"model_id\": model_id,", - " \"env_type\": \"PRODUCTION\",", - " \"source\": {", - " \"type\": \"EVENTS\",", - " \"events\": events", - " }", - " }", - "", - " last_error = None", - "", - " for attempt in range(self.max_retries + 1):", - " try:", - " # Add delay and retry header for retries", - " if attempt > 0:", - " delay = self._calculate_backoff(attempt - 1)", - " print(f\"Retry attempt {attempt}/{self.max_retries} after {delay:.1f}s\")", - " time.sleep(delay)", - "", - " headers = self._get_headers(is_retry=attempt > 0)", - "", - " response = requests.post(", - " f\"{self.endpoint}/v3/events\",", - " headers=headers,", - " data=json.dumps(payload),", - " timeout=30", - " )", - "", - " # Streaming returns 202 Accepted (both batch and streaming use 202)", - " if response.status_code == 200 or response.status_code == 202:", - " result = response.json()", - " if attempt > 0:", - " print(f\"\u2713 Succeeded on retry attempt {attempt}\")", - " return result[\"data\"].get(\"event_ids\", [])", - "", - " # Check if retryable", - " if not self._is_retryable(response.status_code):", - " error_body = response.json()", - " raise Exception(", - " f\"Non-retryable error ({response.status_code}): \"", - " f\"{error_body.get('message', 'Unknown error')}\"", - " )", - "", - " error_body = response.json()", - " last_error = Exception(", - " f\"HTTP {response.status_code}: \"", - " f\"{error_body.get('message', 'Unknown error')}\"", - " )", - "", - " except requests.exceptions.RequestException as e:", - " last_error = e", - " # Network errors are retryable", - " if attempt == self.max_retries:", - " break", - "", - " raise Exception(", - " f\"Max retries ({self.max_retries}) exceeded. \"", - " f\"Last error: {str(last_error)}\"", - " )", - "", - " def publish_batch(", - " self,", - " model_id: str,", - " events: List[Dict],", - " batch_size: int = 1000", - " ) -> List[str]:", - " \"\"\"Publish large event lists in batches with retry.\"\"\"", - " all_event_ids = []", - "", - " for i in range(0, len(events), batch_size):", - " batch = events[i:i + batch_size]", - " print(f\"Publishing batch {i // batch_size + 1} ({len(batch)} events)\")", - "", - " event_ids = self.publish_events_with_retry(model_id, batch)", - " all_event_ids.extend(event_ids)", - "", - " print(f\"\u2713 Published {len(event_ids)} events\")", - "", - " return all_event_ids", - "", - "print(\"\u2713 ProductionStreamingPublisher class defined\")", - "", - "# Test the class with sample events", - "publisher = ProductionStreamingPublisher(", - " api_key=api_key,", - " endpoint=fiddler_endpoint,", - " max_retries=2,", - " initial_delay=1.0", - ")", - "", - "# Create test events with bank churn schema", - "test_events = [", - " {", - " \"customer_id\": \"27c349a2\",", - " \"creditscore\": 559,", - " \"geography\": \"California\",", - " \"gender\": \"Male\",", - " \"age\": 52,", - " \"tenure\": 2,", - " \"balance\": 0.0,", - " \"numofproducts\": 1,", - " \"hascrcard\": 1,", - " \"isactivemember\": 0,", - " \"estimatedsalary\": 129013.59,", - " \"predicted_churn\": 0.007447,", - " \"churn\": \"no\",", - " \"timestamp\": datetime.now(timezone.utc).isoformat()", - " },", - " {", - " \"customer_id\": \"27c35cee\",", - " \"creditscore\": 482,", - " \"geography\": \"California\",", - " \"gender\": \"Male\",", - " \"age\": 55,", - " \"tenure\": 5,", - " \"balance\": 97318.25,", - " \"numofproducts\": 1,", - " \"hascrcard\": 0,", - " \"isactivemember\": 1,", - " \"estimatedsalary\": 78416.14,", - " \"predicted_churn\": 0.804852,", - " \"churn\": \"yes\",", - " \"timestamp\": datetime.now(timezone.utc).isoformat()", - " }", - "]", - "", - "try:", - " event_ids = publisher.publish_events_with_retry(", - " model_id=model_id,", - " events=test_events", - " )", - " print(f\"\u2713 Successfully published {len(event_ids)} events with ProductionStreamingPublisher\")", - "except Exception as e:", + "from typing import Optional\n", + "\n", + "\n", + "class ProductionStreamingPublisher:\n", + " def __init__(\n", + " self,\n", + " api_key: str,\n", + " endpoint: str,\n", + " max_retries: int = 3,\n", + " initial_delay: float = 1.0,\n", + " backoff_multiplier: float = 2.0\n", + " ):\n", + " self.api_key = api_key\n", + " self.endpoint = endpoint\n", + " self.max_retries = max_retries\n", + " self.initial_delay = initial_delay\n", + " self.backoff_multiplier = backoff_multiplier\n", + "\n", + " def _get_headers(self, is_retry: bool = False) -> Dict[str, str]:\n", + " headers = {\n", + " \"Content-Type\": \"application/json\",\n", + " \"Authorization\": f\"Bearer {self.api_key}\"\n", + " }\n", + " if is_retry:\n", + " headers[\"X-Fiddler-Client-Retry\"] = \"true\"\n", + " return headers\n", + "\n", + " def _calculate_backoff(self, attempt: int) -> float:\n", + " return self.initial_delay * (self.backoff_multiplier ** attempt)\n", + "\n", + " def _is_retryable(self, status_code: int) -> bool:\n", + " return status_code in [429, 500, 502, 503, 504]\n", + "\n", + " def publish_events_with_retry(\n", + " self,\n", + " model_id: str,\n", + " events: List[Dict]\n", + " ) -> Optional[List[str]]:\n", + " \"\"\"Publish events with exponential backoff retry logic.\"\"\"\n", + "\n", + " if not events:\n", + " raise ValueError(\"Events list cannot be empty\")\n", + "\n", + " if len(events) > 1000:\n", + " raise ValueError(f\"Batch size {len(events)} exceeds maximum 1000\")\n", + "\n", + " payload = {\n", + " \"model_id\": model_id,\n", + " \"env_type\": \"PRODUCTION\",\n", + " \"source\": {\n", + " \"type\": \"EVENTS\",\n", + " \"events\": events\n", + " }\n", + " }\n", + "\n", + " last_error = None\n", + "\n", + " for attempt in range(self.max_retries + 1):\n", + " try:\n", + " # Add delay and retry header for retries\n", + " if attempt > 0:\n", + " delay = self._calculate_backoff(attempt - 1)\n", + " print(f\"Retry attempt {attempt}/{self.max_retries} after {delay:.1f}s\")\n", + " time.sleep(delay)\n", + "\n", + " headers = self._get_headers(is_retry=attempt > 0)\n", + "\n", + " response = requests.post(\n", + " f\"{self.endpoint}/v3/events\",\n", + " headers=headers,\n", + " data=json.dumps(payload),\n", + " timeout=30\n", + " )\n", + "\n", + " # Streaming returns 202 Accepted (both batch and streaming use 202)\n", + " if response.status_code == 200 or response.status_code == 202:\n", + " result = response.json()\n", + " if attempt > 0:\n", + " print(f\"✓ Succeeded on retry attempt {attempt}\")\n", + " return result[\"data\"].get(\"event_ids\", [])\n", + "\n", + " # Check if retryable\n", + " if not self._is_retryable(response.status_code):\n", + " error_body = response.json()\n", + " raise Exception(\n", + " f\"Non-retryable error ({response.status_code}): \"\n", + " f\"{error_body.get('message', 'Unknown error')}\"\n", + " )\n", + "\n", + " error_body = response.json()\n", + " last_error = Exception(\n", + " f\"HTTP {response.status_code}: \"\n", + " f\"{error_body.get('message', 'Unknown error')}\"\n", + " )\n", + "\n", + " except requests.exceptions.RequestException as e:\n", + " last_error = e\n", + " # Network errors are retryable\n", + " if attempt == self.max_retries:\n", + " break\n", + "\n", + " raise Exception(\n", + " f\"Max retries ({self.max_retries}) exceeded. \"\n", + " f\"Last error: {str(last_error)}\"\n", + " )\n", + "\n", + " def publish_batch(\n", + " self,\n", + " model_id: str,\n", + " events: List[Dict],\n", + " batch_size: int = 1000\n", + " ) -> List[str]:\n", + " \"\"\"Publish large event lists in batches with retry.\"\"\"\n", + " all_event_ids = []\n", + "\n", + " for i in range(0, len(events), batch_size):\n", + " batch = events[i:i + batch_size]\n", + " print(f\"Publishing batch {i // batch_size + 1} ({len(batch)} events)\")\n", + "\n", + " event_ids = self.publish_events_with_retry(model_id, batch)\n", + " all_event_ids.extend(event_ids)\n", + "\n", + " print(f\"✓ Published {len(event_ids)} events\")\n", + "\n", + " return all_event_ids\n", + "\n", + "print(\"✓ ProductionStreamingPublisher class defined\")\n", + "\n", + "# Test the class with sample events\n", + "publisher = ProductionStreamingPublisher(\n", + " api_key=api_key,\n", + " endpoint=fiddler_endpoint,\n", + " max_retries=2,\n", + " initial_delay=1.0\n", + ")\n", + "\n", + "# Create test events with bank churn schema\n", + "test_events = [\n", + " {\n", + " \"customer_id\": \"27c349a2\",\n", + " \"creditscore\": 559,\n", + " \"geography\": \"California\",\n", + " \"gender\": \"Male\",\n", + " \"age\": 52,\n", + " \"tenure\": 2,\n", + " \"balance\": 0.0,\n", + " \"numofproducts\": 1,\n", + " \"hascrcard\": 1,\n", + " \"isactivemember\": 0,\n", + " \"estimatedsalary\": 129013.59,\n", + " \"predicted_churn\": 0.007447,\n", + " \"churn\": \"no\",\n", + " \"timestamp\": datetime.now(timezone.utc).isoformat()\n", + " },\n", + " {\n", + " \"customer_id\": \"27c35cee\",\n", + " \"creditscore\": 482,\n", + " \"geography\": \"California\",\n", + " \"gender\": \"Male\",\n", + " \"age\": 55,\n", + " \"tenure\": 5,\n", + " \"balance\": 97318.25,\n", + " \"numofproducts\": 1,\n", + " \"hascrcard\": 0,\n", + " \"isactivemember\": 1,\n", + " \"estimatedsalary\": 78416.14,\n", + " \"predicted_churn\": 0.804852,\n", + " \"churn\": \"yes\",\n", + " \"timestamp\": datetime.now(timezone.utc).isoformat()\n", + " }\n", + "]\n", + "\n", + "try:\n", + " event_ids = publisher.publish_events_with_retry(\n", + " model_id=model_id,\n", + " events=test_events\n", + " )\n", + " print(f\"✓ Successfully published {len(event_ids)} events with ProductionStreamingPublisher\")\n", + "except Exception as e:\n", " print(f\"Publishing test: {e}\")" ] }, @@ -546,39 +494,25 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u2713 Removed churn_events.csv\n", - "\u2713 Removed file_id.txt\n", - "\u2713 Removed job_id.txt\n", - "\u2713 Removed event_id.txt\n", - "\n", - "=== Validation Complete ===\n", - "All Python code examples from REST API publishing guides have been tested.\n" - ] - } - ], + "outputs": [], "source": [ - "import os", - "", - "temp_files = [", - " \"churn_events.csv\",", - " \"file_id.txt\",", - " \"job_id.txt\",", - " \"event_id.txt\"", - "]", - "", - "for file in temp_files:", - " if os.path.exists(file):", - " os.remove(file)", - " print(f\"\u2713 Removed {file}\")", - "", - "print(\"\\n=== Validation Complete ===\")", + "import os\n", + "\n", + "temp_files = [\n", + " \"churn_events.csv\",\n", + " \"file_id.txt\",\n", + " \"job_id.txt\",\n", + " \"event_id.txt\"\n", + "]\n", + "\n", + "for file in temp_files:\n", + " if os.path.exists(file):\n", + " os.remove(file)\n", + " print(f\"✓ Removed {file}\")\n", + "\n", + "print(\"\\n=== Validation Complete ===\")\n", "print(\"All Python code examples from REST API publishing guides have been tested.\")" ] } @@ -604,4 +538,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +}