Initial commit: Add http-convert project
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-01-29 11:34:25 +00:00
parent e7de95c0dd
commit d43572b7f1

576
src/http_convert/web.py Normal file
View File

@@ -0,0 +1,576 @@
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from pathlib import Path
from typing import Optional, Dict, Any, List
from .models import HTTPRequest, HttpMethod, OutputFormat
from .generators import Generator
from .parsers import Parser
from .highlighter import SyntaxHighlighter
app = FastAPI(title="HTTP Convert", description="HTTP Request Format Converter - Web Interface")
static_path = Path(__file__).parent.parent / "static"
if static_path.exists():
app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTTP Convert - Request Builder</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
color: #fff;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 30px;
}
h1 {
font-size: 2.5rem;
background: linear-gradient(90deg, #00d4ff, #7b2cbf);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
color: #888;
margin-top: 10px;
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
@media (max-width: 1024px) {
.main-content {
grid-template-columns: 1fr;
}
}
.panel {
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
padding: 25px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.panel-title {
font-size: 1.2rem;
margin-bottom: 20px;
color: #00d4ff;
display: flex;
align-items: center;
gap: 10px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #ccc;
}
input, select, textarea {
width: 100%;
padding: 12px 16px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 14px;
transition: all 0.3s ease;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #00d4ff;
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1);
}
textarea {
min-height: 120px;
font-family: 'Monaco', 'Consolas', monospace;
}
.row {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 15px;
}
.btn {
padding: 14px 28px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(90deg, #00d4ff, #7b2cbf);
color: #fff;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(0, 212, 255, 0.3);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.2);
}
.output-tabs {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.tab {
padding: 10px 20px;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 8px;
color: #fff;
cursor: pointer;
transition: all 0.3s ease;
}
.tab.active {
background: linear-gradient(90deg, #00d4ff, #7b2cbf);
}
.tab:hover:not(.active) {
background: rgba(255, 255, 255, 0.2);
}
.output-area {
background: #1e1e1e;
border-radius: 8px;
padding: 20px;
min-height: 200px;
font-family: 'Monaco', 'Consolas', monospace;
font-size: 14px;
overflow-x: auto;
position: relative;
}
.copy-btn {
position: absolute;
top: 10px;
right: 10px;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 6px;
color: #fff;
cursor: pointer;
font-size: 12px;
}
.copy-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.copy-btn.copied {
background: #4caf50;
}
.headers-section {
margin-top: 15px;
}
.header-row {
display: grid;
grid-template-columns: 1fr 1fr 40px;
gap: 10px;
margin-bottom: 10px;
align-items: center;
}
.add-header-btn {
width: 100%;
padding: 10px;
background: rgba(255, 255, 255, 0.05);
border: 1px dashed rgba(255, 255, 255, 0.3);
border-radius: 8px;
color: #888;
cursor: pointer;
transition: all 0.3s ease;
}
.add-header-btn:hover {
border-color: #00d4ff;
color: #00d4ff;
}
.remove-header {
background: #ff4757;
border: none;
border-radius: 6px;
color: #fff;
cursor: pointer;
padding: 8px;
font-size: 16px;
}
.quick-actions {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.quick-btn {
padding: 8px 16px;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 6px;
color: #fff;
cursor: pointer;
font-size: 13px;
transition: all 0.3s ease;
}
.quick-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.input-section {
margin-bottom: 20px;
}
.input-tabs {
display: flex;
gap: 5px;
margin-bottom: 10px;
}
.input-tab {
padding: 8px 16px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px 6px 0 0;
color: #888;
cursor: pointer;
font-size: 13px;
}
.input-tab.active {
background: rgba(255, 255, 255, 0.1);
color: #fff;
border-bottom-color: transparent;
}
.parser-area {
min-height: 150px;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>HTTP Convert</h1>
<p class="subtitle">Convert HTTP requests between cURL, HTTPie, fetch, and axios formats</p>
</header>
<div class="main-content">
<div class="left-column">
<div class="panel">
<div class="panel-title">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Build Request
</div>
<div class="form-group">
<label>HTTP Method</label>
<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>
</div>
<div class="form-group">
<label>URL</label>
<input type="text" id="url" placeholder="https://api.example.com/users">
</div>
<div class="form-group">
<label>Headers</label>
<div id="headers-container"></div>
<button class="add-header-btn" onclick="addHeader()">+ Add Header</button>
</div>
<div class="form-group">
<label>Body (optional)</label>
<textarea id="body" placeholder="Enter request body (JSON, form data, etc.)"></textarea>
</div>
<button class="btn btn-primary" onclick="generateOutput()">Generate Output</button>
</div>
<div class="panel" style="margin-top: 20px;">
<div class="panel-title">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
Parse Input
</div>
<div class="input-tabs">
<button class="input-tab active" onclick="switchInputTab('curl')">cURL</button>
<button class="input-tab" onclick="switchInputTab('httpie')">HTTPie</button>
<button class="input-tab" onclick="switchInputTab('fetch')">fetch</button>
<button class="input-tab" onclick="switchInputTab('axios')">axios</button>
</div>
<textarea class="parser-area" id="parser-input" placeholder="Paste your cURL, HTTPie, fetch, or axios command here..."></textarea>
<button class="btn btn-secondary" style="margin-top: 15px;" onclick="parseInput()">Parse Input</button>
</div>
</div>
<div class="right-column">
<div class="panel">
<div class="panel-title">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Converted Output
</div>
<div class="output-tabs" id="output-tabs">
<button class="tab active" onclick="switchOutputTab('curl')">cURL</button>
<button class="tab" onclick="switchOutputTab('httpie')">HTTPie</button>
<button class="tab" onclick="switchOutputTab('fetch')">fetch</button>
<button class="tab" onclick="switchOutputTab('axios')">axios</button>
</div>
<div class="output-area" id="output-content">
<button class="copy-btn" onclick="copyOutput(this)">Copy</button>
<pre id="output-text">Generated output will appear here...</pre>
</div>
<div style="margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap;">
<button class="quick-btn" onclick="loadExample('simple')">Simple GET</button>
<button class="quick-btn" onclick="loadExample('post')">POST with JSON</button>
<button class="quick-btn" onclick="loadExample('auth')">With Auth</button>
<button class="quick-btn" onclick="clearAll()">Clear All</button>
</div>
</div>
</div>
</div>
</div>
<script>
let currentOutputFormat = 'curl';
let currentInputFormat = 'curl';
let parsedData = null;
function addHeader(key = '', value = '') {
const container = document.getElementById('headers-container');
const row = document.createElement('div');
row.className = 'header-row';
row.innerHTML = `
<input type="text" placeholder="Header name" value="${key}" class="header-key">
<input type="text" placeholder="Value" value="${value}" class="header-value">
<button class="remove-header" onclick="this.parentElement.remove()">×</button>
`;
container.appendChild(row);
}
function getHeaders() {
const headers = {};
document.querySelectorAll('.header-row').forEach(row => {
const key = row.querySelector('.header-key').value.trim();
const value = row.querySelector('.header-value').value.trim();
if (key) headers[key] = value;
});
return headers;
}
function generateOutput() {
const method = document.getElementById('method').value;
const url = document.getElementById('url').value;
const headers = getHeaders();
const body = document.getElementById('body').value;
fetch('/api/convert', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
method,
url,
headers,
body: body || null,
format: currentOutputFormat
})
})
.then(r => r.json())
.then(data => {
document.getElementById('output-text').textContent = data.result;
})
.catch(err => {
document.getElementById('output-text').textContent = 'Error: ' + err.message;
});
}
function parseInput() {
const input = document.getElementById('parser-input').value;
fetch('/api/parse', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
input,
format: currentInputFormat
})
})
.then(r => r.json())
.then(data => {
if (data.error) {
alert('Parse error: ' + data.error);
return;
}
parsedData = data;
document.getElementById('method').value = data.method;
document.getElementById('url').value = data.url;
document.getElementById('body').value = data.body || '';
document.getElementById('headers-container').innerHTML = '';
Object.entries(data.headers || {}).forEach(([k, v]) => addHeader(k, v));
generateOutput();
})
.catch(err => {
alert('Error: ' + err.message);
});
}
function switchOutputTab(format) {
currentOutputFormat = format;
document.querySelectorAll('#output-tabs .tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
if (parsedData || document.getElementById('url').value) {
generateOutput();
}
}
function switchInputTab(format) {
currentInputFormat = format;
document.querySelectorAll('.input-tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
}
function copyOutput(btn) {
navigator.clipboard.writeText(document.getElementById('output-text').textContent)
.then(() => {
btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(() => {
btn.textContent = 'Copy';
btn.classList.remove('copied');
}, 2000);
});
}
function loadExample(type) {
if (type === 'simple') {
document.getElementById('method').value = 'GET';
document.getElementById('url').value = 'https://api.example.com/users';
document.getElementById('body').value = '';
document.getElementById('headers-container').innerHTML = '';
} else if (type === 'post') {
document.getElementById('method').value = 'POST';
document.getElementById('url').value = 'https://api.example.com/users';
document.getElementById('body').value = JSON.stringify({name: 'John', email: 'john@example.com'}, null, 2);
document.getElementById('headers-container').innerHTML = '';
addHeader('Content-Type', 'application/json');
} else if (type === 'auth') {
document.getElementById('method').value = 'GET';
document.getElementById('url').value = 'https://api.example.com/protected';
document.getElementById('body').value = '';
document.getElementById('headers-container').innerHTML = '';
addHeader('Authorization', 'Bearer YOUR_TOKEN');
addHeader('Accept', 'application/json');
}
generateOutput();
}
function clearAll() {
document.getElementById('method').value = 'GET';
document.getElementById('url').value = '';
document.getElementById('body').value = '';
document.getElementById('headers-container').innerHTML = '';
document.getElementById('parser-input').value = '';
document.getElementById('output-text').textContent = 'Generated output will appear here...';
parsedData = null;
}
document.addEventListener('DOMContentLoaded', () => {
addHeader();
});
</script>
</body>
</html>
"""
@app.get("/", response_class=HTMLResponse)
async def root():
return HTML_TEMPLATE
class ConvertRequest(BaseModel):
method: str
url: str
headers: Dict[str, str] = {}
body: Optional[str] = None
format: str = "curl"
class ParseRequest(BaseModel):
input: str
format: str
@app.post("/api/convert")
async def convert(request: ConvertRequest):
try:
http_request = HTTPRequest(
method=HttpMethod(request.method),
url=request.url,
headers=request.headers,
body=request.body
)
output = Generator.generate(http_request, OutputFormat(request.format))
return {"result": output}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/api/parse")
async def parse(request: ParseRequest):
try:
http_request = Parser.parse(request.input, request.format)
return {
"method": http_request.method.value,
"url": http_request.url,
"headers": http_request.headers,
"params": http_request.params,
"body": http_request.body,
"body_json": http_request.body_json
}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/api/formats")
async def get_formats():
return {
"formats": [f.value for f in OutputFormat]
}