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/postman.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""Postman Collection v2.1 and Postman environment conversion.
|
|
2
|
+
|
|
3
|
+
`from_collection()` turns a Postman collection into the same shape as pyhttp's
|
|
4
|
+
own export ({folders, requests, variables}); `to_collection()` does the
|
|
5
|
+
reverse so a pyhttp collection can be opened in Postman. Scripts (pre-request
|
|
6
|
+
and test JavaScript) cannot run here; they are kept in the request description
|
|
7
|
+
so nothing is silently lost.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import uuid
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
SCHEMA = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_collection(data: Any) -> bool:
|
|
19
|
+
return isinstance(data, dict) and isinstance(data.get("info"), dict) and "item" in data
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_environment(data: Any) -> bool:
|
|
23
|
+
return isinstance(data, dict) and isinstance(data.get("values"), list) and "name" in data and "item" not in data
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ------------------------------------------------------------------ import
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _kv(items: Any, key_name: str = "key") -> list[dict[str, Any]]:
|
|
30
|
+
out = []
|
|
31
|
+
for it in items or []:
|
|
32
|
+
if not isinstance(it, dict):
|
|
33
|
+
continue
|
|
34
|
+
out.append({"key": str(it.get(key_name) or ""), "value": "" if it.get("value") is None else str(it.get("value")),
|
|
35
|
+
"enabled": not it.get("disabled", False)})
|
|
36
|
+
return out
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _url(u: Any) -> tuple[str, list[dict[str, Any]]]:
|
|
40
|
+
if isinstance(u, str):
|
|
41
|
+
return u, []
|
|
42
|
+
if not isinstance(u, dict):
|
|
43
|
+
return "", []
|
|
44
|
+
raw = u.get("raw") or ""
|
|
45
|
+
if not raw:
|
|
46
|
+
host = u.get("host")
|
|
47
|
+
host = ".".join(host) if isinstance(host, list) else (host or "")
|
|
48
|
+
path = u.get("path")
|
|
49
|
+
path = "/".join(path) if isinstance(path, list) else (path or "")
|
|
50
|
+
proto = u.get("protocol")
|
|
51
|
+
raw = (f"{proto}://" if proto else "") + host + (f":{u['port']}" if u.get("port") else "") + ("/" + path.lstrip("/") if path else "")
|
|
52
|
+
params = _kv(u.get("query"))
|
|
53
|
+
if params and "?" in raw:
|
|
54
|
+
raw = raw.split("?", 1)[0]
|
|
55
|
+
return raw, params
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _auth(a: Any) -> dict[str, Any]:
|
|
59
|
+
if not isinstance(a, dict) or not a.get("type"):
|
|
60
|
+
return {"type": "inherit"}
|
|
61
|
+
t = a["type"]
|
|
62
|
+
|
|
63
|
+
def vals(name: str) -> dict[str, str]:
|
|
64
|
+
rows = a.get(name)
|
|
65
|
+
if isinstance(rows, list):
|
|
66
|
+
return {str(r.get("key")): str(r.get("value") if r.get("value") is not None else "") for r in rows if isinstance(r, dict)}
|
|
67
|
+
if isinstance(rows, dict):
|
|
68
|
+
return {k: str(v) for k, v in rows.items()}
|
|
69
|
+
return {}
|
|
70
|
+
|
|
71
|
+
if t == "noauth":
|
|
72
|
+
return {"type": "none"}
|
|
73
|
+
if t == "basic":
|
|
74
|
+
v = vals("basic")
|
|
75
|
+
return {"type": "basic", "username": v.get("username", ""), "password": v.get("password", "")}
|
|
76
|
+
if t == "bearer":
|
|
77
|
+
v = vals("bearer")
|
|
78
|
+
return {"type": "bearer", "token": v.get("token", ""), "prefix": "Bearer"}
|
|
79
|
+
if t == "apikey":
|
|
80
|
+
v = vals("apikey")
|
|
81
|
+
return {"type": "apikey", "key": v.get("key", ""), "value": v.get("value", ""),
|
|
82
|
+
"add_to": "query" if v.get("in") == "query" else "header"}
|
|
83
|
+
if t == "digest":
|
|
84
|
+
v = vals("digest")
|
|
85
|
+
return {"type": "digest", "username": v.get("username", ""), "password": v.get("password", "")}
|
|
86
|
+
if t == "oauth2":
|
|
87
|
+
v = vals("oauth2")
|
|
88
|
+
grant = v.get("grant_type", "client_credentials")
|
|
89
|
+
if grant not in ("client_credentials", "password_credentials"):
|
|
90
|
+
# authorization code / implicit need a browser callback: keep the token if there is one
|
|
91
|
+
return {"type": "bearer", "token": v.get("accessToken", ""), "prefix": v.get("headerPrefix") or "Bearer"}
|
|
92
|
+
return {"type": "oauth2", "grant": "password" if grant == "password_credentials" else "client_credentials",
|
|
93
|
+
"token_url": v.get("accessTokenUrl", ""), "client_id": v.get("clientId", ""),
|
|
94
|
+
"client_secret": v.get("clientSecret", ""), "scope": v.get("scope", ""),
|
|
95
|
+
"username": v.get("username", ""), "password": v.get("password", ""),
|
|
96
|
+
"audience": v.get("audience", ""), "prefix": v.get("headerPrefix") or "Bearer",
|
|
97
|
+
"client_auth": "body" if v.get("client_authentication") == "body" else "basic"}
|
|
98
|
+
return {"type": "inherit"}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _body(b: Any) -> tuple[str, str]:
|
|
102
|
+
if not isinstance(b, dict) or not b.get("mode"):
|
|
103
|
+
return "none", ""
|
|
104
|
+
mode = b["mode"]
|
|
105
|
+
if mode == "raw":
|
|
106
|
+
lang = ((b.get("options") or {}).get("raw") or {}).get("language", "")
|
|
107
|
+
raw = b.get("raw") or ""
|
|
108
|
+
if lang in ("json", "xml", "html", "javascript", "text"):
|
|
109
|
+
return lang, raw
|
|
110
|
+
return ("json" if raw.lstrip().startswith(("{", "[")) else "text"), raw
|
|
111
|
+
if mode == "urlencoded":
|
|
112
|
+
rows = b.get("urlencoded") or []
|
|
113
|
+
return "form", "\n".join(("// " if r.get("disabled") else "") + f"{r.get('key', '')}: {r.get('value', '')}" for r in rows if isinstance(r, dict))
|
|
114
|
+
if mode == "formdata":
|
|
115
|
+
lines = []
|
|
116
|
+
for r in b.get("formdata") or []:
|
|
117
|
+
if not isinstance(r, dict):
|
|
118
|
+
continue
|
|
119
|
+
prefix = "// " if r.get("disabled") else ""
|
|
120
|
+
if r.get("type") == "file":
|
|
121
|
+
src = r.get("src")
|
|
122
|
+
src = src[0] if isinstance(src, list) and src else (src or "")
|
|
123
|
+
lines.append(f"{prefix}{r.get('key', '')}: @{src}")
|
|
124
|
+
else:
|
|
125
|
+
lines.append(f"{prefix}{r.get('key', '')}: {r.get('value', '')}")
|
|
126
|
+
return "multipart", "\n".join(lines)
|
|
127
|
+
if mode == "file":
|
|
128
|
+
src = (b.get("file") or {}).get("src") or ""
|
|
129
|
+
return "binary", src
|
|
130
|
+
if mode == "graphql":
|
|
131
|
+
g = b.get("graphql") or {}
|
|
132
|
+
variables = g.get("variables") or "{}"
|
|
133
|
+
try:
|
|
134
|
+
variables = json.loads(variables) if isinstance(variables, str) else variables
|
|
135
|
+
except ValueError:
|
|
136
|
+
pass
|
|
137
|
+
return "graphql", json.dumps({"query": g.get("query") or "", "variables": variables}, indent=2)
|
|
138
|
+
return "none", ""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _scripts(events: Any) -> dict[str, str]:
|
|
142
|
+
"""Pre-request and test scripts of an item; they run unchanged in pyhttp's JavaScript engine."""
|
|
143
|
+
out = {"pre": "", "test": "", "lang": "js"}
|
|
144
|
+
for ev in events or []:
|
|
145
|
+
if not isinstance(ev, dict):
|
|
146
|
+
continue
|
|
147
|
+
exec_ = (ev.get("script") or {}).get("exec")
|
|
148
|
+
code = "\n".join(exec_) if isinstance(exec_, list) else (exec_ or "")
|
|
149
|
+
if not code.strip():
|
|
150
|
+
continue
|
|
151
|
+
key = "pre" if ev.get("listen") == "prerequest" else "test"
|
|
152
|
+
out[key] = (out[key] + "\n\n" + code).strip() if out[key] else code.strip()
|
|
153
|
+
return out
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _scripts_for(r: dict[str, Any], body: dict[str, Any] | None) -> dict[str, Any]:
|
|
157
|
+
"""The request's scripts for one exported body: those for all bodies followed by the body's own."""
|
|
158
|
+
sc = dict((r.get("options") or {}).get("scripts") or {})
|
|
159
|
+
own = (sc.get("bodies") or {}).get(body.get("id") or "") if body else None
|
|
160
|
+
if isinstance(own, dict):
|
|
161
|
+
for key in ("pre", "test"):
|
|
162
|
+
parts = [c for c in ((sc.get(key) or "").strip(), (own.get(key) or "").strip()) if c]
|
|
163
|
+
sc[key] = "\n\n".join(parts)
|
|
164
|
+
return sc
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _events(scripts: dict[str, Any] | None, extra_tests: list[str]) -> list[dict[str, Any]]:
|
|
168
|
+
"""Postman events for export: pyhttp's scripts (JavaScript as-is, Python as comments) plus translated tests."""
|
|
169
|
+
scripts = scripts or {}
|
|
170
|
+
python = scripts.get("lang") == "py"
|
|
171
|
+
out = []
|
|
172
|
+
for listen, key in (("prerequest", "pre"), ("test", "test")):
|
|
173
|
+
code = (scripts.get(key) or "").strip()
|
|
174
|
+
lines: list[str] = []
|
|
175
|
+
if code:
|
|
176
|
+
if python:
|
|
177
|
+
lines = ["// pyhttp Python script (does not run in Postman):"] + ["// " + ln for ln in code.splitlines()]
|
|
178
|
+
else:
|
|
179
|
+
lines = code.splitlines()
|
|
180
|
+
if key == "test" and extra_tests:
|
|
181
|
+
lines += ([""] if lines else []) + extra_tests
|
|
182
|
+
if lines:
|
|
183
|
+
out.append({"listen": listen, "script": {"type": "text/javascript", "exec": lines}})
|
|
184
|
+
return out
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def from_collection(data: dict[str, Any]) -> dict[str, Any]:
|
|
188
|
+
"""Convert a Postman v2.1 collection into pyhttp's import shape."""
|
|
189
|
+
folders: list[dict[str, Any]] = []
|
|
190
|
+
requests: list[dict[str, Any]] = []
|
|
191
|
+
root_name = (data.get("info") or {}).get("name") or "Imported collection"
|
|
192
|
+
root_id = uuid.uuid4().hex
|
|
193
|
+
folders.append({"id": root_id, "name": root_name, "parent_id": None, "description": _desc(data.get("info", {}).get("description")),
|
|
194
|
+
"auth": _auth(data.get("auth")), "headers": [], "variables": _kv(data.get("variable"))})
|
|
195
|
+
|
|
196
|
+
def walk(items: Any, parent_id: str) -> None:
|
|
197
|
+
for it in items or []:
|
|
198
|
+
if not isinstance(it, dict):
|
|
199
|
+
continue
|
|
200
|
+
if "item" in it: # folder
|
|
201
|
+
fid = uuid.uuid4().hex
|
|
202
|
+
folders.append({"id": fid, "name": it.get("name") or "Folder", "parent_id": parent_id,
|
|
203
|
+
"description": _desc(it.get("description")), "auth": _auth(it.get("auth")),
|
|
204
|
+
"headers": [], "variables": _kv(it.get("variable"))})
|
|
205
|
+
walk(it.get("item"), fid)
|
|
206
|
+
continue
|
|
207
|
+
req = it.get("request")
|
|
208
|
+
if isinstance(req, str):
|
|
209
|
+
req = {"url": req, "method": "GET"}
|
|
210
|
+
if not isinstance(req, dict):
|
|
211
|
+
continue
|
|
212
|
+
url, params = _url(req.get("url"))
|
|
213
|
+
body_type, body = _body(req.get("body"))
|
|
214
|
+
desc = _desc(req.get("description") or it.get("description"))
|
|
215
|
+
requests.append({
|
|
216
|
+
"folder_id": parent_id, "name": it.get("name") or url or "Request",
|
|
217
|
+
"description": desc, "tags": [], "method": (req.get("method") or "GET").upper(), "url": url,
|
|
218
|
+
"params": params, "headers": _kv(req.get("header")), "body_type": body_type, "body": body,
|
|
219
|
+
"bodies": [{"id": "default", "name": "Default", "body": body, "enabled": True}] if body else [],
|
|
220
|
+
"auth": _auth(req.get("auth")), "tests": [], "timeout": 30,
|
|
221
|
+
"options": {"scripts": _scripts(it.get("event"))},
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
walk(data.get("item"), root_id)
|
|
225
|
+
return {"folders": folders, "requests": requests, "variables": {}, "environments": [], "name": root_name}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _desc(d: Any) -> str:
|
|
229
|
+
if isinstance(d, dict):
|
|
230
|
+
return str(d.get("content") or "")
|
|
231
|
+
return str(d or "")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def from_environment(data: dict[str, Any]) -> dict[str, Any]:
|
|
235
|
+
return {"name": data.get("name") or "Imported environment",
|
|
236
|
+
"variables": [{"key": str(v.get("key") or ""), "value": "" if v.get("value") is None else str(v.get("value")),
|
|
237
|
+
"enabled": v.get("enabled", True) is not False, **({"secret": True} if v.get("type") == "secret" else {})}
|
|
238
|
+
for v in data.get("values") or [] if isinstance(v, dict)]}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ------------------------------------------------------------------ export
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _pm_auth(a: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
245
|
+
a = a or {}
|
|
246
|
+
t = a.get("type", "inherit")
|
|
247
|
+
if t == "inherit":
|
|
248
|
+
return None
|
|
249
|
+
if t == "none":
|
|
250
|
+
return {"type": "noauth"}
|
|
251
|
+
if t == "basic":
|
|
252
|
+
return {"type": "basic", "basic": [{"key": "username", "value": a.get("username", ""), "type": "string"},
|
|
253
|
+
{"key": "password", "value": a.get("password", ""), "type": "string"}]}
|
|
254
|
+
if t == "bearer":
|
|
255
|
+
return {"type": "bearer", "bearer": [{"key": "token", "value": a.get("token", ""), "type": "string"}]}
|
|
256
|
+
if t == "apikey":
|
|
257
|
+
return {"type": "apikey", "apikey": [{"key": "key", "value": a.get("key", ""), "type": "string"},
|
|
258
|
+
{"key": "value", "value": a.get("value", ""), "type": "string"},
|
|
259
|
+
{"key": "in", "value": a.get("add_to", "header"), "type": "string"}]}
|
|
260
|
+
if t == "digest":
|
|
261
|
+
return {"type": "digest", "digest": [{"key": "username", "value": a.get("username", ""), "type": "string"},
|
|
262
|
+
{"key": "password", "value": a.get("password", ""), "type": "string"}]}
|
|
263
|
+
if t == "oauth2":
|
|
264
|
+
fields = {
|
|
265
|
+
"grant_type": "password_credentials" if a.get("grant") == "password" else "client_credentials",
|
|
266
|
+
"accessTokenUrl": a.get("token_url", ""), "clientId": a.get("client_id", ""),
|
|
267
|
+
"clientSecret": a.get("client_secret", ""), "scope": a.get("scope", ""),
|
|
268
|
+
"client_authentication": "body" if a.get("client_auth") == "body" else "header",
|
|
269
|
+
"headerPrefix": a.get("prefix") or "Bearer", "addTokenTo": "header",
|
|
270
|
+
}
|
|
271
|
+
if a.get("grant") == "password":
|
|
272
|
+
fields.update(username=a.get("username", ""), password=a.get("password", ""))
|
|
273
|
+
if a.get("audience"):
|
|
274
|
+
fields["audience"] = a["audience"]
|
|
275
|
+
return {"type": "oauth2", "oauth2": [{"key": k, "value": v, "type": "string"} for k, v in fields.items()]}
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _pm_kv(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
280
|
+
return [{"key": r.get("key", ""), "value": r.get("value", ""), **({"disabled": True} if not r.get("enabled", True) else {})}
|
|
281
|
+
for r in rows or []]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _form_rows(text: str) -> list[dict[str, Any]]:
|
|
285
|
+
from .runner import parse_form_lines
|
|
286
|
+
|
|
287
|
+
out = []
|
|
288
|
+
for r in parse_form_lines(text):
|
|
289
|
+
row: dict[str, Any] = {"key": r["key"], "type": "file" if r["file"] else "text"}
|
|
290
|
+
if r["file"]:
|
|
291
|
+
row["src"] = r["value"]
|
|
292
|
+
else:
|
|
293
|
+
row["value"] = r["value"]
|
|
294
|
+
if not r["enabled"]:
|
|
295
|
+
row["disabled"] = True
|
|
296
|
+
out.append(row)
|
|
297
|
+
return out
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _pm_body(r: dict[str, Any], body: str) -> dict[str, Any] | None:
|
|
301
|
+
t = r.get("body_type", "none")
|
|
302
|
+
if t == "none":
|
|
303
|
+
return None
|
|
304
|
+
if t in ("json", "text", "xml", "html", "javascript"):
|
|
305
|
+
return {"mode": "raw", "raw": body, "options": {"raw": {"language": t}}}
|
|
306
|
+
if t == "form":
|
|
307
|
+
return {"mode": "urlencoded", "urlencoded": [{k: v for k, v in row.items() if k != "type"} for row in _form_rows(body)]}
|
|
308
|
+
if t == "multipart":
|
|
309
|
+
return {"mode": "formdata", "formdata": _form_rows(body)}
|
|
310
|
+
if t == "binary":
|
|
311
|
+
return {"mode": "file", "file": {"src": body.strip()}}
|
|
312
|
+
if t == "graphql":
|
|
313
|
+
try:
|
|
314
|
+
d = json.loads(body)
|
|
315
|
+
v = d.get("variables")
|
|
316
|
+
return {"mode": "graphql", "graphql": {"query": d.get("query", ""), "variables": v if isinstance(v, str) else json.dumps(v or {})}}
|
|
317
|
+
except (ValueError, AttributeError):
|
|
318
|
+
return {"mode": "graphql", "graphql": {"query": body, "variables": "{}"}}
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _pm_tests(tests: list[dict[str, Any]]) -> list[str]:
|
|
323
|
+
from .assertions import describe
|
|
324
|
+
|
|
325
|
+
lines = []
|
|
326
|
+
for t in tests or []:
|
|
327
|
+
if not t.get("enabled", True):
|
|
328
|
+
continue
|
|
329
|
+
name = (t.get("name") or describe(t)).replace('"', '\\"')
|
|
330
|
+
kind, op, val, target = t.get("type"), t.get("op"), t.get("value", ""), t.get("target", "")
|
|
331
|
+
if kind == "status" and op == "equals":
|
|
332
|
+
lines.append(f'pm.test("{name}", function () {{ pm.response.to.have.status({val}); }});')
|
|
333
|
+
elif kind == "time" and op == "less_than":
|
|
334
|
+
lines.append(f'pm.test("{name}", function () {{ pm.expect(pm.response.responseTime).to.be.below({val}); }});')
|
|
335
|
+
elif kind == "header" and op == "exists":
|
|
336
|
+
lines.append(f'pm.test("{name}", function () {{ pm.response.to.have.header("{target}"); }});')
|
|
337
|
+
elif kind == "body" and op == "contains":
|
|
338
|
+
lines.append(f'pm.test("{name}", function () {{ pm.expect(pm.response.text()).to.include({json.dumps(val)}); }});')
|
|
339
|
+
elif kind == "json" and op == "equals":
|
|
340
|
+
lines.append(f'pm.test("{name}", function () {{ pm.expect(String(pm.response.json().{target})).to.eql({json.dumps(str(val))}); }});')
|
|
341
|
+
elif kind == "json" and op == "exists":
|
|
342
|
+
lines.append(f'pm.test("{name}", function () {{ pm.expect(pm.response.json().{target}).to.not.be.undefined; }});')
|
|
343
|
+
else:
|
|
344
|
+
lines.append(f"// pyhttp test not translated: {name}")
|
|
345
|
+
return lines
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def to_collection(name: str, folders: list[dict[str, Any]], requests: list[dict[str, Any]],
|
|
349
|
+
variables: dict[str, str]) -> dict[str, Any]:
|
|
350
|
+
"""Build a Postman v2.1 collection. Each named body becomes its own request."""
|
|
351
|
+
by_parent: dict[str | None, list[dict[str, Any]]] = {}
|
|
352
|
+
for f in folders:
|
|
353
|
+
by_parent.setdefault(f.get("parent_id"), []).append(f)
|
|
354
|
+
reqs_by_folder: dict[str | None, list[dict[str, Any]]] = {}
|
|
355
|
+
for r in requests:
|
|
356
|
+
reqs_by_folder.setdefault(r.get("folder_id"), []).append(r)
|
|
357
|
+
|
|
358
|
+
def request_items(r: dict[str, Any]) -> list[dict[str, Any]]:
|
|
359
|
+
bodies = [b for b in r.get("bodies") or [] if r.get("body_type", "none") != "none"] or [None]
|
|
360
|
+
items = []
|
|
361
|
+
for b in bodies:
|
|
362
|
+
body_text = b["body"] if b else ""
|
|
363
|
+
label = r["name"] if (b is None or len(bodies) == 1) else f"{r['name']} · {b['name']}"
|
|
364
|
+
url = r.get("url", "")
|
|
365
|
+
params = [p for p in r.get("params") or [] if p.get("key")]
|
|
366
|
+
pm_url: dict[str, Any] = {"raw": url + ("?" + "&".join(f"{p['key']}={p['value']}" for p in params if p.get("enabled", True)) if params else "")}
|
|
367
|
+
if params:
|
|
368
|
+
pm_url["query"] = _pm_kv(params)
|
|
369
|
+
req: dict[str, Any] = {"method": r.get("method", "GET"), "header": _pm_kv(r.get("headers") or []), "url": pm_url}
|
|
370
|
+
if r.get("description"):
|
|
371
|
+
req["description"] = r["description"]
|
|
372
|
+
auth = _pm_auth(r.get("auth"))
|
|
373
|
+
if auth:
|
|
374
|
+
req["auth"] = auth
|
|
375
|
+
body = _pm_body(r, body_text)
|
|
376
|
+
if body:
|
|
377
|
+
req["body"] = body
|
|
378
|
+
item: dict[str, Any] = {"name": label, "request": req, "response": []}
|
|
379
|
+
events = _events(_scripts_for(r, b), _pm_tests(r.get("tests") or []))
|
|
380
|
+
if events:
|
|
381
|
+
item["event"] = events
|
|
382
|
+
items.append(item)
|
|
383
|
+
return items
|
|
384
|
+
|
|
385
|
+
def walk(parent: str | None) -> list[dict[str, Any]]:
|
|
386
|
+
out: list[dict[str, Any]] = []
|
|
387
|
+
for f in by_parent.get(parent, []):
|
|
388
|
+
node: dict[str, Any] = {"name": f["name"], "item": walk(f["id"])}
|
|
389
|
+
if f.get("description"):
|
|
390
|
+
node["description"] = f["description"]
|
|
391
|
+
auth = _pm_auth(f.get("auth"))
|
|
392
|
+
if auth:
|
|
393
|
+
node["auth"] = auth
|
|
394
|
+
if f.get("variables"):
|
|
395
|
+
node["variable"] = _pm_kv(f["variables"])
|
|
396
|
+
out.append(node)
|
|
397
|
+
for r in reqs_by_folder.get(parent, []):
|
|
398
|
+
out.extend(request_items(r))
|
|
399
|
+
return out
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
"info": {"_postman_id": str(uuid.uuid4()), "name": name, "schema": SCHEMA},
|
|
403
|
+
"item": walk(None),
|
|
404
|
+
"variable": [{"key": k, "value": v, "type": "string"} for k, v in (variables or {}).items()],
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def to_environment(env: dict[str, Any]) -> dict[str, Any]:
|
|
409
|
+
return {"id": str(uuid.uuid4()), "name": env["name"], "_postman_variable_scope": "environment",
|
|
410
|
+
"values": [{"key": v["key"], "value": v["value"], "enabled": v.get("enabled", True),
|
|
411
|
+
"type": "secret" if v.get("secret") else "default"}
|
|
412
|
+
for v in env.get("variables") or []]}
|