Add .http file import support
Parse and import IntelliJ HTTP Client (.http/.rest) files into projects.
This commit is contained in:
parent
727d8bbab1
commit
728697711c
360
src/http_file_import_dialog.py
Normal file
360
src/http_file_import_dialog.py
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
# http_file_import_dialog.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
|
||||||
|
|
||||||
|
from gi.repository import Adw, Gtk, GObject, Gio
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .http_file_importer import (
|
||||||
|
parse_http_file, build_http_request,
|
||||||
|
HttpFileParseResult, HttpFileRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
_METHOD_CLASS = {
|
||||||
|
'GET': 'accent',
|
||||||
|
'POST': 'success',
|
||||||
|
'PUT': 'warning',
|
||||||
|
'PATCH': 'warning',
|
||||||
|
'DELETE': 'error',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HttpFileImportDialog(Adw.Dialog):
|
||||||
|
"""Two-step dialog: pick .http file → select requests → import."""
|
||||||
|
|
||||||
|
__gtype_name__ = 'HttpFileImportDialog'
|
||||||
|
|
||||||
|
__gsignals__ = {
|
||||||
|
'import-completed': (GObject.SIGNAL_RUN_FIRST, None, (str,)),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, project_manager, existing_projects):
|
||||||
|
super().__init__()
|
||||||
|
self.set_title("Import from .http File")
|
||||||
|
self.set_content_width(580)
|
||||||
|
self.set_content_height(560)
|
||||||
|
|
||||||
|
self._pm = project_manager
|
||||||
|
self._existing_projects = list(existing_projects)
|
||||||
|
self._parse_result: HttpFileParseResult | None = None
|
||||||
|
self._req_checkboxes: List[tuple[HttpFileRequest, Gtk.CheckButton]] = []
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ build
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
tv = Adw.ToolbarView()
|
||||||
|
tv.add_top_bar(Adw.HeaderBar())
|
||||||
|
|
||||||
|
self._stack = Gtk.Stack()
|
||||||
|
self._stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
|
||||||
|
self._stack.set_transition_duration(200)
|
||||||
|
self._stack.add_named(self._make_file_page(), "file")
|
||||||
|
self._stack.add_named(self._make_reqs_page(), "reqs")
|
||||||
|
tv.set_content(self._stack)
|
||||||
|
|
||||||
|
ab = Gtk.ActionBar()
|
||||||
|
|
||||||
|
self._back_btn = Gtk.Button(label="Back")
|
||||||
|
self._back_btn.connect("clicked", lambda _: self._show_file_page())
|
||||||
|
ab.pack_start(self._back_btn)
|
||||||
|
|
||||||
|
self._browse_btn = Gtk.Button(label="Choose File…")
|
||||||
|
self._browse_btn.add_css_class("suggested-action")
|
||||||
|
self._browse_btn.connect("clicked", self._on_browse)
|
||||||
|
ab.pack_end(self._browse_btn)
|
||||||
|
|
||||||
|
self._import_btn = Gtk.Button(label="Import")
|
||||||
|
self._import_btn.add_css_class("suggested-action")
|
||||||
|
self._import_btn.connect("clicked", self._on_import)
|
||||||
|
ab.pack_end(self._import_btn)
|
||||||
|
|
||||||
|
tv.add_bottom_bar(ab)
|
||||||
|
self.set_child(tv)
|
||||||
|
self._show_file_page()
|
||||||
|
|
||||||
|
def _make_file_page(self) -> Gtk.Widget:
|
||||||
|
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16)
|
||||||
|
box.set_valign(Gtk.Align.CENTER)
|
||||||
|
box.set_vexpand(True)
|
||||||
|
box.set_margin_top(32)
|
||||||
|
box.set_margin_bottom(24)
|
||||||
|
box.set_margin_start(24)
|
||||||
|
box.set_margin_end(24)
|
||||||
|
|
||||||
|
icon = Gtk.Image.new_from_icon_name("document-open-symbolic")
|
||||||
|
icon.set_pixel_size(64)
|
||||||
|
icon.add_css_class("dim-label")
|
||||||
|
box.append(icon)
|
||||||
|
|
||||||
|
title = Gtk.Label(label="Import from .http File")
|
||||||
|
title.add_css_class("title-2")
|
||||||
|
box.append(title)
|
||||||
|
|
||||||
|
subtitle = Gtk.Label(
|
||||||
|
label="Choose a .http or .rest file (IntelliJ HTTP Client format) to import its requests"
|
||||||
|
)
|
||||||
|
subtitle.add_css_class("dim-label")
|
||||||
|
subtitle.set_wrap(True)
|
||||||
|
subtitle.set_justify(Gtk.Justification.CENTER)
|
||||||
|
box.append(subtitle)
|
||||||
|
|
||||||
|
self._file_label = Gtk.Label()
|
||||||
|
self._file_label.add_css_class("monospace")
|
||||||
|
self._file_label.add_css_class("dim-label")
|
||||||
|
self._file_label.set_visible(False)
|
||||||
|
box.append(self._file_label)
|
||||||
|
|
||||||
|
self._err_label = Gtk.Label()
|
||||||
|
self._err_label.add_css_class("error")
|
||||||
|
self._err_label.set_wrap(True)
|
||||||
|
self._err_label.set_visible(False)
|
||||||
|
box.append(self._err_label)
|
||||||
|
|
||||||
|
return box
|
||||||
|
|
||||||
|
def _make_reqs_page(self) -> Gtk.Widget:
|
||||||
|
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||||
|
outer.set_margin_top(14)
|
||||||
|
outer.set_margin_bottom(14)
|
||||||
|
outer.set_margin_start(16)
|
||||||
|
outer.set_margin_end(16)
|
||||||
|
|
||||||
|
# File info row
|
||||||
|
file_grp = Adw.PreferencesGroup()
|
||||||
|
file_grp.set_title("File")
|
||||||
|
self._file_row = Adw.ActionRow()
|
||||||
|
self._file_row.set_title("Path")
|
||||||
|
file_grp.add(self._file_row)
|
||||||
|
outer.append(file_grp)
|
||||||
|
|
||||||
|
# Requests header with select-all button
|
||||||
|
reqs_hdr = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||||
|
reqs_hdr.set_margin_top(2)
|
||||||
|
reqs_lbl = Gtk.Label(label="Requests")
|
||||||
|
reqs_lbl.add_css_class("title-4")
|
||||||
|
reqs_lbl.set_hexpand(True)
|
||||||
|
reqs_lbl.set_xalign(0)
|
||||||
|
reqs_hdr.append(reqs_lbl)
|
||||||
|
self._sel_all_btn = Gtk.Button(label="Deselect All")
|
||||||
|
self._sel_all_btn.add_css_class("flat")
|
||||||
|
self._sel_all_btn.connect("clicked", self._on_select_all)
|
||||||
|
reqs_hdr.append(self._sel_all_btn)
|
||||||
|
outer.append(reqs_hdr)
|
||||||
|
|
||||||
|
scroll = Gtk.ScrolledWindow()
|
||||||
|
scroll.set_vexpand(True)
|
||||||
|
scroll.set_min_content_height(80)
|
||||||
|
self._reqs_list = Gtk.ListBox()
|
||||||
|
self._reqs_list.add_css_class("boxed-list")
|
||||||
|
self._reqs_list.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||||
|
scroll.set_child(self._reqs_list)
|
||||||
|
outer.append(scroll)
|
||||||
|
|
||||||
|
# Import target
|
||||||
|
tgt_grp = Adw.PreferencesGroup()
|
||||||
|
tgt_grp.set_title("Import to")
|
||||||
|
tgt_grp.set_margin_top(2)
|
||||||
|
|
||||||
|
new_row = Adw.ActionRow()
|
||||||
|
new_row.set_title("Create new project")
|
||||||
|
self._new_radio = Gtk.CheckButton()
|
||||||
|
self._new_radio.set_active(True)
|
||||||
|
new_row.add_prefix(self._new_radio)
|
||||||
|
new_row.set_activatable_widget(self._new_radio)
|
||||||
|
self._new_name_entry = Gtk.Entry()
|
||||||
|
self._new_name_entry.set_placeholder_text("Project name")
|
||||||
|
self._new_name_entry.set_hexpand(True)
|
||||||
|
self._new_name_entry.set_valign(Gtk.Align.CENTER)
|
||||||
|
new_row.add_suffix(self._new_name_entry)
|
||||||
|
tgt_grp.add(new_row)
|
||||||
|
|
||||||
|
if self._existing_projects:
|
||||||
|
exist_row = Adw.ActionRow()
|
||||||
|
exist_row.set_title("Add to existing project")
|
||||||
|
self._exist_radio = Gtk.CheckButton()
|
||||||
|
self._exist_radio.set_group(self._new_radio)
|
||||||
|
exist_row.add_prefix(self._exist_radio)
|
||||||
|
exist_row.set_activatable_widget(self._exist_radio)
|
||||||
|
proj_list = Gtk.StringList.new([p.name for p in self._existing_projects])
|
||||||
|
self._proj_dd = Gtk.DropDown(model=proj_list)
|
||||||
|
self._proj_dd.set_valign(Gtk.Align.CENTER)
|
||||||
|
self._proj_dd.set_sensitive(False)
|
||||||
|
exist_row.add_suffix(self._proj_dd)
|
||||||
|
tgt_grp.add(exist_row)
|
||||||
|
self._new_radio.connect("toggled", self._on_target_toggled)
|
||||||
|
else:
|
||||||
|
self._exist_radio = None
|
||||||
|
self._proj_dd = None
|
||||||
|
|
||||||
|
outer.append(tgt_grp)
|
||||||
|
return outer
|
||||||
|
|
||||||
|
# ----------------------------------------------------------- page control
|
||||||
|
|
||||||
|
def _show_file_page(self):
|
||||||
|
self._stack.set_visible_child_name("file")
|
||||||
|
self._back_btn.set_visible(False)
|
||||||
|
self._browse_btn.set_visible(True)
|
||||||
|
self._import_btn.set_visible(False)
|
||||||
|
|
||||||
|
def _show_reqs_page(self):
|
||||||
|
self._stack.set_visible_child_name("reqs")
|
||||||
|
self._back_btn.set_visible(True)
|
||||||
|
self._browse_btn.set_visible(False)
|
||||||
|
self._import_btn.set_visible(True)
|
||||||
|
|
||||||
|
# --------------------------------------------------------- event handlers
|
||||||
|
|
||||||
|
def _on_target_toggled(self, _radio):
|
||||||
|
is_new = self._new_radio.get_active()
|
||||||
|
self._new_name_entry.set_sensitive(is_new)
|
||||||
|
if self._proj_dd:
|
||||||
|
self._proj_dd.set_sensitive(not is_new)
|
||||||
|
|
||||||
|
def _on_select_all(self, _btn):
|
||||||
|
all_on = all(cb.get_active() for _, cb in self._req_checkboxes)
|
||||||
|
new_state = not all_on
|
||||||
|
for _, cb in self._req_checkboxes:
|
||||||
|
cb.set_active(new_state)
|
||||||
|
self._sel_all_btn.set_label("Deselect All" if new_state else "Select All")
|
||||||
|
|
||||||
|
def _on_browse(self, _btn):
|
||||||
|
self._err_label.set_visible(False)
|
||||||
|
|
||||||
|
chooser = Gtk.FileChooserNative(
|
||||||
|
title="Open HTTP File",
|
||||||
|
action=Gtk.FileChooserAction.OPEN,
|
||||||
|
accept_label="Open",
|
||||||
|
cancel_label="Cancel",
|
||||||
|
transient_for=self.get_root(),
|
||||||
|
)
|
||||||
|
|
||||||
|
http_filter = Gtk.FileFilter()
|
||||||
|
http_filter.set_name("HTTP files (*.http, *.rest)")
|
||||||
|
http_filter.add_pattern("*.http")
|
||||||
|
http_filter.add_pattern("*.rest")
|
||||||
|
chooser.add_filter(http_filter)
|
||||||
|
|
||||||
|
all_filter = Gtk.FileFilter()
|
||||||
|
all_filter.set_name("All files")
|
||||||
|
all_filter.add_pattern("*")
|
||||||
|
chooser.add_filter(all_filter)
|
||||||
|
|
||||||
|
chooser.connect("response", self._on_file_chosen)
|
||||||
|
chooser.show()
|
||||||
|
# Keep a reference so GC doesn't collect it
|
||||||
|
self._chooser = chooser
|
||||||
|
|
||||||
|
def _on_file_chosen(self, chooser, response):
|
||||||
|
if response != Gtk.ResponseType.ACCEPT:
|
||||||
|
return
|
||||||
|
|
||||||
|
gfile = chooser.get_file()
|
||||||
|
if not gfile:
|
||||||
|
return
|
||||||
|
|
||||||
|
path = gfile.get_path()
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = gfile.load_contents(None)[1].decode('utf-8', errors='replace')
|
||||||
|
except Exception as e:
|
||||||
|
self._set_err(f"Could not read file: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
parsed = parse_http_file(content)
|
||||||
|
|
||||||
|
if parsed.error:
|
||||||
|
self._set_err(parsed.error)
|
||||||
|
self._file_label.set_label(path or "")
|
||||||
|
self._file_label.set_visible(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._parse_result = parsed
|
||||||
|
self._current_path = path or ""
|
||||||
|
self._populate_reqs_page(parsed, path or "")
|
||||||
|
self._show_reqs_page()
|
||||||
|
|
||||||
|
def _on_import(self, _btn):
|
||||||
|
if not self._parse_result:
|
||||||
|
return
|
||||||
|
|
||||||
|
selected = [req for req, cb in self._req_checkboxes if cb.get_active()]
|
||||||
|
if not selected:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._new_radio.get_active():
|
||||||
|
name = self._new_name_entry.get_text().strip()
|
||||||
|
if not name:
|
||||||
|
return
|
||||||
|
project = self._pm.add_project(name)
|
||||||
|
project_id = project.id
|
||||||
|
elif self._exist_radio and self._exist_radio.get_active() and self._proj_dd:
|
||||||
|
idx = self._proj_dd.get_selected()
|
||||||
|
if idx >= len(self._existing_projects):
|
||||||
|
return
|
||||||
|
project_id = self._existing_projects[idx].id
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
for req in selected:
|
||||||
|
http_req = build_http_request(req)
|
||||||
|
self._pm.add_request(project_id, req.name, http_req)
|
||||||
|
|
||||||
|
self.emit('import-completed', project_id)
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------- helpers
|
||||||
|
|
||||||
|
def _set_err(self, msg: str):
|
||||||
|
self._err_label.set_text(msg)
|
||||||
|
self._err_label.set_visible(True)
|
||||||
|
|
||||||
|
def _populate_reqs_page(self, result: HttpFileParseResult, path: str):
|
||||||
|
self._file_row.set_subtitle(path)
|
||||||
|
|
||||||
|
while child := self._reqs_list.get_first_child():
|
||||||
|
self._reqs_list.remove(child)
|
||||||
|
self._req_checkboxes.clear()
|
||||||
|
|
||||||
|
for req in result.requests:
|
||||||
|
row = Adw.ActionRow()
|
||||||
|
row.set_title(req.name)
|
||||||
|
row.set_subtitle(f"{req.method} {req.url}")
|
||||||
|
|
||||||
|
badge = Gtk.Label(label=req.method)
|
||||||
|
badge.add_css_class("caption")
|
||||||
|
badge.add_css_class("monospace")
|
||||||
|
badge.add_css_class(_METHOD_CLASS.get(req.method, 'dim-label'))
|
||||||
|
badge.set_valign(Gtk.Align.CENTER)
|
||||||
|
row.add_suffix(badge)
|
||||||
|
|
||||||
|
cb = Gtk.CheckButton()
|
||||||
|
cb.set_active(True)
|
||||||
|
row.add_prefix(cb)
|
||||||
|
row.set_activatable_widget(cb)
|
||||||
|
|
||||||
|
self._reqs_list.append(row)
|
||||||
|
self._req_checkboxes.append((req, cb))
|
||||||
|
|
||||||
|
# Pre-fill project name from filename (stem of the path)
|
||||||
|
import os
|
||||||
|
stem = os.path.splitext(os.path.basename(path))[0]
|
||||||
|
self._new_name_entry.set_text(stem)
|
||||||
|
self._sel_all_btn.set_label("Deselect All")
|
||||||
188
src/http_file_importer.py
Normal file
188
src/http_file_importer.py
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
# http_file_importer.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 re
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
HTTP_METHODS = frozenset({
|
||||||
|
'GET', 'POST', 'PUT', 'DELETE', 'PATCH',
|
||||||
|
'HEAD', 'OPTIONS', 'TRACE', 'CONNECT',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HttpFileRequest:
|
||||||
|
name: str
|
||||||
|
method: str
|
||||||
|
url: str
|
||||||
|
headers: Dict[str, str]
|
||||||
|
body: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HttpFileParseResult:
|
||||||
|
requests: List[HttpFileRequest] = field(default_factory=list)
|
||||||
|
variables: Dict[str, str] = field(default_factory=dict)
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_http_file(content: str) -> HttpFileParseResult:
|
||||||
|
"""Parse an IntelliJ .http / .rest file into a list of requests."""
|
||||||
|
result = HttpFileParseResult()
|
||||||
|
|
||||||
|
if not content or not content.strip():
|
||||||
|
result.error = "File is empty"
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Split file into named blocks at each "###" separator line
|
||||||
|
blocks: list[tuple[str, list[str]]] = []
|
||||||
|
current_name = ""
|
||||||
|
current_lines: list[str] = []
|
||||||
|
|
||||||
|
for line in content.splitlines():
|
||||||
|
if re.match(r'^###', line):
|
||||||
|
blocks.append((current_name, current_lines))
|
||||||
|
current_name = line[3:].strip()
|
||||||
|
current_lines = []
|
||||||
|
else:
|
||||||
|
current_lines.append(line)
|
||||||
|
blocks.append((current_name, current_lines))
|
||||||
|
|
||||||
|
for block_name, block_lines in blocks:
|
||||||
|
_collect_variables(block_lines, result.variables)
|
||||||
|
req = _parse_block(block_name, block_lines)
|
||||||
|
if req is not None:
|
||||||
|
result.requests.append(req)
|
||||||
|
|
||||||
|
if not result.requests:
|
||||||
|
result.error = "No HTTP requests found in this file"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def build_http_request(req: HttpFileRequest):
|
||||||
|
"""Convert an HttpFileRequest into an HttpRequest model object."""
|
||||||
|
from .models import HttpRequest
|
||||||
|
|
||||||
|
body = req.body
|
||||||
|
syntax = 'RAW'
|
||||||
|
if body.lstrip().startswith('{'):
|
||||||
|
syntax = 'JSON'
|
||||||
|
elif body.lstrip().startswith('<') and not body.lstrip().startswith('< '):
|
||||||
|
syntax = 'XML'
|
||||||
|
|
||||||
|
return HttpRequest(
|
||||||
|
method=req.method,
|
||||||
|
url=req.url,
|
||||||
|
headers=req.headers,
|
||||||
|
body=body,
|
||||||
|
syntax=syntax,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Internal helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _collect_variables(lines: list[str], variables: dict) -> None:
|
||||||
|
"""Extract @name = value file-level variable definitions."""
|
||||||
|
for line in lines:
|
||||||
|
m = re.match(r'^@(\w+)\s*=\s*(.+)', line.strip())
|
||||||
|
if m:
|
||||||
|
variables[m.group(1)] = m.group(2).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_block(name: str, lines: list[str]) -> Optional[HttpFileRequest]:
|
||||||
|
"""Parse one request block. Returns None if no valid request line found."""
|
||||||
|
# Find the request line (METHOD URL), skipping comments and @variables
|
||||||
|
req_idx: Optional[int] = None
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped:
|
||||||
|
continue
|
||||||
|
if stripped.startswith('#') or stripped.startswith('//'):
|
||||||
|
continue
|
||||||
|
if stripped.startswith('@'):
|
||||||
|
continue
|
||||||
|
parts = stripped.split(None, 1)
|
||||||
|
if parts and parts[0].upper() in HTTP_METHODS:
|
||||||
|
req_idx = i
|
||||||
|
break
|
||||||
|
# Any other non-blank, non-comment line before METHOD — not a request block
|
||||||
|
break
|
||||||
|
|
||||||
|
if req_idx is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
method, url = _split_request_line(lines[req_idx].strip())
|
||||||
|
|
||||||
|
# Parse headers: lines after req_idx until blank line or response handler
|
||||||
|
headers: Dict[str, str] = {}
|
||||||
|
body_start = len(lines)
|
||||||
|
i = req_idx + 1
|
||||||
|
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i]
|
||||||
|
stripped = line.strip()
|
||||||
|
|
||||||
|
if not stripped:
|
||||||
|
body_start = i + 1
|
||||||
|
break
|
||||||
|
if stripped.startswith('#') or stripped.startswith('//'):
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if stripped.startswith('>'):
|
||||||
|
break
|
||||||
|
if ':' in stripped:
|
||||||
|
key, _, value = stripped.partition(':')
|
||||||
|
headers[key.strip()] = value.strip()
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Parse body: everything until a response handler line or end
|
||||||
|
body_lines: list[str] = []
|
||||||
|
for line in lines[body_start:]:
|
||||||
|
if line.strip().startswith('>'):
|
||||||
|
break
|
||||||
|
# Skip file-include lines (< ./file.json) — not supported
|
||||||
|
if re.match(r'^<\s+\S', line):
|
||||||
|
continue
|
||||||
|
body_lines.append(line)
|
||||||
|
|
||||||
|
# Strip trailing blank lines from body
|
||||||
|
while body_lines and not body_lines[-1].strip():
|
||||||
|
body_lines.pop()
|
||||||
|
|
||||||
|
body = '\n'.join(body_lines)
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
name = f"{method} {_shorten(url)}"
|
||||||
|
|
||||||
|
return HttpFileRequest(name=name, method=method, url=url, headers=headers, body=body)
|
||||||
|
|
||||||
|
|
||||||
|
def _split_request_line(line: str) -> tuple[str, str]:
|
||||||
|
parts = line.split(None, 1)
|
||||||
|
method = parts[0].upper()
|
||||||
|
url = parts[1].strip() if len(parts) > 1 else ""
|
||||||
|
return method, url
|
||||||
|
|
||||||
|
|
||||||
|
def _shorten(url: str, limit: int = 60) -> str:
|
||||||
|
return url if len(url) <= limit else url[:limit - 1] + '…'
|
||||||
@ -13,6 +13,10 @@
|
|||||||
<attribute name="label">Import from WSDL</attribute>
|
<attribute name="label">Import from WSDL</attribute>
|
||||||
<attribute name="action">win.import-wsdl</attribute>
|
<attribute name="action">win.import-wsdl</attribute>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="label">Import from .http File</attribute>
|
||||||
|
<attribute name="action">win.import-http-file</attribute>
|
||||||
|
</item>
|
||||||
</section>
|
</section>
|
||||||
</menu>
|
</menu>
|
||||||
|
|
||||||
|
|||||||
@ -48,6 +48,8 @@ roster_sources = [
|
|||||||
'wsdl_import_dialog.py',
|
'wsdl_import_dialog.py',
|
||||||
'openapi_importer.py',
|
'openapi_importer.py',
|
||||||
'openapi_import_dialog.py',
|
'openapi_import_dialog.py',
|
||||||
|
'http_file_importer.py',
|
||||||
|
'http_file_import_dialog.py',
|
||||||
]
|
]
|
||||||
|
|
||||||
install_data(roster_sources, install_dir: moduledir)
|
install_data(roster_sources, install_dir: moduledir)
|
||||||
|
|||||||
@ -38,6 +38,7 @@ from .icon_picker_dialog import IconPickerDialog
|
|||||||
from .environments_dialog import EnvironmentsDialog
|
from .environments_dialog import EnvironmentsDialog
|
||||||
from .wsdl_import_dialog import WsdlImportDialog
|
from .wsdl_import_dialog import WsdlImportDialog
|
||||||
from .openapi_import_dialog import OpenApiImportDialog
|
from .openapi_import_dialog import OpenApiImportDialog
|
||||||
|
from .http_file_import_dialog import HttpFileImportDialog
|
||||||
from .request_tab_widget import RequestTabWidget
|
from .request_tab_widget import RequestTabWidget
|
||||||
from .widgets.history_item import HistoryItem
|
from .widgets.history_item import HistoryItem
|
||||||
from .widgets.project_item import ProjectItem
|
from .widgets.project_item import ProjectItem
|
||||||
@ -503,6 +504,10 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
action.connect("activate", lambda a, p: self.on_import_wsdl_clicked(None))
|
action.connect("activate", lambda a, p: self.on_import_wsdl_clicked(None))
|
||||||
self.add_action(action)
|
self.add_action(action)
|
||||||
|
|
||||||
|
action = Gio.SimpleAction.new("import-http-file", None)
|
||||||
|
action.connect("activate", lambda a, p: self.on_import_http_file_clicked(None))
|
||||||
|
self.add_action(action)
|
||||||
|
|
||||||
def _on_send_clicked(self, widget):
|
def _on_send_clicked(self, widget):
|
||||||
"""Handle Send button click from a tab widget."""
|
"""Handle Send button click from a tab widget."""
|
||||||
# Clear previous preprocessing results
|
# Clear previous preprocessing results
|
||||||
@ -1422,6 +1427,22 @@ class RosterWindow(Adw.ApplicationWindow):
|
|||||||
widget.original_request = None
|
widget.original_request = None
|
||||||
widget.modified = True
|
widget.modified = True
|
||||||
|
|
||||||
|
def on_import_http_file_clicked(self, button):
|
||||||
|
"""Open the .http file import dialog."""
|
||||||
|
projects = self.project_manager.load_projects()
|
||||||
|
dialog = HttpFileImportDialog(self.project_manager, projects)
|
||||||
|
dialog.connect('import-completed', self._on_http_file_import_completed)
|
||||||
|
dialog.present(self)
|
||||||
|
|
||||||
|
def _on_http_file_import_completed(self, dialog, project_id: str):
|
||||||
|
"""Refresh sidebar after a successful .http file import."""
|
||||||
|
self._load_projects()
|
||||||
|
projects = self.project_manager.load_projects()
|
||||||
|
for p in projects:
|
||||||
|
if p.id == project_id:
|
||||||
|
self._show_toast(f"Imported {len(p.requests)} request(s) into '{p.name}'")
|
||||||
|
break
|
||||||
|
|
||||||
def on_import_wsdl_clicked(self, button):
|
def on_import_wsdl_clicked(self, button):
|
||||||
"""Open the WSDL import dialog."""
|
"""Open the WSDL import dialog."""
|
||||||
projects = self.project_manager.load_projects()
|
projects = self.project_manager.load_projects()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user