Nipun commited on
Commit
685321f
·
1 Parent(s): a4a1f1b

Add interactive API Explorer UI at root

Browse files
Files changed (1) hide show
  1. app.py +396 -4
app.py CHANGED
@@ -56,11 +56,12 @@ next_id = 4
56
  # ROOT & INFO ENDPOINTS
57
  # ============================================================================
58
 
59
- @app.get("/", tags=["Info"])
60
- def root():
61
- """Welcome endpoint with API overview"""
62
  return {
63
  "message": "Welcome to the API Teaching Tool!",
 
64
  "documentation": "/docs",
65
  "endpoints": {
66
  "GET examples": ["/hello", "/items", "/items/{id}", "/greet"],
@@ -69,10 +70,401 @@ def root():
69
  "DELETE examples": ["/items/{id}"],
70
  "PATCH examples": ["/items/{id}"],
71
  "Response formats": ["/format/json", "/format/text", "/format/html", "/format/xml"],
72
- "Educational": ["/echo", "/headers", "/status/{code}", "/delay/{seconds}"],
73
  }
74
  }
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  # ============================================================================
77
  # SIMPLE GET ENDPOINTS
78
  # ============================================================================
 
56
  # ROOT & INFO ENDPOINTS
57
  # ============================================================================
58
 
59
+ @app.get("/api", tags=["Info"])
60
+ def api_info():
61
+ """API overview endpoint"""
62
  return {
63
  "message": "Welcome to the API Teaching Tool!",
64
+ "ui": "/",
65
  "documentation": "/docs",
66
  "endpoints": {
67
  "GET examples": ["/hello", "/items", "/items/{id}", "/greet"],
 
70
  "DELETE examples": ["/items/{id}"],
71
  "PATCH examples": ["/items/{id}"],
72
  "Response formats": ["/format/json", "/format/text", "/format/html", "/format/xml"],
73
+ "Educational": ["/echo", "/headers", "/status/{code}"],
74
  }
75
  }
76
 
