pyhttp-app 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyhttp/__init__.py +9 -0
- pyhttp/__main__.py +222 -0
- pyhttp/app.py +1333 -0
- pyhttp/assertions.py +289 -0
- pyhttp/cli.py +364 -0
- pyhttp/console.py +82 -0
- pyhttp/curl.py +151 -0
- pyhttp/db.py +1483 -0
- pyhttp/jsengine.py +224 -0
- pyhttp/jsonc.py +54 -0
- pyhttp/mcp.py +316 -0
- pyhttp/mock.py +204 -0
- pyhttp/monitors.py +116 -0
- pyhttp/oauth2.py +153 -0
- pyhttp/openapi.py +365 -0
- pyhttp/pm.js +496 -0
- pyhttp/postman.py +412 -0
- pyhttp/runner.py +1089 -0
- pyhttp/scripts.py +352 -0
- pyhttp/static/app.js +2739 -0
- pyhttp/static/codegen.js +134 -0
- pyhttp/static/editor.js +421 -0
- pyhttp/static/features.css +630 -0
- pyhttp/static/features.js +2396 -0
- pyhttp/static/index.html +372 -0
- pyhttp/static/logo.svg +13 -0
- pyhttp/static/style.css +585 -0
- pyhttp/static/vendor/codemirror.js +28 -0
- pyhttp/static/vendor/fonts/Inter-OFL.txt +93 -0
- pyhttp/static/vendor/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
- pyhttp/static/vendor/fonts/inter-latin-wght-normal.woff2 +0 -0
- pyhttp/static/viewer.js +434 -0
- pyhttp/templating.py +155 -0
- pyhttp/updater.py +387 -0
- pyhttp_app-2.0.1.dist-info/METADATA +654 -0
- pyhttp_app-2.0.1.dist-info/RECORD +40 -0
- pyhttp_app-2.0.1.dist-info/WHEEL +5 -0
- pyhttp_app-2.0.1.dist-info/entry_points.txt +2 -0
- pyhttp_app-2.0.1.dist-info/licenses/LICENSE.md +121 -0
- pyhttp_app-2.0.1.dist-info/top_level.txt +1 -0
pyhttp/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""pyhttp - a local, Postman-like HTTP client with a collection runner."""
|
|
2
|
+
|
|
3
|
+
__version__ = "2.0.1"
|
|
4
|
+
# sent with every request unless the user sets their own in Settings; the convention is "product/version (+url)"
|
|
5
|
+
USER_AGENT = f"pyhttp/{__version__}"
|
|
6
|
+
__license__ = "pyhttp Free License 1.0 (free for everyone; source not distributed)"
|
|
7
|
+
__copyright__ = "Copyright (c) 2026 pyhttp Dev Team"
|
|
8
|
+
__author__ = "pyhttp Dev Team"
|
|
9
|
+
__tagline__ = "From the unknown to the unowned"
|
pyhttp/__main__.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Command line entry point.
|
|
2
|
+
|
|
3
|
+
python -m pyhttp # native desktop window (default)
|
|
4
|
+
python -m pyhttp --browser # serve the UI and open it in the browser
|
|
5
|
+
python -m pyhttp --server # serve only, no UI (for scripting via the API)
|
|
6
|
+
python -m pyhttp run [targets] # run requests headless and report (for CI); see `run --help`
|
|
7
|
+
python -m pyhttp mcp # MCP server on stdio, so AI tools can manage the collection
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import socket
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
import webbrowser
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import uvicorn
|
|
20
|
+
|
|
21
|
+
from . import __version__, updater
|
|
22
|
+
from .app import create_app
|
|
23
|
+
|
|
24
|
+
FROZEN = bool(getattr(sys, "frozen", False)) # running from a packaged .exe / .app
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _user_data_dir() -> Path:
|
|
28
|
+
"""Per-user, writable data folder for the packaged app."""
|
|
29
|
+
if sys.platform.startswith("win"):
|
|
30
|
+
import os
|
|
31
|
+
return Path(os.environ.get("APPDATA") or Path.home() / "AppData" / "Roaming") / "pyhttp"
|
|
32
|
+
if sys.platform == "darwin":
|
|
33
|
+
return Path.home() / "Library" / "Application Support" / "pyhttp"
|
|
34
|
+
import os
|
|
35
|
+
return Path(os.environ.get("XDG_DATA_HOME") or Path.home() / ".local" / "share") / "pyhttp"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# From source: ./data next to the project (unchanged). Packaged: the per-user folder,
|
|
39
|
+
# because the app's own files are read-only (macOS .app) or temporary (onefile .exe).
|
|
40
|
+
DEFAULT_DATA_DIR = _user_data_dir() if FROZEN else Path(__file__).resolve().parent.parent / "data"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _redirect_output_when_windowed(data_dir: Path) -> None:
|
|
44
|
+
"""A windowed build has no console (stdout/stderr are None); log to a file instead."""
|
|
45
|
+
if sys.stdout is not None and sys.stderr is not None:
|
|
46
|
+
return
|
|
47
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
log = open(data_dir / "pyhttp.log", "a", encoding="utf-8", buffering=1) # noqa: SIM115
|
|
49
|
+
sys.stdout = sys.stdout or log
|
|
50
|
+
sys.stderr = sys.stderr or log
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _free_port(host: str) -> int:
|
|
54
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
55
|
+
s.bind((host, 0))
|
|
56
|
+
return s.getsockname()[1]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _preferred_port(host: str, preferred: int) -> int:
|
|
60
|
+
"""The usual port when it is free, any free one otherwise. The window mode used
|
|
61
|
+
to always take a random port; keeping it steady means mock server URLs
|
|
62
|
+
(http://host:<port>/mock/...) stay the same from launch to launch."""
|
|
63
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
64
|
+
try:
|
|
65
|
+
s.bind((host, preferred))
|
|
66
|
+
except OSError:
|
|
67
|
+
return _free_port(host)
|
|
68
|
+
return preferred
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _start_server_thread(app, host: str, port: int) -> uvicorn.Server:
|
|
72
|
+
server = uvicorn.Server(uvicorn.Config(app, host=host, port=port, log_level="warning"))
|
|
73
|
+
thread = threading.Thread(target=server.run, daemon=True)
|
|
74
|
+
thread.start()
|
|
75
|
+
deadline = time.time() + 15
|
|
76
|
+
while not server.started:
|
|
77
|
+
if not thread.is_alive() or time.time() > deadline:
|
|
78
|
+
raise RuntimeError(f"Could not start the local server on {host}:{port}")
|
|
79
|
+
time.sleep(0.05)
|
|
80
|
+
return server
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class NativeApi:
|
|
84
|
+
"""Methods callable from JavaScript as `window.pywebview.api.<name>(...)`."""
|
|
85
|
+
|
|
86
|
+
def __init__(self) -> None:
|
|
87
|
+
self._window = None # private: pywebview does not expose _attrs to JS
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _types_for(filename: str) -> tuple[str, ...]:
|
|
91
|
+
ext = Path(filename).suffix.lower()
|
|
92
|
+
named = {
|
|
93
|
+
".json": "JSON files (*.json)", ".html": "HTML files (*.html)", ".xml": "XML files (*.xml)",
|
|
94
|
+
".txt": "Text files (*.txt)", ".csv": "CSV files (*.csv)", ".png": "PNG images (*.png)",
|
|
95
|
+
".jpg": "JPEG images (*.jpg)", ".pdf": "PDF files (*.pdf)",
|
|
96
|
+
}
|
|
97
|
+
first = named.get(ext)
|
|
98
|
+
return (first, "All files (*.*)") if first else ("All files (*.*)",)
|
|
99
|
+
|
|
100
|
+
def save_file(self, content: str, filename: str) -> str | None:
|
|
101
|
+
import webview
|
|
102
|
+
|
|
103
|
+
result = self._window.create_file_dialog(
|
|
104
|
+
webview.FileDialog.SAVE, save_filename=filename, file_types=self._types_for(filename),
|
|
105
|
+
)
|
|
106
|
+
if not result:
|
|
107
|
+
return None
|
|
108
|
+
path = result if isinstance(result, str) else result[0]
|
|
109
|
+
Path(path).write_text(content, encoding="utf-8")
|
|
110
|
+
return path
|
|
111
|
+
|
|
112
|
+
def save_file_b64(self, content_b64: str, filename: str) -> str | None:
|
|
113
|
+
"""Save binary content (base64 from the UI), e.g. an image response."""
|
|
114
|
+
import base64
|
|
115
|
+
import webview
|
|
116
|
+
|
|
117
|
+
result = self._window.create_file_dialog(
|
|
118
|
+
webview.FileDialog.SAVE, save_filename=filename, file_types=self._types_for(filename),
|
|
119
|
+
)
|
|
120
|
+
if not result:
|
|
121
|
+
return None
|
|
122
|
+
path = result if isinstance(result, str) else result[0]
|
|
123
|
+
Path(path).write_bytes(base64.b64decode(content_b64))
|
|
124
|
+
return path
|
|
125
|
+
|
|
126
|
+
def open_file(self, kind: str = "json") -> dict | None:
|
|
127
|
+
import webview
|
|
128
|
+
|
|
129
|
+
types = ("Variables files (*.csv;*.json)", "All files (*.*)") if kind == "data" else \
|
|
130
|
+
("Collections, OpenAPI, HAR or cURL (*.json;*.yaml;*.yml;*.har;*.txt;*.sh)", "All files (*.*)")
|
|
131
|
+
result = self._window.create_file_dialog(webview.FileDialog.OPEN, file_types=types)
|
|
132
|
+
if not result:
|
|
133
|
+
return None
|
|
134
|
+
path = result if isinstance(result, str) else result[0]
|
|
135
|
+
return {"name": Path(path).name, "content": Path(path).read_text(encoding="utf-8")}
|
|
136
|
+
|
|
137
|
+
def pick_file(self) -> str | None:
|
|
138
|
+
"""Choose a file and return its path (for file fields and binary bodies)."""
|
|
139
|
+
import webview
|
|
140
|
+
|
|
141
|
+
result = self._window.create_file_dialog(webview.FileDialog.OPEN, file_types=("All files (*.*)",))
|
|
142
|
+
if not result:
|
|
143
|
+
return None
|
|
144
|
+
return result if isinstance(result, str) else result[0]
|
|
145
|
+
|
|
146
|
+
def open_url(self, url: str) -> bool:
|
|
147
|
+
if not url.startswith(("http://", "https://")):
|
|
148
|
+
return False
|
|
149
|
+
webbrowser.open(url)
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def run_window(app, host: str, port: int, debug: bool) -> bool:
|
|
154
|
+
"""Open the native window. Returns False if no native webview is available."""
|
|
155
|
+
try:
|
|
156
|
+
import webview
|
|
157
|
+
except ImportError:
|
|
158
|
+
print("pywebview is not installed; falling back to the browser.", file=sys.stderr)
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
port = _preferred_port(host, port)
|
|
162
|
+
from . import db # the window background matches the saved theme (no flash)
|
|
163
|
+
background = "#f6f7f9" if db.get_setting("theme") == "light" else "#111418"
|
|
164
|
+
server = _start_server_thread(app, host, port)
|
|
165
|
+
print(f"pyhttp window open (API at http://{host}:{port}/api/docs)", flush=True)
|
|
166
|
+
api = NativeApi()
|
|
167
|
+
window = webview.create_window(
|
|
168
|
+
f"pyhttp {__version__}", f"http://{host}:{port}/", js_api=api,
|
|
169
|
+
width=1440, height=900, min_size=(960, 600), background_color=background,
|
|
170
|
+
text_select=True,
|
|
171
|
+
)
|
|
172
|
+
api._window = window
|
|
173
|
+
updater.quit_hook = window.destroy # the in-app updater closes the window when the new version is ready
|
|
174
|
+
try:
|
|
175
|
+
webview.start(debug=debug, private_mode=False)
|
|
176
|
+
except Exception as exc: # noqa: BLE001 - missing WebView2 / WebKitGTK etc.
|
|
177
|
+
print(f"Native window unavailable ({exc}); falling back to the browser.", file=sys.stderr)
|
|
178
|
+
server.should_exit = True
|
|
179
|
+
return False
|
|
180
|
+
server.should_exit = True
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def main() -> None:
|
|
185
|
+
if len(sys.argv) > 1 and sys.argv[1] == "run":
|
|
186
|
+
from . import cli
|
|
187
|
+
sys.exit(cli.main(sys.argv[2:], DEFAULT_DATA_DIR))
|
|
188
|
+
if len(sys.argv) > 1 and sys.argv[1] == "mcp":
|
|
189
|
+
from . import mcp
|
|
190
|
+
mcp.main(sys.argv[2:], DEFAULT_DATA_DIR)
|
|
191
|
+
return
|
|
192
|
+
parser = argparse.ArgumentParser(prog="pyhttp", description="Local Postman-like HTTP client",
|
|
193
|
+
epilog="Headless runs for CI: pyhttp run --help")
|
|
194
|
+
mode = parser.add_mutually_exclusive_group()
|
|
195
|
+
mode.add_argument("--browser", action="store_true", help="open the UI in your web browser")
|
|
196
|
+
mode.add_argument("--server", action="store_true", help="run the API server only, no UI")
|
|
197
|
+
parser.add_argument("--host", default="127.0.0.1", help="bind address (default: 127.0.0.1)")
|
|
198
|
+
parser.add_argument("--port", type=int, default=8765,
|
|
199
|
+
help="port to listen on (default: 8765; a free port is used when it is taken)")
|
|
200
|
+
parser.add_argument("--data-dir", type=Path, default=DEFAULT_DATA_DIR,
|
|
201
|
+
help=f"where the database and outputs live (default: {DEFAULT_DATA_DIR})")
|
|
202
|
+
parser.add_argument("--debug", action="store_true", help="enable devtools in the native window")
|
|
203
|
+
# parse_known_args: macOS may pass a "-psn_…" argument when launching an .app
|
|
204
|
+
args, _unknown = parser.parse_known_args()
|
|
205
|
+
|
|
206
|
+
_redirect_output_when_windowed(args.data_dir)
|
|
207
|
+
updater.cleanup(args.data_dir) # leftovers of a previous in-app update
|
|
208
|
+
app = create_app(args.data_dir)
|
|
209
|
+
|
|
210
|
+
if not args.browser and not args.server:
|
|
211
|
+
if run_window(app, args.host, args.port, args.debug):
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
url = f"http://{args.host}:{args.port}/"
|
|
215
|
+
print(f"pyhttp running at {url} (data: {args.data_dir.resolve()}) Ctrl+C to stop")
|
|
216
|
+
if not args.server:
|
|
217
|
+
threading.Timer(1.0, lambda: webbrowser.open(url)).start()
|
|
218
|
+
uvicorn.run(app, host=args.host, port=args.port, log_level="warning")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if __name__ == "__main__":
|
|
222
|
+
main()
|