roster/src/http_client.py
vesp 95d5197e26 Initial commit: Roster HTTP client for GNOME
Roster is a modern HTTP client application for GNOME, similar to Postman,
built with GTK 4 and libadwaita.

Features:
- Send HTTP requests (GET, POST, PUT, DELETE)
- Configure custom headers with add/remove functionality
- Request body editor for POST/PUT/DELETE requests
- View response headers and bodies in separate tabs
- Track request history with JSON persistence
- Load previous requests from history with confirmation dialog
- Beautiful GNOME-native UI with libadwaita components
- HTTPie backend for reliable HTTP communication

Technical implementation:
- Python 3 with GTK 4 and libadwaita 1
- Meson build system with Flatpak support
- Custom widgets (HeaderRow, HistoryItem) with GObject signals
- Background threading for non-blocking HTTP requests
- AdwTabView for modern tabbed interface
- History persistence to ~/.config/roster/history.json
- Comprehensive error handling and user feedback
2025-12-18 11:05:33 +01:00

142 lines
4.8 KiB
Python

# http_client.py
#
# Copyright 2025 Pavel Baksy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
import subprocess
import time
from typing import Tuple, Optional
from .models import HttpRequest, HttpResponse
class HttpClient:
"""HTTP client using HTTPie as backend."""
def execute_request(self, request: HttpRequest) -> Tuple[Optional[HttpResponse], Optional[str]]:
"""
Execute HTTP request using HTTPie.
Args:
request: HttpRequest object with method, url, headers, body
Returns:
Tuple of (response, error_message)
If successful: (HttpResponse, None)
If failed: (None, error_message)
"""
# Build command
cmd = ['http', '--print=HhBb', request.method, request.url]
# Add headers
for key, value in request.headers.items():
if key and value: # Skip empty headers
cmd.append(f'{key}:{value}')
# Prepare stdin for body (if POST/PUT/DELETE and body exists)
stdin_data = None
if request.method in ['POST', 'PUT', 'DELETE'] and request.body.strip():
stdin_data = request.body.encode('utf-8')
else:
# Only use --ignore-stdin when there's no body
cmd.insert(2, '--ignore-stdin')
# Execute with timing
start_time = time.time()
try:
result = subprocess.run(
cmd,
input=stdin_data,
capture_output=True,
timeout=30, # 30 second timeout
text=False # We'll decode manually
)
end_time = time.time()
response_time = (end_time - start_time) * 1000 # Convert to ms
# Parse output
output = result.stdout.decode('utf-8', errors='replace')
if result.returncode != 0:
error_msg = result.stderr.decode('utf-8', errors='replace')
return None, error_msg
# Parse HTTPie output
response = self._parse_httpie_output(output, response_time)
return response, None
except subprocess.TimeoutExpired:
return None, "Request timeout (30 seconds)"
except FileNotFoundError:
return None, "HTTPie not installed. Please install with: pip install httpie"
except Exception as e:
return None, f"Error: {str(e)}"
def _parse_httpie_output(self, output: str, response_time: float) -> HttpResponse:
"""
Parse HTTPie --print=HhBb output format.
Format:
REQUEST HEADERS
<blank line>
REQUEST BODY
<blank line>
<blank line>
RESPONSE HEADERS (starts with HTTP/1.1 200 OK)
<blank line>
RESPONSE BODY
"""
# Split output into lines to manually find response section
lines = output.split('\n')
# Find where response headers start (line starting with HTTP/)
response_header_start = 0
for i, line in enumerate(lines):
if line.startswith('HTTP/'):
response_header_start = i
break
# Parse status line
status_line = lines[response_header_start]
parts = status_line.split(' ', 2)
if len(parts) < 2:
raise ValueError(f"Invalid status line: {status_line}")
status_code = int(parts[1])
status_text = parts[2] if len(parts) > 2 else ''
# Find where response headers end (first empty line after response starts)
response_body_start = response_header_start + 1
for i in range(response_header_start + 1, len(lines)):
if lines[i].strip() == '':
response_body_start = i + 1
break
# Extract response headers (from HTTP/ line to first blank line)
response_headers = '\n'.join(lines[response_header_start:response_body_start - 1])
# Extract response body (everything after the blank line)
response_body = '\n'.join(lines[response_body_start:])
return HttpResponse(
status_code=status_code,
status_text=status_text,
headers=response_headers,
body=response_body.strip(),
response_time_ms=response_time
)