77
+ @app.get("/", response_class=HTMLResponse, tags=["Info"])
78
+ def ui():
79
+ """Interactive API Explorer UI"""
80
+ return """
81
+ <!DOCTYPE html>
82
+ <html lang="en">
83
+ <head>
84
+ <meta charset="UTF-8">
85
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
86
+ <title>API Explorer</title>
87
+ <style>
88
+ * { box-sizing: border-box; margin: 0; padding: 0; }
89
+ body {
90
+ font-family: 'Segoe UI', system-ui, sans-serif;
91
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
92
+ min-height: 100vh;
93
+ color: #e4e4e4;
94
+ padding: 20px;
95
+ }
96
+ .container { max-width: 1200px; margin: 0 auto; }
97
+ h1 {
98
+ text-align: center;
99
+ margin-bottom: 10px;
100
+ font-size: 2.5em;
101
+ background: linear-gradient(90deg, #00d9ff, #00ff88);
102
+ -webkit-background-clip: text;
103
+ -webkit-text-fill-color: transparent;
104
+ }
105
+ .subtitle { text-align: center; color: #888; margin-bottom: 30px; }
106
+ .main-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
107
+ @media (max-width: 900px) { .main-grid { grid-template-columns: 1fr; } }
108
+
109
+ .panel {
110
+ background: rgba(255,255,255,0.05);
111
+ border-radius: 16px;
112
+ padding: 20px;
113
+ border: 1px solid rgba(255,255,255,0.1);
114
+ }
115
+ .panel h2 {
116
+ font-size: 1.1em;
117
+ margin-bottom: 15px;
118
+ color: #00d9ff;
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 8px;
122
+ }
123
+
124
+ /* Request Section */
125
+ .request-line {
126
+ display: flex;
127
+ gap: 10px;
128
+ margin-bottom: 15px;
129
+ }
130
+ select, input, textarea, button {
131
+ font-family: inherit;
132
+ font-size: 14px;
133
+ border: none;
134
+ border-radius: 8px;
135
+ outline: none;
136
+ }
137
+ .method-select {
138
+ padding: 12px 15px;
139
+ background: #2d2d44;
140
+ color: #fff;
141
+ font-weight: 600;
142
+ cursor: pointer;
143
+ min-width: 110px;
144
+ }
145
+ .url-input {
146
+ flex: 1;
147
+ padding: 12px 15px;
148
+ background: #2d2d44;
149
+ color: #fff;
150
+ }
151
+ .url-input::placeholder { color: #666; }
152
+ .send-btn {
153
+ padding: 12px 30px;
154
+ background: linear-gradient(90deg, #00d9ff, #00ff88);
155
+ color: #1a1a2e;
156
+ font-weight: 700;
157
+ cursor: pointer;
158
+ transition: transform 0.2s, box-shadow 0.2s;
159
+ }
160
+ .send-btn:hover {
161
+ transform: translateY(-2px);
162
+ box-shadow: 0 5px 20px rgba(0,217,255,0.3);
163
+ }
164
+ .send-btn:active { transform: translateY(0); }
165
+
166
+ /* Tabs */
167
+ .tabs {
168
+ display: flex;
169
+ gap: 5px;
170
+ margin-bottom: 10px;
171
+ }
172
+ .tab {
173
+ padding: 8px 16px;
174
+ background: transparent;
175
+ color: #888;
176
+ cursor: pointer;
177
+ border-radius: 6px;
178
+ transition: all 0.2s;
179
+ }
180
+ .tab:hover { color: #fff; }
181
+ .tab.active { background: #2d2d44; color: #00d9ff; }
182
+
183
+ .tab-content { display: none; }
184
+ .tab-content.active { display: block; }
185
+
186
+ textarea {
187
+ width: 100%;
188
+ min-height: 150px;
189
+ padding: 12px;
190
+ background: #1a1a2e;
191
+ color: #e4e4e4;
192
+ resize: vertical;
193
+ font-family: 'Fira Code', 'Consolas', monospace;
194
+ font-size: 13px;
195
+ line-height: 1.5;
196
+ }
197
+
198
+ /* Response */
199
+ .response-meta {
200
+ display: flex;
201
+ gap: 15px;
202
+ margin-bottom: 15px;
203
+ flex-wrap: wrap;
204
+ }
205
+ .meta-item {
206
+ padding: 6px 12px;
207
+ background: #2d2d44;
208
+ border-radius: 6px;
209
+ font-size: 13px;
210
+ }
211
+ .status-ok { color: #00ff88; }
212
+ .status-err { color: #ff6b6b; }
213
+ .status-warn { color: #ffd93d; }
214
+
215
+ .response-body {
216
+ background: #1a1a2e;
217
+ border-radius: 8px;
218
+ padding: 15px;
219
+ font-family: 'Fira Code', 'Consolas', monospace;
220
+ font-size: 13px;
221
+ line-height: 1.6;
222
+ overflow-x: auto;
223
+ white-space: pre-wrap;
224
+ word-break: break-word;
225
+ max-height: 400px;
226
+ overflow-y: auto;
227
+ }
228
+
229
+ /* Examples */
230
+ .examples {
231
+ display: grid;
232
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
233
+ gap: 10px;
234
+ margin-top: 20px;
235
+ }
236
+ .example-btn {
237
+ padding: 12px 15px;
238
+ background: rgba(255,255,255,0.05);
239
+ border: 1px solid rgba(255,255,255,0.1);
240
+ border-radius: 10px;
241
+ cursor: pointer;
242
+ text-align: left;
243
+ transition: all 0.2s;
244
+ }
245
+ .example-btn:hover {
246
+ background: rgba(0,217,255,0.1);
247
+ border-color: rgba(0,217,255,0.3);
248
+ }
249
+ .example-method {
250
+ display: inline-block;
251
+ padding: 2px 8px;
252
+ border-radius: 4px;
253
+ font-size: 11px;
254
+ font-weight: 700;
255
+ margin-right: 8px;
256
+ }
257
+ .method-GET { background: #00ff88; color: #1a1a2e; }
258
+ .method-POST { background: #ffd93d; color: #1a1a2e; }
259
+ .method-PUT { background: #00d9ff; color: #1a1a2e; }
260
+ .method-DELETE { background: #ff6b6b; color: #1a1a2e; }
261
+ .method-PATCH { background: #c084fc; color: #1a1a2e; }
262
+ .example-title { font-weight: 600; margin-bottom: 4px; }
263
+ .example-desc { font-size: 12px; color: #888; }
264
+
265
+ .links { text-align: center; margin-top: 20px; }
266
+ .links a {
267
+ color: #00d9ff;
268
+ text-decoration: none;
269
+ margin: 0 15px;
270
+ }
271
+ .links a:hover { text-decoration: underline; }
272
+
273
+ /* Loading */
274
+ .loading { opacity: 0.6; pointer-events: none; }
275
+
276
+ /* JSON syntax highlighting */
277
+ .json-key { color: #00d9ff; }
278
+ .json-string { color: #00ff88; }
279
+ .json-number { color: #ffd93d; }
280
+ .json-boolean { color: #c084fc; }
281
+ .json-null { color: #888; }
282
+ </style>
283
+ </head>
284
+ <body>
285
+ <div class="container">
286
+ <h1>API Explorer</h1>
287
+ <p class="subtitle">Learn HTTP methods by doing - no setup required</p>
288
+
289
+ <div class="main-grid">
290
+ <div class="panel">
291
+ <h2>Request</h2>
292
+ <div class="request-line">
293
+ <select class="method-select" id="method">
294
+ <option value="GET">GET</option>
295
+ <option value="POST">POST</option>
296
+ <option value="PUT">PUT</option>
297
+ <option value="PATCH">PATCH</option>
298
+ <option value="DELETE">DELETE</option>
299
+ </select>
300
+ <input type="text" class="url-input" id="url" placeholder="/hello" value="/hello">
301
+ <button class="send-btn" id="send">Send</button>
302
+ </div>
303
+
304
+ <div class="tabs">
305
+ <button class="tab active" data-tab="body">Body</button>
306
+ <button class="tab" data-tab="headers">Headers</button>
307
+ </div>
308
+
309
+ <div class="tab-content active" id="body-tab">
310
+ <textarea id="body" placeholder='{"name": "Laptop", "price": 999.99, "quantity": 5}'></textarea>
311
+ </div>
312
+ <div class="tab-content" id="headers-tab">
313
+ <textarea id="headers" placeholder="Content-Type: application/json">Content-Type: application/json</textarea>
314
+ </div>
315
+ </div>
316
+
317
+ <div class="panel">
318
+ <h2>Response</h2>
319
+ <div class="response-meta" id="meta">
320
+ <span class="meta-item">Status: <span id="status">-</span></span>
321
+ <span class="meta-item">Time: <span id="time">-</span></span>
322
+ <span class="meta-item">Size: <span id="size">-</span></span>
323
+ </div>
324
+ <div class="response-body" id="response">Send a request to see the response...</div>
325
+ </div>
326
+ </div>
327
+
328
+ <div class="panel" style="margin-top: 20px;">
329
+ <h2>Try These Examples</h2>
330
+ <div class="examples" id="examples"></div>
331
+ </div>
332
+
333
+ <div class="links">
334
+ <a href="/docs">Swagger Docs</a>
335
+ <a href="/api">API Info</a>
336
+ </div>
337
+ </div>
338
+
339
+ <script>
340
+ const examples = [
341
+ { method: 'GET', url: '/hello', title: 'Hello World', desc: 'Simplest GET request' },
342
+ { method: 'GET', url: '/time', title: 'Server Time', desc: 'Get current timestamp' },
343
+ { method: 'GET', url: '/greet?name=Student', title: 'Query Parameter', desc: 'Pass data via URL' },
344
+ { method: 'GET', url: '/greet/Alice', title: 'Path Parameter', desc: 'Data in the URL path' },
345
+ { method: 'GET', url: '/items', title: 'List Items', desc: 'Get all items in store' },
346
+ { method: 'GET', url: '/items/1', title: 'Get One Item', desc: 'Fetch item by ID' },
347
+ { method: 'POST', url: '/items', title: 'Create Item', desc: 'Add new item to store', body: '{"name": "Laptop", "price": 999.99, "quantity": 5}' },
348
+ { method: 'POST', url: '/echo', title: 'Echo JSON', desc: 'Server echoes your data', body: '{"message": "Hello!", "count": 42}' },
349
+ { method: 'PUT', url: '/items/1', title: 'Replace Item', desc: 'Full update of item', body: '{"name": "Green Apple", "price": 2.50, "quantity": 200}' },
350
+ { method: 'PATCH', url: '/items/1', title: 'Partial Update', desc: 'Update only price', body: '{"price": 1.99}' },
351
+ { method: 'DELETE', url: '/items/3', title: 'Delete Item', desc: 'Remove item from store' },
352
+ { method: 'GET', url: '/format/json', title: 'JSON Response', desc: 'Default JSON format' },
353
+ { method: 'GET', url: '/format/xml', title: 'XML Response', desc: 'XML formatted data' },
354
+ { method: 'GET', url: '/format/html', title: 'HTML Response', desc: 'Returns HTML page' },
355
+ { method: 'GET', url: '/headers', title: 'See Headers', desc: 'View request headers' },
356
+ { method: 'GET', url: '/status/404', title: 'Error 404', desc: 'Test error handling' },
357
+ { method: 'GET', url: '/status/201', title: 'Status 201', desc: 'Created status code' },
358
+ { method: 'GET', url: '/method', title: 'Method Check', desc: 'Shows HTTP method used' },
359
+ ];
360
+
361
+ // Render examples
362
+ const examplesContainer = document.getElementById('examples');
363
+ examples.forEach(ex => {
364
+ const btn = document.createElement('button');
365
+ btn.className = 'example-btn';
366
+ btn.innerHTML = `
367
+ <div class="example-title">
368
+ <span class="example-method method-${ex.method}">${ex.method}</span>
369
+ ${ex.title}
370
+ </div>
371
+ <div class="example-desc">${ex.url}</div>
372
+ `;
373
+ btn.onclick = () => loadExample(ex);
374
+ examplesContainer.appendChild(btn);
375
+ });
376
+
377
+ function loadExample(ex) {
378
+ document.getElementById('method').value = ex.method;
379
+ document.getElementById('url').value = ex.url;
380
+ document.getElementById('body').value = ex.body || '';
381
+ }
382
+
383
+ // Tab switching
384
+ document.querySelectorAll('.tab').forEach(tab => {
385
+ tab.onclick = () => {
386
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
387
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
388
+ tab.classList.add('active');
389
+ document.getElementById(tab.dataset.tab + '-tab').classList.add('active');
390
+ };
391
+ });
392
+
393
+ // Syntax highlighting for JSON
394
+ function highlightJSON(json) {
395
+ if (typeof json !== 'string') json = JSON.stringify(json, null, 2);
396
+ return json
397
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
398
+ .replace(/"([^"]+)":/g, '<span class="json-key">"$1"</span>:')
399
+ .replace(/: "([^"]*)"/g, ': <span class="json-string">"$1"</span>')
400
+ .replace(/: (\\d+\\.?\\d*)/g, ': <span class="json-number">$1</span>')
401
+ .replace(/: (true|false)/g, ': <span class="json-boolean">$1</span>')
402
+ .replace(/: (null)/g, ': <span class="json-null">$1</span>');
403
+ }
404
+
405
+ // Send request
406
+ async function sendRequest() {
407
+ const method = document.getElementById('method').value;
408
+ const url = document.getElementById('url').value;
409
+ const body = document.getElementById('body').value;
410
+ const headersText = document.getElementById('headers').value;
411
+
412
+ const headers = {};
413
+ headersText.split('\\n').forEach(line => {
414
+ const [key, ...vals] = line.split(':');
415
+ if (key && vals.length) headers[key.trim()] = vals.join(':').trim();
416
+ });
417
+
418
+ const opts = { method, headers };
419
+ if (['POST', 'PUT', 'PATCH'].includes(method) && body) {
420
+ opts.body = body;
421
+ }
422
+
423
+ document.getElementById('send').classList.add('loading');
424
+ const start = performance.now();
425
+
426
+ try {
427
+ const res = await fetch(url, opts);
428
+ const elapsed = Math.round(performance.now() - start);
429
+ const text = await res.text();
430
+
431
+ // Update status
432
+ const statusEl = document.getElementById('status');
433
+ statusEl.textContent = res.status + ' ' + res.statusText;
434
+ statusEl.className = res.ok ? 'status-ok' : (res.status >= 500 ? 'status-err' : 'status-warn');
435
+
436
+ document.getElementById('time').textContent = elapsed + 'ms';
437
+ document.getElementById('size').textContent = (text.length / 1024).toFixed(2) + ' KB';
438
+
439
+ // Try to format as JSON
440
+ let display;
441
+ try {
442
+ const json = JSON.parse(text);
443
+ display = highlightJSON(JSON.stringify(json, null, 2));
444
+ } catch {
445
+ display = text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
446
+ }
447
+ document.getElementById('response').innerHTML = display;
448
+
449
+ } catch (err) {
450
+ document.getElementById('status').textContent = 'Error';
451
+ document.getElementById('status').className = 'status-err';
452
+ document.getElementById('response').textContent = 'Request failed: ' + err.message;
453
+ }
454
+
455
+ document.getElementById('send').classList.remove('loading');
456
+ }
457
+
458
+ document.getElementById('send').onclick = sendRequest;
459
+ document.getElementById('url').onkeydown = (e) => { if (e.key === 'Enter') sendRequest(); };
460
+
461
+ // Send initial request
462
+ sendRequest();
463
+ </script>
464
+ </body>
465
+ </html>
466
+ """
467
+
468
  # ============================================================================
469
  # SIMPLE GET ENDPOINTS
470
  # ============================================================================