Spaces:
Sleeping
Sleeping
| """ | |
| Educational API for Teaching HTTP Methods & Response Formats | |
| ============================================================ | |
| A simple FastAPI application designed for teaching REST API concepts. | |
| Endpoints cover: | |
| - HTTP Methods: GET, POST, PUT, DELETE, PATCH | |
| - Response Formats: JSON, XML, Plain Text, HTML | |
| - Query Parameters, Path Parameters, Request Bodies | |
| - Headers, Status Codes, and more | |
| """ | |
| from fastapi import FastAPI, Query, Path, Body, Header, Response, HTTPException, Request | |
| from fastapi.responses import PlainTextResponse, HTMLResponse, JSONResponse | |
| from pydantic import BaseModel, Field | |
| from typing import Optional, List | |
| from datetime import datetime | |
| import json | |
| app = FastAPI( | |
| title="API Teaching Tool", | |
| description="Educational API for learning HTTP methods, parameters, and response formats", | |
| version="1.0.0", | |
| ) | |
| # ============================================================================ | |
| # DATA MODELS | |
| # ============================================================================ | |
| class Item(BaseModel): | |
| """A simple item model for POST/PUT examples""" | |
| name: str = Field(..., example="Laptop") | |
| price: float = Field(..., example=999.99) | |
| quantity: int = Field(default=1, example=5) | |
| description: Optional[str] = Field(default=None, example="A powerful laptop") | |
| class User(BaseModel): | |
| """User model for registration examples""" | |
| username: str = Field(..., example="john_doe") | |
| email: str = Field(..., example="john@example.com") | |
| age: Optional[int] = Field(default=None, example=25) | |
| class Message(BaseModel): | |
| """Simple message model""" | |
| text: str = Field(..., example="Hello, World!") | |
| # In-memory storage for demo purposes | |
| items_db = { | |
| 1: {"id": 1, "name": "Apple", "price": 1.50, "quantity": 100, "description": "Fresh red apple"}, | |
| 2: {"id": 2, "name": "Banana", "price": 0.75, "quantity": 150, "description": "Yellow banana"}, | |
| 3: {"id": 3, "name": "Orange", "price": 2.00, "quantity": 80, "description": "Juicy orange"}, | |
| } | |
| next_id = 4 | |
| # ============================================================================ | |
| # ROOT & INFO ENDPOINTS | |
| # ============================================================================ | |
| def api_info(): | |
| """API overview endpoint""" | |
| return { | |
| "message": "Welcome to the API Teaching Tool!", | |
| "ui": "/", | |
| "documentation": "/docs", | |
| "endpoints": { | |
| "GET examples": ["/hello", "/items", "/items/{id}", "/greet"], | |
| "POST examples": ["/items", "/echo", "/users"], | |
| "PUT examples": ["/items/{id}"], | |
| "DELETE examples": ["/items/{id}"], | |
| "PATCH examples": ["/items/{id}"], | |
| "Response formats": ["/format/json", "/format/text", "/format/html", "/format/xml"], | |
| "Educational": ["/echo", "/headers", "/status/{code}"], | |
| } | |
| } | |
| def ui(): | |
| """Interactive API Explorer UI""" | |
| return """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>API Explorer</title> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif; | |
| background: #f5f5f7; | |
| min-height: 100vh; | |
| color: #1d1d1f; | |
| font-size: 14px; | |
| line-height: 1.5; | |
| } | |
| .container { max-width: 1400px; margin: 0 auto; padding: 20px; } | |
| header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 16px 0; | |
| border-bottom: 1px solid #d2d2d7; | |
| margin-bottom: 24px; | |
| } | |
| h1 { font-size: 21px; font-weight: 600; } | |
| .header-links a { | |
| color: #06c; | |
| text-decoration: none; | |
| margin-left: 24px; | |
| font-size: 13px; | |
| } | |
| .header-links a:hover { text-decoration: underline; } | |
| /* Request Bar */ | |
| .request-bar { | |
| display: flex; | |
| gap: 8px; | |
| background: #fff; | |
| padding: 8px; | |
| border-radius: 10px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.08); | |
| margin-bottom: 20px; | |
| } | |
| select, input, textarea, button { | |
| font-family: inherit; | |
| font-size: 14px; | |
| border: none; | |
| outline: none; | |
| } | |
| .method-select { | |
| padding: 10px 12px; | |
| background: #f5f5f7; | |
| border-radius: 6px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| color: #1d1d1f; | |
| } | |
| .url-input { | |
| flex: 1; | |
| padding: 10px 12px; | |
| background: #f5f5f7; | |
| border-radius: 6px; | |
| font-family: 'SF Mono', Menlo, monospace; | |
| } | |
| .send-btn { | |
| padding: 10px 24px; | |
| background: #007aff; | |
| color: #fff; | |
| font-weight: 500; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| } | |
| .send-btn:hover { background: #0056b3; } | |
| .send-btn:disabled { background: #86868b; cursor: not-allowed; } | |
| /* Copy dropdown */ | |
| .copy-dropdown { | |
| position: relative; | |
| } | |
| .copy-btn { | |
| padding: 10px 16px; | |
| background: #fff; | |
| border: 1px solid #d2d2d7; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-weight: 500; | |
| color: #1d1d1f; | |
| transition: all 0.2s; | |
| } | |
| .copy-btn:hover { background: #f5f5f7; border-color: #007aff; } | |
| .copy-menu { | |
| display: none; | |
| position: absolute; | |
| top: 100%; | |
| right: 0; | |
| margin-top: 4px; | |
| background: #fff; | |
| border: 1px solid #d2d2d7; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
| z-index: 100; | |
| min-width: 160px; | |
| overflow: hidden; | |
| } | |
| .copy-menu.show { display: block; } | |
| .copy-option { | |
| display: block; | |
| width: 100%; | |
| padding: 10px 14px; | |
| text-align: left; | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| font-size: 13px; | |
| color: #1d1d1f; | |
| transition: background 0.1s; | |
| } | |
| .copy-option:hover { background: #f5f5f7; } | |
| .copy-option-label { font-weight: 500; } | |
| .copy-option-desc { font-size: 11px; color: #86868b; } | |
| /* Toast notification */ | |
| .toast { | |
| position: fixed; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%) translateY(100px); | |
| background: #1d1d1f; | |
| color: #fff; | |
| padding: 12px 24px; | |
| border-radius: 8px; | |
| font-size: 13px; | |
| opacity: 0; | |
| transition: all 0.3s; | |
| z-index: 1000; | |
| } | |
| .toast.show { transform: translateX(-50%) translateY(0); opacity: 1; } | |
| /* Main Layout */ | |
| .main-layout { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| } | |
| @media (max-width: 1000px) { .main-layout { grid-template-columns: 1fr; } } | |
| .panel { | |
| background: #fff; | |
| border-radius: 10px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.08); | |
| overflow: hidden; | |
| } | |
| /* Tabs */ | |
| .tabs { | |
| display: flex; | |
| border-bottom: 1px solid #d2d2d7; | |
| background: #fafafa; | |
| } | |
| .tab { | |
| padding: 10px 16px; | |
| font-size: 12px; | |
| font-weight: 500; | |
| color: #86868b; | |
| cursor: pointer; | |
| border-bottom: 2px solid transparent; | |
| margin-bottom: -1px; | |
| background: none; | |
| transition: color 0.2s; | |
| } | |
| .tab:hover { color: #1d1d1f; } | |
| .tab.active { color: #007aff; border-bottom-color: #007aff; } | |
| .tab-content { display: none; } | |
| .tab-content.active { display: block; } | |
| .panel-body { padding: 16px; } | |
| textarea { | |
| width: 100%; | |
| min-height: 180px; | |
| padding: 12px; | |
| background: #f5f5f7; | |
| border-radius: 6px; | |
| resize: vertical; | |
| font-family: 'SF Mono', Menlo, monospace; | |
| font-size: 13px; | |
| line-height: 1.6; | |
| color: #1d1d1f; | |
| } | |
| /* Response Panel */ | |
| .status-bar { | |
| display: flex; | |
| gap: 20px; | |
| padding: 12px 16px; | |
| background: #fafafa; | |
| border-bottom: 1px solid #d2d2d7; | |
| font-size: 12px; | |
| } | |
| .status-item { color: #86868b; } | |
| .status-value { font-weight: 600; color: #1d1d1f; margin-left: 6px; } | |
| .status-ok { color: #34c759; } | |
| .status-err { color: #ff3b30; } | |
| .status-warn { color: #ff9500; } | |
| .response-content { | |
| padding: 16px; | |
| font-family: 'SF Mono', Menlo, monospace; | |
| font-size: 12px; | |
| line-height: 1.7; | |
| overflow-x: auto; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| max-height: 500px; | |
| overflow-y: auto; | |
| background: #fff; | |
| } | |
| /* Headers display */ | |
| .headers-list { | |
| font-family: 'SF Mono', Menlo, monospace; | |
| font-size: 12px; | |
| } | |
| .header-row { | |
| display: flex; | |
| padding: 6px 0; | |
| border-bottom: 1px solid #f0f0f0; | |
| } | |
| .header-row:last-child { border-bottom: none; } | |
| .header-name { | |
| width: 200px; | |
| flex-shrink: 0; | |
| font-weight: 600; | |
| color: #86868b; | |
| } | |
| .header-value { color: #1d1d1f; word-break: break-all; } | |
| /* Examples */ | |
| .examples-section { margin-top: 20px; } | |
| .examples-section h3 { | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: #86868b; | |
| margin-bottom: 12px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .examples-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); | |
| gap: 8px; | |
| } | |
| .example-btn { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 10px 12px; | |
| background: #fff; | |
| border: 1px solid #d2d2d7; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| text-align: left; | |
| transition: all 0.15s; | |
| } | |
| .example-btn:hover { | |
| border-color: #007aff; | |
| background: #f5f5f7; | |
| } | |
| .method-badge { | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-size: 10px; | |
| font-weight: 700; | |
| font-family: 'SF Mono', Menlo, monospace; | |
| } | |
| .method-GET { background: #e8f5e9; color: #2e7d32; } | |
| .method-POST { background: #fff3e0; color: #ef6c00; } | |
| .method-PUT { background: #e3f2fd; color: #1565c0; } | |
| .method-DELETE { background: #ffebee; color: #c62828; } | |
| .method-PATCH { background: #f3e5f5; color: #7b1fa2; } | |
| .example-info { flex: 1; min-width: 0; } | |
| .example-title { font-weight: 500; font-size: 13px; } | |
| .example-url { | |
| font-size: 11px; | |
| color: #86868b; | |
| font-family: 'SF Mono', Menlo, monospace; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| /* JSON highlighting - subtle */ | |
| .json-key { color: #007aff; } | |
| .json-string { color: #c41a16; } | |
| .json-number { color: #1c00cf; } | |
| .json-boolean { color: #aa0d91; } | |
| .json-null { color: #86868b; } | |
| /* Preview */ | |
| .preview-container { | |
| padding: 16px; | |
| min-height: 200px; | |
| max-height: 500px; | |
| overflow: auto; | |
| } | |
| .preview-placeholder { color: #86868b; } | |
| .preview-iframe { | |
| width: 100%; | |
| min-height: 400px; | |
| border: 1px solid #d2d2d7; | |
| border-radius: 6px; | |
| background: #fff; | |
| } | |
| .preview-image { | |
| max-width: 100%; | |
| border-radius: 6px; | |
| border: 1px solid #d2d2d7; | |
| } | |
| .preview-json { | |
| background: #f5f5f7; | |
| padding: 16px; | |
| border-radius: 8px; | |
| font-family: 'SF Mono', Menlo, monospace; | |
| font-size: 13px; | |
| line-height: 1.6; | |
| } | |
| .preview-json .key { color: #007aff; font-weight: 500; } | |
| .preview-json .string { color: #c41a16; } | |
| .preview-json .number { color: #1c00cf; } | |
| .preview-json .boolean { color: #aa0d91; } | |
| .preview-json .null { color: #86868b; font-style: italic; } | |
| .preview-json .bracket { color: #1d1d1f; } | |
| .preview-json details { margin-left: 20px; } | |
| .preview-json summary { cursor: pointer; margin-left: -20px; } | |
| .preview-json summary:hover { color: #007aff; } | |
| .preview-text { | |
| background: #f5f5f7; | |
| padding: 16px; | |
| border-radius: 8px; | |
| font-family: 'SF Mono', Menlo, monospace; | |
| font-size: 13px; | |
| white-space: pre-wrap; | |
| } | |
| .preview-xml { color: #1d1d1f; } | |
| .preview-xml .tag { color: #aa0d91; } | |
| .preview-xml .attr { color: #007aff; } | |
| .preview-xml .value { color: #c41a16; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>API Explorer</h1> | |
| <div class="header-links"> | |
| <a href="/docs">Swagger Docs</a> | |
| <a href="/api">API Info</a> | |
| </div> | |
| </header> | |
| <div class="request-bar"> | |
| <select class="method-select" id="method"> | |
| <option value="GET">GET</option> | |
| <option value="POST">POST</option> | |
| <option value="PUT">PUT</option> | |
| <option value="PATCH">PATCH</option> | |
| <option value="DELETE">DELETE</option> | |
| </select> | |
| <input type="text" class="url-input" id="url" placeholder="/hello" value="/hello"> | |
| <button class="send-btn" id="send">Send</button> | |
| <div class="copy-dropdown"> | |
| <button class="copy-btn" id="copy-toggle">Copy as...</button> | |
| <div class="copy-menu" id="copy-menu"> | |
| <button class="copy-option" data-format="curl"> | |
| <div class="copy-option-label">cURL</div> | |
| <div class="copy-option-desc">Command line</div> | |
| </button> | |
| <button class="copy-option" data-format="python"> | |
| <div class="copy-option-label">Python requests</div> | |
| <div class="copy-option-desc">requests library</div> | |
| </button> | |
| <button class="copy-option" data-format="fetch"> | |
| <div class="copy-option-label">JavaScript fetch</div> | |
| <div class="copy-option-desc">Browser/Node.js</div> | |
| </button> | |
| <button class="copy-option" data-format="httpie"> | |
| <div class="copy-option-label">HTTPie</div> | |
| <div class="copy-option-desc">CLI for humans</div> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="toast" id="toast">Copied to clipboard!</div> | |
| <div class="main-layout"> | |
| <!-- Request Panel --> | |
| <div class="panel"> | |
| <div class="tabs" id="request-tabs"> | |
| <button class="tab active" data-tab="req-body">Body</button> | |
| <button class="tab" data-tab="req-headers">Headers</button> | |
| </div> | |
| <div class="panel-body"> | |
| <div class="tab-content active" id="req-body-tab"> | |
| <textarea id="body" placeholder='{"name": "Laptop", "price": 999.99, "quantity": 5}'></textarea> | |
| </div> | |
| <div class="tab-content" id="req-headers-tab"> | |
| <textarea id="headers" placeholder="Content-Type: application/json">Content-Type: application/json</textarea> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Response Panel --> | |
| <div class="panel"> | |
| <div class="status-bar"> | |
| <div class="status-item">Status<span class="status-value" id="status">-</span></div> | |
| <div class="status-item">Time<span class="status-value" id="time">-</span></div> | |
| <div class="status-item">Size<span class="status-value" id="size">-</span></div> | |
| <div class="status-item">Type<span class="status-value" id="content-type">-</span></div> | |
| </div> | |
| <div class="tabs" id="response-tabs"> | |
| <button class="tab active" data-tab="res-body">Body</button> | |
| <button class="tab" data-tab="res-preview">Preview</button> | |
| <button class="tab" data-tab="res-headers">Headers</button> | |
| <button class="tab" data-tab="res-raw">Raw</button> | |
| </div> | |
| <div class="tab-content active" id="res-body-tab"> | |
| <div class="response-content" id="response">Send a request to see the response</div> | |
| </div> | |
| <div class="tab-content" id="res-preview-tab"> | |
| <div class="preview-container" id="preview-container"> | |
| <div class="preview-placeholder">Send a request to see preview</div> | |
| </div> | |
| </div> | |
| <div class="tab-content" id="res-headers-tab"> | |
| <div class="panel-body"> | |
| <div class="headers-list" id="response-headers"></div> | |
| </div> | |
| </div> | |
| <div class="tab-content" id="res-raw-tab"> | |
| <div class="response-content" id="response-raw"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="examples-section"> | |
| <h3>Examples</h3> | |
| <div class="examples-grid" id="examples"></div> | |
| </div> | |
| </div> | |
| <script> | |
| const examples = [ | |
| { method: 'GET', url: '/hello', title: 'Hello World' }, | |
| { method: 'GET', url: '/time', title: 'Server Time' }, | |
| { method: 'GET', url: '/greet?name=Student', title: 'Query Param' }, | |
| { method: 'GET', url: '/greet/Alice', title: 'Path Param' }, | |
| { method: 'GET', url: '/items', title: 'List Items' }, | |
| { method: 'GET', url: '/items/1', title: 'Get Item' }, | |
| { method: 'POST', url: '/items', title: 'Create Item', body: '{"name": "Laptop", "price": 999.99, "quantity": 5}' }, | |
| { method: 'POST', url: '/echo', title: 'Echo JSON', body: '{"message": "Hello!", "count": 42}' }, | |
| { method: 'PUT', url: '/items/1', title: 'Replace Item', body: '{"name": "Green Apple", "price": 2.50, "quantity": 200}' }, | |
| { method: 'PATCH', url: '/items/1', title: 'Partial Update', body: '{"price": 1.99}' }, | |
| { method: 'DELETE', url: '/items/3', title: 'Delete Item' }, | |
| { method: 'GET', url: '/format/json', title: 'JSON Format' }, | |
| { method: 'GET', url: '/format/xml', title: 'XML Format' }, | |
| { method: 'GET', url: '/format/html', title: 'HTML Format' }, | |
| { method: 'GET', url: '/format/csv', title: 'CSV Format' }, | |
| { method: 'GET', url: '/format/yaml', title: 'YAML Format' }, | |
| { method: 'GET', url: '/format/markdown', title: 'Markdown' }, | |
| { method: 'GET', url: '/format/image', title: 'PNG Image' }, | |
| { method: 'GET', url: '/headers', title: 'View Headers' }, | |
| { method: 'GET', url: '/status/404', title: 'Error 404' }, | |
| { method: 'GET', url: '/status/201', title: 'Status 201' }, | |
| ]; | |
| // Render examples | |
| const examplesContainer = document.getElementById('examples'); | |
| examples.forEach(ex => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'example-btn'; | |
| btn.innerHTML = ` | |
| <span class="method-badge method-${ex.method}">${ex.method}</span> | |
| <div class="example-info"> | |
| <div class="example-title">${ex.title}</div> | |
| <div class="example-url">${ex.url}</div> | |
| </div> | |
| `; | |
| btn.onclick = () => { | |
| document.getElementById('method').value = ex.method; | |
| document.getElementById('url').value = ex.url; | |
| document.getElementById('body').value = ex.body || ''; | |
| }; | |
| examplesContainer.appendChild(btn); | |
| }); | |
| // Tab switching | |
| function setupTabs(containerId) { | |
| const container = document.getElementById(containerId); | |
| container.querySelectorAll('.tab').forEach(tab => { | |
| tab.onclick = () => { | |
| container.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); | |
| tab.classList.add('active'); | |
| const panel = container.closest('.panel') || document; | |
| panel.querySelectorAll(':scope > .tab-content, :scope > .panel-body > .tab-content').forEach(c => c.classList.remove('active')); | |
| document.getElementById(tab.dataset.tab + '-tab').classList.add('active'); | |
| }; | |
| }); | |
| } | |
| setupTabs('request-tabs'); | |
| setupTabs('response-tabs'); | |
| // JSON highlighting | |
| function highlightJSON(str) { | |
| return str | |
| .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') | |
| .replace(/"([^"]+)":/g, '<span class="json-key">"$1"</span>:') | |
| .replace(/: "([^"]*)"/g, ': <span class="json-string">"$1"</span>') | |
| .replace(/: (-?\\d+\\.?\\d*)/g, ': <span class="json-number">$1</span>') | |
| .replace(/: (true|false)/g, ': <span class="json-boolean">$1</span>') | |
| .replace(/: (null)/g, ': <span class="json-null">$1</span>'); | |
| } | |
| // Send request | |
| async function sendRequest() { | |
| const method = document.getElementById('method').value; | |
| const url = document.getElementById('url').value; | |
| const body = document.getElementById('body').value; | |
| const headersText = document.getElementById('headers').value; | |
| const headers = {}; | |
| headersText.split('\\n').forEach(line => { | |
| const [key, ...vals] = line.split(':'); | |
| if (key && vals.length) headers[key.trim()] = vals.join(':').trim(); | |
| }); | |
| const opts = { method, headers }; | |
| if (['POST', 'PUT', 'PATCH'].includes(method) && body) { | |
| opts.body = body; | |
| } | |
| document.getElementById('send').disabled = true; | |
| document.getElementById('send').textContent = 'Sending...'; | |
| const start = performance.now(); | |
| try { | |
| const res = await fetch(url, opts); | |
| const elapsed = Math.round(performance.now() - start); | |
| const contentType = res.headers.get('content-type') || ''; | |
| const isBinary = contentType.includes('image/') || contentType.includes('octet-stream'); | |
| // Status | |
| const statusEl = document.getElementById('status'); | |
| statusEl.textContent = res.status + ' ' + res.statusText; | |
| statusEl.className = 'status-value ' + (res.ok ? 'status-ok' : (res.status >= 500 ? 'status-err' : 'status-warn')); | |
| // Response headers | |
| const headersHtml = []; | |
| res.headers.forEach((value, name) => { | |
| headersHtml.push(`<div class="header-row"><span class="header-name">${name}</span><span class="header-value">${value}</span></div>`); | |
| }); | |
| document.getElementById('response-headers').innerHTML = headersHtml.join('') || '<div style="color:#86868b">No headers</div>'; | |
| if (isBinary) { | |
| // Handle binary data | |
| const blob = await res.blob(); | |
| document.getElementById('time').textContent = elapsed + 'ms'; | |
| document.getElementById('size').textContent = blob.size > 1024 ? (blob.size / 1024).toFixed(1) + ' KB' : blob.size + ' B'; | |
| document.getElementById('content-type').textContent = contentType.split(';')[0] || '-'; | |
| document.getElementById('response-raw').textContent = `[Binary data: ${blob.size} bytes]`; | |
| document.getElementById('response').innerHTML = `<span style="color:#86868b">[Binary data: ${blob.size} bytes, type: ${contentType}]</span>`; | |
| // Preview binary | |
| renderBinaryPreview(blob, contentType, url); | |
| } else { | |
| // Handle text data | |
| const text = await res.text(); | |
| document.getElementById('time').textContent = elapsed + 'ms'; | |
| document.getElementById('size').textContent = text.length > 1024 ? (text.length / 1024).toFixed(1) + ' KB' : text.length + ' B'; | |
| document.getElementById('content-type').textContent = contentType.split(';')[0] || '-'; | |
| // Raw response | |
| document.getElementById('response-raw').textContent = text; | |
| // Formatted response | |
| let display; | |
| try { | |
| const json = JSON.parse(text); | |
| display = highlightJSON(JSON.stringify(json, null, 2)); | |
| } catch { | |
| display = text.replace(/</g, '<').replace(/>/g, '>'); | |
| } | |
| document.getElementById('response').innerHTML = display; | |
| // Preview | |
| renderPreview(text, contentType); | |
| } | |
| } catch (err) { | |
| document.getElementById('status').textContent = 'Error'; | |
| document.getElementById('status').className = 'status-value status-err'; | |
| document.getElementById('response').textContent = 'Request failed: ' + err.message; | |
| document.getElementById('response-headers').innerHTML = ''; | |
| document.getElementById('response-raw').textContent = ''; | |
| } | |
| document.getElementById('send').disabled = false; | |
| document.getElementById('send').textContent = 'Send'; | |
| } | |
| document.getElementById('send').onclick = sendRequest; | |
| document.getElementById('url').onkeydown = (e) => { if (e.key === 'Enter') sendRequest(); }; | |
| sendRequest(); | |
| // Preview rendering for binary content | |
| function renderBinaryPreview(blob, contentType, url) { | |
| const container = document.getElementById('preview-container'); | |
| const ct = contentType.toLowerCase(); | |
| if (ct.includes('image/')) { | |
| const blobUrl = URL.createObjectURL(blob); | |
| container.innerHTML = `<img class="preview-image" src="${blobUrl}" alt="Image preview" onload="URL.revokeObjectURL(this.src)">`; | |
| } else { | |
| // Generic binary | |
| container.innerHTML = `<div class="preview-text" style="text-align:center;padding:40px;"> | |
| <div style="font-size:32px;margin-bottom:12px;">Binary Data</div> | |
| <div style="color:#86868b;">${blob.size} bytes</div> | |
| <div style="color:#86868b;">${contentType}</div> | |
| <a href="${url}" download style="display:inline-block;margin-top:16px;padding:8px 16px;background:#007aff;color:#fff;border-radius:6px;text-decoration:none;">Download</a> | |
| </div>`; | |
| } | |
| } | |
| // Preview rendering for text content | |
| function renderPreview(text, contentType) { | |
| const container = document.getElementById('preview-container'); | |
| const ct = contentType.toLowerCase(); | |
| if (ct.includes('text/html')) { | |
| // Render HTML in sandboxed iframe | |
| const iframe = document.createElement('iframe'); | |
| iframe.className = 'preview-iframe'; | |
| iframe.sandbox = 'allow-same-origin'; | |
| iframe.srcdoc = text; | |
| container.innerHTML = ''; | |
| container.appendChild(iframe); | |
| } else if (ct.includes('application/json') || ct.includes('json')) { | |
| // Interactive JSON tree | |
| try { | |
| const json = JSON.parse(text); | |
| container.innerHTML = '<div class="preview-json">' + renderJSONTree(json) + '</div>'; | |
| } catch { | |
| container.innerHTML = '<div class="preview-text">' + text.replace(/</g, '<') + '</div>'; | |
| } | |
| } else if (ct.includes('xml')) { | |
| // Syntax highlighted XML | |
| const highlighted = text | |
| .replace(/</g, '<').replace(/>/g, '>') | |
| .replace(/<(\\/?[\\w:-]+)/g, '<<span class="tag">$1</span>') | |
| .replace(/(\\w+)=("[^"]*")/g, '<span class="attr">$1</span>=<span class="value">$2</span>'); | |
| container.innerHTML = '<pre class="preview-text preview-xml">' + highlighted + '</pre>'; | |
| } else if (ct.includes('csv')) { | |
| // Render CSV as table | |
| container.innerHTML = renderCSVTable(text); | |
| } else if (ct.includes('markdown')) { | |
| // Basic markdown rendering | |
| container.innerHTML = '<div class="preview-text">' + renderBasicMarkdown(text) + '</div>'; | |
| } else { | |
| // Plain text | |
| container.innerHTML = '<pre class="preview-text">' + text.replace(/</g, '<').replace(/>/g, '>') + '</pre>'; | |
| } | |
| } | |
| function renderJSONTree(obj, level = 0) { | |
| if (obj === null) return '<span class="null">null</span>'; | |
| if (typeof obj === 'boolean') return '<span class="boolean">' + obj + '</span>'; | |
| if (typeof obj === 'number') return '<span class="number">' + obj + '</span>'; | |
| if (typeof obj === 'string') return '<span class="string">"' + obj.replace(/</g, '<') + '"</span>'; | |
| if (Array.isArray(obj)) { | |
| if (obj.length === 0) return '<span class="bracket">[]</span>'; | |
| let html = '<span class="bracket">[</span>'; | |
| if (level < 2) { | |
| html += '<br>' + obj.map((v, i) => | |
| ' '.repeat(level + 1) + renderJSONTree(v, level + 1) + (i < obj.length - 1 ? ',' : '') | |
| ).join('<br>') + '<br>' + ' '.repeat(level); | |
| } else { | |
| html += obj.map((v, i) => renderJSONTree(v, level + 1) + (i < obj.length - 1 ? ', ' : '')).join(''); | |
| } | |
| html += '<span class="bracket">]</span>'; | |
| return html; | |
| } | |
| if (typeof obj === 'object') { | |
| const keys = Object.keys(obj); | |
| if (keys.length === 0) return '<span class="bracket">{}</span>'; | |
| let html = '<span class="bracket">{</span>'; | |
| if (level < 2) { | |
| html += '<br>' + keys.map((k, i) => | |
| ' '.repeat(level + 1) + '<span class="key">"' + k + '"</span>: ' + renderJSONTree(obj[k], level + 1) + (i < keys.length - 1 ? ',' : '') | |
| ).join('<br>') + '<br>' + ' '.repeat(level); | |
| } else { | |
| html += keys.map((k, i) => | |
| '<span class="key">"' + k + '"</span>: ' + renderJSONTree(obj[k], level + 1) + (i < keys.length - 1 ? ', ' : '') | |
| ).join(''); | |
| } | |
| html += '<span class="bracket">}</span>'; | |
| return html; | |
| } | |
| return String(obj); | |
| } | |
| function renderCSVTable(csv) { | |
| const lines = csv.trim().split('\\n'); | |
| if (lines.length === 0) return '<div class="preview-text">Empty CSV</div>'; | |
| let html = '<table style="width:100%;border-collapse:collapse;font-size:13px;">'; | |
| lines.forEach((line, i) => { | |
| const cells = line.split(','); | |
| const tag = i === 0 ? 'th' : 'td'; | |
| const style = 'padding:8px 12px;border:1px solid #d2d2d7;text-align:left;' + (i === 0 ? 'background:#f5f5f7;font-weight:600;' : ''); | |
| html += '<tr>' + cells.map(c => `<${tag} style="${style}">${c.trim()}</${tag}>`).join('') + '</tr>'; | |
| }); | |
| html += '</table>'; | |
| return html; | |
| } | |
| function renderBasicMarkdown(md) { | |
| return md | |
| .replace(/^### (.+)$/gm, '<h3 style="margin:12px 0 8px;font-size:16px;">$1</h3>') | |
| .replace(/^## (.+)$/gm, '<h2 style="margin:16px 0 8px;font-size:18px;">$1</h2>') | |
| .replace(/^# (.+)$/gm, '<h1 style="margin:20px 0 12px;font-size:22px;">$1</h1>') | |
| .replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>') | |
| .replace(/\\*(.+?)\\*/g, '<em>$1</em>') | |
| .replace(/`([^`]+)`/g, '<code style="background:#e8e8e8;padding:2px 6px;border-radius:4px;">$1</code>') | |
| .replace(/^- (.+)$/gm, '<li style="margin:4px 0;">$1</li>') | |
| .replace(/\\n/g, '<br>'); | |
| } | |
| // Copy as... functionality | |
| const copyToggle = document.getElementById('copy-toggle'); | |
| const copyMenu = document.getElementById('copy-menu'); | |
| const toast = document.getElementById('toast'); | |
| copyToggle.onclick = (e) => { | |
| e.stopPropagation(); | |
| copyMenu.classList.toggle('show'); | |
| }; | |
| document.addEventListener('click', () => copyMenu.classList.remove('show')); | |
| function getBaseUrl() { | |
| return window.location.origin; | |
| } | |
| function generateCode(format) { | |
| const method = document.getElementById('method').value; | |
| const url = document.getElementById('url').value; | |
| const body = document.getElementById('body').value; | |
| const headersText = document.getElementById('headers').value; | |
| const fullUrl = getBaseUrl() + url; | |
| const headers = {}; | |
| headersText.split('\\n').forEach(line => { | |
| const [key, ...vals] = line.split(':'); | |
| if (key && vals.length) headers[key.trim()] = vals.join(':').trim(); | |
| }); | |
| const hasBody = ['POST', 'PUT', 'PATCH'].includes(method) && body; | |
| switch (format) { | |
| case 'curl': { | |
| let cmd = `curl -X ${method} "${fullUrl}"`; | |
| for (const [k, v] of Object.entries(headers)) { | |
| cmd += ` \\\\\\n -H "${k}: ${v}"`; | |
| } | |
| if (hasBody) { | |
| cmd += ` \\\\\\n -d '${body.replace(/'/g, "\\'")}'`; | |
| } | |
| return cmd; | |
| } | |
| case 'python': { | |
| let code = `import requests\\n\\n`; | |
| code += `url = "${fullUrl}"\\n`; | |
| if (Object.keys(headers).length) { | |
| code += `headers = ${JSON.stringify(headers, null, 4)}\\n`; | |
| } | |
| if (hasBody) { | |
| code += `data = ${body}\\n\\n`; | |
| code += `response = requests.${method.toLowerCase()}(url`; | |
| if (Object.keys(headers).length) code += `, headers=headers`; | |
| code += `, json=data)`; | |
| } else { | |
| code += `\\nresponse = requests.${method.toLowerCase()}(url`; | |
| if (Object.keys(headers).length) code += `, headers=headers`; | |
| code += `)`; | |
| } | |
| code += `\\nprint(response.status_code)\\nprint(response.json())`; | |
| return code; | |
| } | |
| case 'fetch': { | |
| let code = `fetch("${fullUrl}", {\\n`; | |
| code += ` method: "${method}",\\n`; | |
| if (Object.keys(headers).length) { | |
| code += ` headers: ${JSON.stringify(headers, null, 4).replace(/\\n/g, '\\n ')},\\n`; | |
| } | |
| if (hasBody) { | |
| code += ` body: JSON.stringify(${body})\\n`; | |
| } | |
| code += `})\\n`; | |
| code += `.then(res => res.json())\\n`; | |
| code += `.then(data => console.log(data))\\n`; | |
| code += `.catch(err => console.error(err));`; | |
| return code; | |
| } | |
| case 'httpie': { | |
| let cmd = `http ${method} "${fullUrl}"`; | |
| for (const [k, v] of Object.entries(headers)) { | |
| cmd += ` "${k}:${v}"`; | |
| } | |
| if (hasBody) { | |
| try { | |
| const jsonBody = JSON.parse(body); | |
| for (const [k, v] of Object.entries(jsonBody)) { | |
| const val = typeof v === 'string' ? `="${v}"` : `:=${JSON.stringify(v)}`; | |
| cmd += ` ${k}${val}`; | |
| } | |
| } catch { | |
| cmd += ` --raw '${body}'`; | |
| } | |
| } | |
| return cmd; | |
| } | |
| } | |
| } | |
| function showToast(msg) { | |
| toast.textContent = msg; | |
| toast.classList.add('show'); | |
| setTimeout(() => toast.classList.remove('show'), 2000); | |
| } | |
| document.querySelectorAll('.copy-option').forEach(btn => { | |
| btn.onclick = async (e) => { | |
| e.stopPropagation(); | |
| const format = btn.dataset.format; | |
| const code = generateCode(format); | |
| try { | |
| await navigator.clipboard.writeText(code.replace(/\\\\n/g, '\\n')); | |
| showToast(`Copied as ${format.toUpperCase()}!`); | |
| } catch { | |
| showToast('Failed to copy'); | |
| } | |
| copyMenu.classList.remove('show'); | |
| }; | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # ============================================================================ | |
| # SIMPLE GET ENDPOINTS | |
| # ============================================================================ | |
| def hello(): | |
| """Simplest GET endpoint - returns a greeting""" | |
| return {"message": "Hello, World!"} | |
| def get_time(): | |
| """Returns current server time""" | |
| now = datetime.now() | |
| return { | |
| "current_time": now.isoformat(), | |
| "timestamp": now.timestamp(), | |
| "formatted": now.strftime("%B %d, %Y at %I:%M %p") | |
| } | |
| def greet(name: str = Query(default="Guest", description="Your name")): | |
| """GET with query parameter - /greet?name=YourName""" | |
| return {"message": f"Hello, {name}!", "parameter_received": name} | |
| def greet_path(name: str = Path(..., description="Your name in the URL path")): | |
| """GET with path parameter - /greet/YourName""" | |
| return {"message": f"Hello, {name}!", "path_parameter": name} | |
| def search( | |
| q: str = Query(..., description="Search query (required)"), | |
| limit: int = Query(default=10, ge=1, le=100, description="Max results"), | |
| offset: int = Query(default=0, ge=0, description="Skip first N results"), | |
| sort: str = Query(default="relevance", description="Sort by: relevance, date, name") | |
| ): | |
| """GET with multiple query parameters - demonstrates pagination""" | |
| return { | |
| "query": q, | |
| "limit": limit, | |
| "offset": offset, | |
| "sort": sort, | |
| "explanation": "This shows how query parameters work for filtering/pagination", | |
| "example_url": f"/search?q={q}&limit={limit}&offset={offset}&sort={sort}" | |
| } | |
| # ============================================================================ | |
| # CRUD OPERATIONS ON ITEMS | |
| # ============================================================================ | |
| def list_items(): | |
| """GET all items""" | |
| return {"items": list(items_db.values()), "count": len(items_db)} | |
| def get_item(item_id: int = Path(..., description="The ID of the item to retrieve")): | |
| """GET a single item by ID""" | |
| if item_id not in items_db: | |
| raise HTTPException(status_code=404, detail=f"Item with id {item_id} not found") | |
| return items_db[item_id] | |
| def create_item(item: Item): | |
| """POST - Create a new item""" | |
| global next_id | |
| new_item = {"id": next_id, **item.model_dump()} | |
| items_db[next_id] = new_item | |
| next_id += 1 | |
| return {"message": "Item created successfully", "item": new_item} | |
| def update_item(item_id: int, item: Item): | |
| """PUT - Replace an entire item""" | |
| if item_id not in items_db: | |
| raise HTTPException(status_code=404, detail=f"Item with id {item_id} not found") | |
| updated_item = {"id": item_id, **item.model_dump()} | |
| items_db[item_id] = updated_item | |
| return {"message": "Item updated successfully", "item": updated_item} | |
| def partial_update_item( | |
| item_id: int, | |
| name: Optional[str] = Body(default=None), | |
| price: Optional[float] = Body(default=None), | |
| quantity: Optional[int] = Body(default=None), | |
| description: Optional[str] = Body(default=None) | |
| ): | |
| """PATCH - Partially update an item (only send fields to change)""" | |
| if item_id not in items_db: | |
| raise HTTPException(status_code=404, detail=f"Item with id {item_id} not found") | |
| current_item = items_db[item_id] | |
| if name is not None: | |
| current_item["name"] = name | |
| if price is not None: | |
| current_item["price"] = price | |
| if quantity is not None: | |
| current_item["quantity"] = quantity | |
| if description is not None: | |
| current_item["description"] = description | |
| return {"message": "Item partially updated", "item": current_item} | |
| def delete_item(item_id: int): | |
| """DELETE - Remove an item""" | |
| if item_id not in items_db: | |
| raise HTTPException(status_code=404, detail=f"Item with id {item_id} not found") | |
| deleted_item = items_db.pop(item_id) | |
| return {"message": "Item deleted successfully", "deleted_item": deleted_item} | |
| # ============================================================================ | |
| # POST EXAMPLES | |
| # ============================================================================ | |
| def create_user(user: User): | |
| """POST - Create a new user (demonstrates JSON body)""" | |
| return { | |
| "message": "User created successfully", | |
| "user": user.model_dump(), | |
| "note": "In a real app, this would save to a database" | |
| } | |
| def echo_body(data: dict = Body(...)): | |
| """POST - Echo back whatever JSON you send""" | |
| return { | |
| "received": data, | |
| "type": str(type(data).__name__), | |
| "keys": list(data.keys()) if isinstance(data, dict) else None | |
| } | |
| async def echo_text(request: Request): | |
| """POST - Echo back raw text body""" | |
| body = await request.body() | |
| return { | |
| "received": body.decode("utf-8"), | |
| "length": len(body), | |
| "content_type": request.headers.get("content-type", "not specified") | |
| } | |
| # ============================================================================ | |
| # DIFFERENT RESPONSE FORMATS | |
| # ============================================================================ | |
| def format_json(): | |
| """Returns data as JSON (default)""" | |
| return { | |
| "format": "JSON", | |
| "content_type": "application/json", | |
| "data": {"name": "Alice", "age": 30, "city": "Mumbai"} | |
| } | |
| def format_text(): | |
| """Returns data as plain text""" | |
| return """Format: Plain Text | |
| Content-Type: text/plain | |
| Name: Alice | |
| Age: 30 | |
| City: Mumbai | |
| This is plain text - no special formatting!""" | |
| def format_html(): | |
| """Returns data as HTML""" | |
| return """ | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>HTML Response</title> | |
| <style> | |
| body { font-family: Arial, sans-serif; padding: 20px; } | |
| .card { border: 1px solid #ddd; padding: 15px; border-radius: 8px; max-width: 300px; } | |
| h1 { color: #333; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>HTML Response Example</h1> | |
| <div class="card"> | |
| <h2>User Info</h2> | |
| <p><strong>Name:</strong> Alice</p> | |
| <p><strong>Age:</strong> 30</p> | |
| <p><strong>City:</strong> Mumbai</p> | |
| </div> | |
| <p><em>Content-Type: text/html</em></p> | |
| </body> | |
| </html> | |
| """ | |
| def format_xml(): | |
| """Returns data as XML""" | |
| xml_content = """<?xml version="1.0" encoding="UTF-8"?> | |
| <response> | |
| <format>XML</format> | |
| <content_type>application/xml</content_type> | |
| <data> | |
| <user> | |
| <name>Alice</name> | |
| <age>30</age> | |
| <city>Mumbai</city> | |
| </user> | |
| </data> | |
| </response>""" | |
| return Response(content=xml_content, media_type="application/xml") | |
| def format_csv(): | |
| """Returns data as CSV (spreadsheet format)""" | |
| csv_content = """id,name,price,quantity,description | |
| 1,Apple,1.50,100,Fresh red apple | |
| 2,Banana,0.75,150,Yellow banana | |
| 3,Orange,2.00,80,Juicy orange""" | |
| return Response( | |
| content=csv_content, | |
| media_type="text/csv", | |
| headers={"Content-Disposition": "inline; filename=items.csv"} | |
| ) | |
| def format_markdown(): | |
| """Returns data as Markdown""" | |
| md_content = """# User Profile | |
| | Field | Value | | |
| |-------|-------| | |
| | Name | Alice | | |
| | Age | 30 | | |
| | City | Mumbai | | |
| ## About | |
| This is a **markdown** formatted response. | |
| - Supports *italic* text | |
| - Supports **bold** text | |
| - Supports `code` blocks | |
| ```python | |
| print("Hello, World!") | |
| ``` | |
| """ | |
| return Response(content=md_content, media_type="text/markdown") | |
| def format_yaml(): | |
| """Returns data as YAML""" | |
| yaml_content = """# User Data in YAML format | |
| format: YAML | |
| content_type: application/x-yaml | |
| data: | |
| user: | |
| name: Alice | |
| age: 30 | |
| city: Mumbai | |
| hobbies: | |
| - reading | |
| - coding | |
| - hiking | |
| """ | |
| return Response(content=yaml_content, media_type="application/x-yaml") | |
| def format_image(): | |
| """Returns a tiny PNG image (1x1 red pixel)""" | |
| # Minimal valid PNG: 1x1 red pixel | |
| import base64 | |
| png_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==" | |
| png_bytes = base64.b64decode(png_base64) | |
| return Response(content=png_bytes, media_type="image/png") | |
| def format_binary(): | |
| """Returns raw binary data (demonstrates non-text response)""" | |
| # Some example bytes | |
| binary_data = bytes([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0xFF, 0xFE, 0x00, 0x01]) | |
| return Response( | |
| content=binary_data, | |
| media_type="application/octet-stream", | |
| headers={"Content-Disposition": "inline; filename=data.bin"} | |
| ) | |
| # ============================================================================ | |
| # EDUCATIONAL ENDPOINTS | |
| # ============================================================================ | |
| def show_headers(request: Request): | |
| """Shows all request headers sent by the client""" | |
| headers_dict = dict(request.headers) | |
| return { | |
| "your_headers": headers_dict, | |
| "common_headers_explained": { | |
| "user-agent": "Identifies your browser/client", | |
| "accept": "What content types you accept", | |
| "host": "The server you're connecting to", | |
| "content-type": "Format of data you're sending (for POST/PUT)" | |
| } | |
| } | |
| def custom_header_response(): | |
| """Returns a response with custom headers""" | |
| content = {"message": "Check the response headers!"} | |
| headers = { | |
| "X-Custom-Header": "Hello from the server!", | |
| "X-Request-Time": datetime.now().isoformat(), | |
| "X-API-Version": "1.0.0" | |
| } | |
| return JSONResponse(content=content, headers=headers) | |
| def return_status_code( | |
| code: int = Path(..., description="HTTP status code to return (100-599)") | |
| ): | |
| """Returns the specified HTTP status code - useful for testing error handling""" | |
| status_messages = { | |
| 200: "OK - Request succeeded", | |
| 201: "Created - Resource created successfully", | |
| 204: "No Content - Success but no content to return", | |
| 400: "Bad Request - Invalid request syntax", | |
| 401: "Unauthorized - Authentication required", | |
| 403: "Forbidden - Access denied", | |
| 404: "Not Found - Resource doesn't exist", | |
| 405: "Method Not Allowed - Wrong HTTP method", | |
| 500: "Internal Server Error - Server error", | |
| 502: "Bad Gateway - Invalid response from upstream", | |
| 503: "Service Unavailable - Server temporarily unavailable", | |
| } | |
| if code < 100 or code > 599: | |
| raise HTTPException(status_code=400, detail="Status code must be between 100 and 599") | |
| message = status_messages.get(code, f"Status code {code}") | |
| if code >= 400: | |
| raise HTTPException(status_code=code, detail=message) | |
| return JSONResponse( | |
| status_code=code, | |
| content={"status_code": code, "message": message} | |
| ) | |
| async def show_method(request: Request): | |
| """Accepts any HTTP method and shows what was used""" | |
| body = None | |
| if request.method in ["POST", "PUT", "PATCH"]: | |
| try: | |
| body = await request.json() | |
| except: | |
| body = (await request.body()).decode("utf-8") or None | |
| return { | |
| "method_used": request.method, | |
| "method_explanation": { | |
| "GET": "Retrieve data (no body)", | |
| "POST": "Create new resource (has body)", | |
| "PUT": "Replace entire resource (has body)", | |
| "PATCH": "Partial update (has body)", | |
| "DELETE": "Remove resource (usually no body)" | |
| }.get(request.method, "Unknown method"), | |
| "body_received": body, | |
| "url": str(request.url), | |
| "query_params": dict(request.query_params) | |
| } | |
| def get_client_ip(request: Request): | |
| """Returns the client's IP address""" | |
| forwarded = request.headers.get("x-forwarded-for") | |
| if forwarded: | |
| ip = forwarded.split(",")[0].strip() | |
| else: | |
| ip = request.client.host if request.client else "unknown" | |
| return { | |
| "ip": ip, | |
| "note": "Behind a proxy, this shows the proxy's IP unless X-Forwarded-For is set" | |
| } | |
| # ============================================================================ | |
| # QUERY PARAMETER TYPES | |
| # ============================================================================ | |
| def parameter_types( | |
| string_param: str = Query(default="hello", description="A string parameter"), | |
| int_param: int = Query(default=42, description="An integer parameter"), | |
| float_param: float = Query(default=3.14, description="A float parameter"), | |
| bool_param: bool = Query(default=True, description="A boolean parameter"), | |
| list_param: List[str] = Query(default=["a", "b"], description="A list parameter") | |
| ): | |
| """Demonstrates different query parameter types""" | |
| return { | |
| "parameters_received": { | |
| "string_param": {"value": string_param, "type": "str"}, | |
| "int_param": {"value": int_param, "type": "int"}, | |
| "float_param": {"value": float_param, "type": "float"}, | |
| "bool_param": {"value": bool_param, "type": "bool"}, | |
| "list_param": {"value": list_param, "type": "list"} | |
| }, | |
| "example_url": "/params/types?string_param=test&int_param=100&float_param=2.5&bool_param=false&list_param=x&list_param=y" | |
| } | |
| # ============================================================================ | |
| # RUN SERVER (for local development) | |
| # ============================================================================ | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |