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/openapi.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Import OpenAPI 3.x / Swagger 2.0 specs and HAR files as collections.
|
|
2
|
+
|
|
3
|
+
Both produce the same import shape as postman.from_collection():
|
|
4
|
+
{name, folders, requests, variables, environments}.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.parse import parse_qsl, urlsplit
|
|
13
|
+
|
|
14
|
+
METHODS = ("get", "put", "post", "delete", "options", "head", "patch", "trace")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_text(text: str) -> Any:
|
|
18
|
+
"""JSON, or YAML when PyYAML is installed (OpenAPI files are often YAML)."""
|
|
19
|
+
try:
|
|
20
|
+
return json.loads(text)
|
|
21
|
+
except ValueError:
|
|
22
|
+
pass
|
|
23
|
+
try:
|
|
24
|
+
import yaml # type: ignore[import-untyped]
|
|
25
|
+
except ImportError:
|
|
26
|
+
raise ValueError("not valid JSON (YAML needs the PyYAML package)") from None
|
|
27
|
+
try:
|
|
28
|
+
return yaml.safe_load(text)
|
|
29
|
+
except yaml.YAMLError as exc:
|
|
30
|
+
raise ValueError(f"not valid JSON or YAML: {exc}") from None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ------------------------------------------------------------------ OpenAPI
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_openapi(data: Any) -> bool:
|
|
37
|
+
return isinstance(data, dict) and isinstance(data.get("paths"), dict) and (
|
|
38
|
+
str(data.get("openapi", "")).startswith("3") or str(data.get("swagger", "")).startswith("2"))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _Spec:
|
|
42
|
+
def __init__(self, data: dict[str, Any]) -> None:
|
|
43
|
+
self.data = data
|
|
44
|
+
|
|
45
|
+
def deref(self, obj: Any, seen: frozenset[str] = frozenset()) -> Any:
|
|
46
|
+
"""Follow a local $ref (#/components/...); unknown or circular refs become {}."""
|
|
47
|
+
while isinstance(obj, dict) and isinstance(obj.get("$ref"), str):
|
|
48
|
+
ref = obj["$ref"]
|
|
49
|
+
if ref in seen or not ref.startswith("#/"):
|
|
50
|
+
return {}
|
|
51
|
+
seen = seen | {ref}
|
|
52
|
+
node: Any = self.data
|
|
53
|
+
for part in ref[2:].split("/"):
|
|
54
|
+
part = part.replace("~1", "/").replace("~0", "~")
|
|
55
|
+
node = node.get(part) if isinstance(node, dict) else None
|
|
56
|
+
obj = node if node is not None else {}
|
|
57
|
+
return obj
|
|
58
|
+
|
|
59
|
+
def example(self, schema: Any, depth: int = 0, seen: frozenset[str] = frozenset()) -> Any:
|
|
60
|
+
"""A sample value for a JSON schema: example, default, enum[0], or a typed placeholder."""
|
|
61
|
+
if isinstance(schema, dict) and isinstance(schema.get("$ref"), str):
|
|
62
|
+
ref = schema["$ref"]
|
|
63
|
+
if ref in seen:
|
|
64
|
+
return {}
|
|
65
|
+
return self.example(self.deref(schema), depth, seen | {ref})
|
|
66
|
+
if not isinstance(schema, dict) or depth > 8:
|
|
67
|
+
return None
|
|
68
|
+
for key in ("example", "default"):
|
|
69
|
+
if key in schema:
|
|
70
|
+
return schema[key]
|
|
71
|
+
if isinstance(schema.get("examples"), list) and schema["examples"]:
|
|
72
|
+
return schema["examples"][0]
|
|
73
|
+
if schema.get("enum"):
|
|
74
|
+
return schema["enum"][0]
|
|
75
|
+
for key in ("allOf",):
|
|
76
|
+
if isinstance(schema.get(key), list):
|
|
77
|
+
merged: dict[str, Any] = {}
|
|
78
|
+
for part in schema[key]:
|
|
79
|
+
v = self.example(part, depth + 1, seen)
|
|
80
|
+
if isinstance(v, dict):
|
|
81
|
+
merged.update(v)
|
|
82
|
+
return merged
|
|
83
|
+
for key in ("oneOf", "anyOf"):
|
|
84
|
+
if isinstance(schema.get(key), list) and schema[key]:
|
|
85
|
+
return self.example(schema[key][0], depth + 1, seen)
|
|
86
|
+
t = schema.get("type")
|
|
87
|
+
if isinstance(t, list):
|
|
88
|
+
t = next((x for x in t if x != "null"), None)
|
|
89
|
+
if t == "object" or (t is None and "properties" in schema):
|
|
90
|
+
props = schema.get("properties") or {}
|
|
91
|
+
return {k: self.example(v, depth + 1, seen) for k, v in props.items()}
|
|
92
|
+
if t == "array":
|
|
93
|
+
item = self.example(schema.get("items") or {}, depth + 1, seen)
|
|
94
|
+
return [item] if item is not None else []
|
|
95
|
+
if t == "integer":
|
|
96
|
+
return 0
|
|
97
|
+
if t == "number":
|
|
98
|
+
return 0.0
|
|
99
|
+
if t == "boolean":
|
|
100
|
+
return True
|
|
101
|
+
if t == "string":
|
|
102
|
+
fmt = schema.get("format", "")
|
|
103
|
+
return {"date-time": "2024-01-01T00:00:00Z", "date": "2024-01-01", "email": "user@example.com",
|
|
104
|
+
"uuid": "00000000-0000-0000-0000-000000000000", "uri": "https://example.com",
|
|
105
|
+
"binary": ""}.get(fmt, "string")
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _var_name(name: str) -> str:
|
|
110
|
+
"""Path variables are `:name` with word characters only."""
|
|
111
|
+
v = re.sub(r"\W", "_", name)
|
|
112
|
+
return v if re.match(r"[A-Za-z_]", v) else "_" + v
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _base_url(data: dict[str, Any]) -> str:
|
|
116
|
+
if data.get("swagger"):
|
|
117
|
+
host = data.get("host") or "localhost"
|
|
118
|
+
scheme = (data.get("schemes") or ["https"])[0]
|
|
119
|
+
return f"{scheme}://{host}{data.get('basePath') or ''}".rstrip("/")
|
|
120
|
+
servers = data.get("servers") or []
|
|
121
|
+
if not servers or not isinstance(servers[0], dict):
|
|
122
|
+
return "http://localhost"
|
|
123
|
+
url = str(servers[0].get("url") or "")
|
|
124
|
+
for name, var in (servers[0].get("variables") or {}).items():
|
|
125
|
+
url = url.replace("{" + name + "}", str((var or {}).get("default", "")))
|
|
126
|
+
return url.rstrip("/") or "http://localhost"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _security_auth(spec: _Spec, requirements: Any) -> dict[str, Any] | None:
|
|
130
|
+
"""Map the first usable security requirement to a pyhttp auth (values left as variables)."""
|
|
131
|
+
data = spec.data
|
|
132
|
+
schemes = (data.get("components") or {}).get("securitySchemes") or data.get("securityDefinitions") or {}
|
|
133
|
+
if requirements == []:
|
|
134
|
+
return {"type": "none"} # security: [] turns authentication off
|
|
135
|
+
for req in requirements or []:
|
|
136
|
+
if not isinstance(req, dict):
|
|
137
|
+
continue
|
|
138
|
+
if not req:
|
|
139
|
+
return {"type": "none"} # {} in the list: security is optional
|
|
140
|
+
for name in req:
|
|
141
|
+
s = spec.deref(schemes.get(name) or {})
|
|
142
|
+
t, scheme = s.get("type"), str(s.get("scheme", "")).lower()
|
|
143
|
+
if (t == "http" and scheme == "basic") or t == "basic":
|
|
144
|
+
return {"type": "basic", "username": "{{username}}", "password": "{{password}}"}
|
|
145
|
+
if t == "http" and scheme == "bearer":
|
|
146
|
+
return {"type": "bearer", "token": "{{token}}", "prefix": "Bearer"}
|
|
147
|
+
if t == "apiKey" and s.get("in") in ("header", "query"):
|
|
148
|
+
return {"type": "apikey", "key": s.get("name") or "X-API-Key", "value": "{{apiKey}}",
|
|
149
|
+
"add_to": "query" if s.get("in") == "query" else "header"}
|
|
150
|
+
if t == "oauth2":
|
|
151
|
+
flows = s.get("flows") or {}
|
|
152
|
+
if "clientCredentials" in flows or s.get("flow") == "application":
|
|
153
|
+
f = flows.get("clientCredentials") or s
|
|
154
|
+
return {"type": "oauth2", "grant": "client_credentials", "token_url": f.get("tokenUrl", ""),
|
|
155
|
+
"client_id": "{{clientId}}", "client_secret": "{{clientSecret}}",
|
|
156
|
+
"scope": " ".join((f.get("scopes") or {}).keys())}
|
|
157
|
+
if "password" in flows or s.get("flow") == "password":
|
|
158
|
+
f = flows.get("password") or s
|
|
159
|
+
return {"type": "oauth2", "grant": "password", "token_url": f.get("tokenUrl", ""),
|
|
160
|
+
"client_id": "{{clientId}}", "client_secret": "{{clientSecret}}",
|
|
161
|
+
"username": "{{username}}", "password": "{{password}}",
|
|
162
|
+
"scope": " ".join((f.get("scopes") or {}).keys())}
|
|
163
|
+
return {"type": "bearer", "token": "{{token}}", "prefix": "Bearer"}
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _param_value(spec: _Spec, p: dict[str, Any]) -> str:
|
|
168
|
+
if "example" in p:
|
|
169
|
+
v = p["example"]
|
|
170
|
+
else:
|
|
171
|
+
schema = p.get("schema") or {k: p[k] for k in ("type", "default", "enum", "format", "items") if k in p}
|
|
172
|
+
v = spec.example(schema)
|
|
173
|
+
if v is None or isinstance(v, (dict, list)):
|
|
174
|
+
return "" if v is None else json.dumps(v)
|
|
175
|
+
return str(v).lower() if isinstance(v, bool) else str(v)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _body_from_content(spec: _Spec, content: dict[str, Any]) -> tuple[str, str, str]:
|
|
179
|
+
"""(body_type, body text, content type) for an OpenAPI 3 requestBody content map."""
|
|
180
|
+
order = sorted(content, key=lambda ct: (0 if "json" in ct else 1 if "x-www-form-urlencoded" in ct
|
|
181
|
+
else 2 if "multipart" in ct else 3 if "xml" in ct else 4))
|
|
182
|
+
for ct in order:
|
|
183
|
+
media = content[ct] or {}
|
|
184
|
+
if "example" in media:
|
|
185
|
+
sample = media["example"]
|
|
186
|
+
elif isinstance(media.get("examples"), dict) and media["examples"]:
|
|
187
|
+
sample = spec.deref(next(iter(media["examples"].values()))).get("value")
|
|
188
|
+
else:
|
|
189
|
+
sample = spec.example(media.get("schema") or {})
|
|
190
|
+
if "json" in ct:
|
|
191
|
+
return "json", json.dumps(sample if sample is not None else {}, indent=2, ensure_ascii=False), ct
|
|
192
|
+
if "x-www-form-urlencoded" in ct or "multipart" in ct:
|
|
193
|
+
rows = sample if isinstance(sample, dict) else {}
|
|
194
|
+
lines = []
|
|
195
|
+
props = (spec.deref(media.get("schema") or {}).get("properties") or {})
|
|
196
|
+
for k, v in rows.items():
|
|
197
|
+
is_file = (spec.deref(props.get(k) or {}).get("format") == "binary")
|
|
198
|
+
lines.append(f"// {k}: @path/to/file" if is_file else f"{k}: {'' if v is None else (json.dumps(v) if isinstance(v, (dict, list)) else v)}")
|
|
199
|
+
return ("form" if "urlencoded" in ct else "multipart"), "\n".join(lines), ct
|
|
200
|
+
if "xml" in ct:
|
|
201
|
+
return "xml", sample if isinstance(sample, str) else "", ct
|
|
202
|
+
if ct.startswith("text/"):
|
|
203
|
+
return "text", sample if isinstance(sample, str) else "", ct
|
|
204
|
+
return "none", "", ""
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def from_openapi(data: dict[str, Any]) -> dict[str, Any]:
|
|
208
|
+
spec = _Spec(data)
|
|
209
|
+
info = data.get("info") or {}
|
|
210
|
+
root_name = str(info.get("title") or "Imported API")
|
|
211
|
+
root_id = uuid.uuid4().hex
|
|
212
|
+
root_auth = _security_auth(spec, data.get("security")) or {"type": "none"}
|
|
213
|
+
folders: list[dict[str, Any]] = [{
|
|
214
|
+
"id": root_id, "name": root_name, "parent_id": None,
|
|
215
|
+
"description": "\n\n".join(x for x in (str(info.get("description") or ""),
|
|
216
|
+
f"Version {info['version']}" if info.get("version") else "") if x),
|
|
217
|
+
"auth": root_auth, "headers": [],
|
|
218
|
+
"variables": [{"key": "baseUrl", "value": _base_url(data), "enabled": True}],
|
|
219
|
+
}]
|
|
220
|
+
tag_folders: dict[str, str] = {}
|
|
221
|
+
requests: list[dict[str, Any]] = []
|
|
222
|
+
|
|
223
|
+
def folder_for(tag: str | None) -> str:
|
|
224
|
+
if not tag:
|
|
225
|
+
return root_id
|
|
226
|
+
if tag not in tag_folders:
|
|
227
|
+
tag_folders[tag] = uuid.uuid4().hex
|
|
228
|
+
desc = next((str(t.get("description") or "") for t in data.get("tags") or [] if isinstance(t, dict) and t.get("name") == tag), "")
|
|
229
|
+
folders.append({"id": tag_folders[tag], "name": tag, "parent_id": root_id, "description": desc,
|
|
230
|
+
"auth": {"type": "inherit"}, "headers": [], "variables": []})
|
|
231
|
+
return tag_folders[tag]
|
|
232
|
+
|
|
233
|
+
for path, item in (data.get("paths") or {}).items():
|
|
234
|
+
item = spec.deref(item)
|
|
235
|
+
if not isinstance(item, dict):
|
|
236
|
+
continue
|
|
237
|
+
shared = [spec.deref(p) for p in item.get("parameters") or []]
|
|
238
|
+
for method in METHODS:
|
|
239
|
+
op = item.get(method)
|
|
240
|
+
if not isinstance(op, dict):
|
|
241
|
+
continue
|
|
242
|
+
params_by_key = {(p.get("in"), p.get("name")): p for p in shared if isinstance(p, dict)}
|
|
243
|
+
for p in op.get("parameters") or []:
|
|
244
|
+
p = spec.deref(p)
|
|
245
|
+
if isinstance(p, dict):
|
|
246
|
+
params_by_key[(p.get("in"), p.get("name"))] = p
|
|
247
|
+
url_path = re.sub(r"\{([^}/]+)\}", lambda m: ":" + _var_name(m.group(1)), path)
|
|
248
|
+
query, headers, path_rows, form_rows = [], [], [], []
|
|
249
|
+
body_type, body, body_ct = "none", "", ""
|
|
250
|
+
for (where, name), p in params_by_key.items():
|
|
251
|
+
if not name:
|
|
252
|
+
continue
|
|
253
|
+
row = {"key": name, "value": _param_value(spec, p), "enabled": bool(p.get("required")) or where == "path"}
|
|
254
|
+
if where == "query":
|
|
255
|
+
query.append(row)
|
|
256
|
+
elif where == "header" and name.lower() not in ("authorization", "content-type", "accept"):
|
|
257
|
+
headers.append(row)
|
|
258
|
+
elif where == "path":
|
|
259
|
+
path_rows.append({**row, "key": _var_name(name)})
|
|
260
|
+
elif where == "formData": # Swagger 2
|
|
261
|
+
form_rows.append(f"// {name}: @path/to/file" if p.get("type") == "file" else f"{name}: {row['value']}")
|
|
262
|
+
elif where == "body": # Swagger 2
|
|
263
|
+
sample = spec.example(p.get("schema") or {})
|
|
264
|
+
body_type, body = "json", json.dumps(sample if sample is not None else {}, indent=2, ensure_ascii=False)
|
|
265
|
+
if form_rows and body_type == "none":
|
|
266
|
+
consumes = op.get("consumes") or data.get("consumes") or []
|
|
267
|
+
body_type = "multipart" if any("multipart" in c for c in consumes) else "form"
|
|
268
|
+
body = "\n".join(form_rows)
|
|
269
|
+
rb = spec.deref(op.get("requestBody") or {})
|
|
270
|
+
if isinstance(rb.get("content"), dict):
|
|
271
|
+
body_type, body, body_ct = _body_from_content(spec, rb["content"])
|
|
272
|
+
if body_ct and body_type in ("json", "xml", "text") and body_ct not in ("application/json", "application/xml", "text/plain"):
|
|
273
|
+
headers.append({"key": "Content-Type", "value": body_ct, "enabled": True})
|
|
274
|
+
auth = {"type": "inherit"}
|
|
275
|
+
if "security" in op:
|
|
276
|
+
auth = _security_auth(spec, op["security"]) or {"type": "inherit"}
|
|
277
|
+
desc_parts = [str(op.get("description") or "")]
|
|
278
|
+
if op.get("operationId"):
|
|
279
|
+
desc_parts.append(f"operationId: `{op['operationId']}`")
|
|
280
|
+
if op.get("deprecated"):
|
|
281
|
+
desc_parts.insert(0, "**Deprecated.**")
|
|
282
|
+
tags = op.get("tags") or []
|
|
283
|
+
requests.append({
|
|
284
|
+
"folder_id": folder_for(tags[0] if tags else None),
|
|
285
|
+
"name": str(op.get("summary") or op.get("operationId") or f"{method.upper()} {path}")[:200],
|
|
286
|
+
"description": "\n\n".join(x for x in desc_parts if x), "tags": [str(t) for t in tags[1:]],
|
|
287
|
+
"method": method.upper(), "url": "{{baseUrl}}" + url_path,
|
|
288
|
+
"params": path_rows + query, "headers": headers, "body_type": body_type, "body": body,
|
|
289
|
+
"bodies": [{"id": "default", "name": "Default", "body": body, "enabled": True}] if body else [],
|
|
290
|
+
"auth": auth, "tests": [], "timeout": 30,
|
|
291
|
+
})
|
|
292
|
+
return {"folders": folders, "requests": requests, "variables": {}, "environments": [], "name": root_name}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ---------------------------------------------------------------------- HAR
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def is_har(data: Any) -> bool:
|
|
299
|
+
return isinstance(data, dict) and isinstance(data.get("log"), dict) and isinstance(data["log"].get("entries"), list)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
_SKIP_HEADERS = {"host", "content-length", "connection", "accept-encoding", "cookie"}
|
|
303
|
+
_SKIP_TYPES = (".js", ".css", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".map", ".webp")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def from_har(data: dict[str, Any], include_assets: bool = False) -> dict[str, Any]:
|
|
307
|
+
"""Every request in a browser HAR export, one folder per host. Static assets (scripts,
|
|
308
|
+
styles, images, fonts) are skipped unless include_assets is set."""
|
|
309
|
+
entries = data["log"]["entries"]
|
|
310
|
+
root_id = uuid.uuid4().hex
|
|
311
|
+
root_name = "HAR import"
|
|
312
|
+
pages = data["log"].get("pages") or []
|
|
313
|
+
if pages and isinstance(pages[0], dict) and pages[0].get("title"):
|
|
314
|
+
root_name = f"HAR: {str(pages[0]['title'])[:80]}"
|
|
315
|
+
folders = [{"id": root_id, "name": root_name, "parent_id": None, "description": "Imported from a HAR file.",
|
|
316
|
+
"auth": {"type": "none"}, "headers": [], "variables": []}]
|
|
317
|
+
host_folders: dict[str, str] = {}
|
|
318
|
+
requests: list[dict[str, Any]] = []
|
|
319
|
+
for e in entries:
|
|
320
|
+
r = (e or {}).get("request") or {}
|
|
321
|
+
url = str(r.get("url") or "")
|
|
322
|
+
if not url or url.startswith(("data:", "blob:", "chrome-extension:")):
|
|
323
|
+
continue
|
|
324
|
+
parts = urlsplit(url)
|
|
325
|
+
if not include_assets and parts.path.lower().endswith(_SKIP_TYPES):
|
|
326
|
+
continue
|
|
327
|
+
host = parts.netloc or "local"
|
|
328
|
+
if host not in host_folders:
|
|
329
|
+
host_folders[host] = uuid.uuid4().hex
|
|
330
|
+
folders.append({"id": host_folders[host], "name": host, "parent_id": root_id, "description": "",
|
|
331
|
+
"auth": {"type": "inherit"}, "headers": [], "variables": []})
|
|
332
|
+
base = parts._replace(query="", fragment="").geturl()
|
|
333
|
+
params = [{"key": k, "value": v, "enabled": True} for k, v in parse_qsl(parts.query, keep_blank_values=True)]
|
|
334
|
+
headers = [{"key": h.get("name", ""), "value": h.get("value", ""), "enabled": True}
|
|
335
|
+
for h in r.get("headers") or [] if isinstance(h, dict)
|
|
336
|
+
and h.get("name") and not h["name"].startswith(":") and h["name"].lower() not in _SKIP_HEADERS]
|
|
337
|
+
body_type, body = "none", ""
|
|
338
|
+
post = r.get("postData") or {}
|
|
339
|
+
mime = str(post.get("mimeType") or "").lower()
|
|
340
|
+
if post.get("params") and ("x-www-form-urlencoded" in mime or "multipart" in mime):
|
|
341
|
+
body_type = "multipart" if "multipart" in mime else "form"
|
|
342
|
+
body = "\n".join(f"{p.get('name', '')}: " + (f"@{p['fileName']}" if p.get("fileName") else str(p.get("value", "")))
|
|
343
|
+
for p in post["params"] if isinstance(p, dict))
|
|
344
|
+
headers = [h for h in headers if h["key"].lower() != "content-type"]
|
|
345
|
+
elif post.get("text") is not None:
|
|
346
|
+
body = str(post["text"])
|
|
347
|
+
body_type = "json" if "json" in mime else "xml" if "xml" in mime else "form" if "urlencoded" in mime else "text"
|
|
348
|
+
if body_type == "json":
|
|
349
|
+
try:
|
|
350
|
+
body = json.dumps(json.loads(body), indent=2, ensure_ascii=False)
|
|
351
|
+
except ValueError:
|
|
352
|
+
pass
|
|
353
|
+
elif body_type == "form":
|
|
354
|
+
body = "\n".join(f"{k}: {v}" for k, v in parse_qsl(body, keep_blank_values=True))
|
|
355
|
+
method = str(r.get("method") or "GET").upper()
|
|
356
|
+
status = ((e or {}).get("response") or {}).get("status")
|
|
357
|
+
requests.append({
|
|
358
|
+
"folder_id": host_folders[host], "name": f"{method} {parts.path or '/'}"[:200],
|
|
359
|
+
"description": f"Recorded response: HTTP {status}" if status else "", "tags": ["har"],
|
|
360
|
+
"method": method, "url": base, "params": params, "headers": headers,
|
|
361
|
+
"body_type": body_type, "body": body,
|
|
362
|
+
"bodies": [{"id": "default", "name": "Default", "body": body, "enabled": True}] if body else [],
|
|
363
|
+
"auth": {"type": "inherit"}, "tests": [], "timeout": 30,
|
|
364
|
+
})
|
|
365
|
+
return {"folders": folders, "requests": requests, "variables": {}, "environments": [], "name": root_name}
|