@ohos-ports/curlconverter 4.12.0-beta.0

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.
Files changed (270) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/dist/src/Headers.d.ts +23 -0
  4. package/dist/src/Headers.js +287 -0
  5. package/dist/src/Headers.js.map +1 -0
  6. package/dist/src/Query.d.ts +8 -0
  7. package/dist/src/Query.js +136 -0
  8. package/dist/src/Query.js.map +1 -0
  9. package/dist/src/Request.d.ts +200 -0
  10. package/dist/src/Request.js +1191 -0
  11. package/dist/src/Request.js.map +1 -0
  12. package/dist/src/Warnings.d.ts +15 -0
  13. package/dist/src/Warnings.js +111 -0
  14. package/dist/src/Warnings.js.map +1 -0
  15. package/dist/src/cli.d.ts +2 -0
  16. package/dist/src/cli.js +333 -0
  17. package/dist/src/cli.js.map +1 -0
  18. package/dist/src/curl/auth.d.ts +12 -0
  19. package/dist/src/curl/auth.js +40 -0
  20. package/dist/src/curl/auth.js.map +1 -0
  21. package/dist/src/curl/form.d.ts +28 -0
  22. package/dist/src/curl/form.js +218 -0
  23. package/dist/src/curl/form.js.map +1 -0
  24. package/dist/src/curl/opts.d.ts +2052 -0
  25. package/dist/src/curl/opts.js +1000 -0
  26. package/dist/src/curl/opts.js.map +1 -0
  27. package/dist/src/curl/url.d.ts +14 -0
  28. package/dist/src/curl/url.js +218 -0
  29. package/dist/src/curl/url.js.map +1 -0
  30. package/dist/src/generators/ansible.d.ts +5 -0
  31. package/dist/src/generators/ansible.js +282 -0
  32. package/dist/src/generators/ansible.js.map +1 -0
  33. package/dist/src/generators/c.d.ts +12 -0
  34. package/dist/src/generators/c.js +1230 -0
  35. package/dist/src/generators/c.js.map +1 -0
  36. package/dist/src/generators/cfml.d.ts +5 -0
  37. package/dist/src/generators/cfml.js +134 -0
  38. package/dist/src/generators/cfml.js.map +1 -0
  39. package/dist/src/generators/clojure.d.ts +7 -0
  40. package/dist/src/generators/clojure.js +436 -0
  41. package/dist/src/generators/clojure.js.map +1 -0
  42. package/dist/src/generators/csharp.d.ts +8 -0
  43. package/dist/src/generators/csharp.js +356 -0
  44. package/dist/src/generators/csharp.js.map +1 -0
  45. package/dist/src/generators/dart.d.ts +5 -0
  46. package/dist/src/generators/dart.js +273 -0
  47. package/dist/src/generators/dart.js.map +1 -0
  48. package/dist/src/generators/elixir.d.ts +8 -0
  49. package/dist/src/generators/elixir.js +296 -0
  50. package/dist/src/generators/elixir.js.map +1 -0
  51. package/dist/src/generators/go.d.ts +5 -0
  52. package/dist/src/generators/go.js +228 -0
  53. package/dist/src/generators/go.js.map +1 -0
  54. package/dist/src/generators/har.d.ts +7 -0
  55. package/dist/src/generators/har.js +115 -0
  56. package/dist/src/generators/har.js.map +1 -0
  57. package/dist/src/generators/http.d.ts +7 -0
  58. package/dist/src/generators/http.js +165 -0
  59. package/dist/src/generators/http.js.map +1 -0
  60. package/dist/src/generators/httpie.d.ts +5 -0
  61. package/dist/src/generators/httpie.js +446 -0
  62. package/dist/src/generators/httpie.js.map +1 -0
  63. package/dist/src/generators/java/httpurlconnection.d.ts +5 -0
  64. package/dist/src/generators/java/httpurlconnection.js +123 -0
  65. package/dist/src/generators/java/httpurlconnection.js.map +1 -0
  66. package/dist/src/generators/java/java.d.ts +8 -0
  67. package/dist/src/generators/java/java.js +247 -0
  68. package/dist/src/generators/java/java.js.map +1 -0
  69. package/dist/src/generators/java/jsoup.d.ts +5 -0
  70. package/dist/src/generators/java/jsoup.js +152 -0
  71. package/dist/src/generators/java/jsoup.js.map +1 -0
  72. package/dist/src/generators/java/okhttp.d.ts +5 -0
  73. package/dist/src/generators/java/okhttp.js +232 -0
  74. package/dist/src/generators/java/okhttp.js.map +1 -0
  75. package/dist/src/generators/javascript/axios.d.ts +5 -0
  76. package/dist/src/generators/javascript/axios.js +332 -0
  77. package/dist/src/generators/javascript/axios.js.map +1 -0
  78. package/dist/src/generators/javascript/got.d.ts +5 -0
  79. package/dist/src/generators/javascript/got.js +277 -0
  80. package/dist/src/generators/javascript/got.js.map +1 -0
  81. package/dist/src/generators/javascript/http.d.ts +8 -0
  82. package/dist/src/generators/javascript/http.js +183 -0
  83. package/dist/src/generators/javascript/http.js.map +1 -0
  84. package/dist/src/generators/javascript/javascript.d.ts +37 -0
  85. package/dist/src/generators/javascript/javascript.js +676 -0
  86. package/dist/src/generators/javascript/javascript.js.map +1 -0
  87. package/dist/src/generators/javascript/jquery.d.ts +15 -0
  88. package/dist/src/generators/javascript/jquery.js +297 -0
  89. package/dist/src/generators/javascript/jquery.js.map +1 -0
  90. package/dist/src/generators/javascript/ky.d.ts +7 -0
  91. package/dist/src/generators/javascript/ky.js +245 -0
  92. package/dist/src/generators/javascript/ky.js.map +1 -0
  93. package/dist/src/generators/javascript/request.d.ts +5 -0
  94. package/dist/src/generators/javascript/request.js +113 -0
  95. package/dist/src/generators/javascript/request.js.map +1 -0
  96. package/dist/src/generators/javascript/superagent.d.ts +10 -0
  97. package/dist/src/generators/javascript/superagent.js +301 -0
  98. package/dist/src/generators/javascript/superagent.js.map +1 -0
  99. package/dist/src/generators/javascript/xhr.d.ts +8 -0
  100. package/dist/src/generators/javascript/xhr.js +147 -0
  101. package/dist/src/generators/javascript/xhr.js.map +1 -0
  102. package/dist/src/generators/json.d.ts +45 -0
  103. package/dist/src/generators/json.js +197 -0
  104. package/dist/src/generators/json.js.map +1 -0
  105. package/dist/src/generators/julia.d.ts +8 -0
  106. package/dist/src/generators/julia.js +365 -0
  107. package/dist/src/generators/julia.js.map +1 -0
  108. package/dist/src/generators/kotlin.d.ts +8 -0
  109. package/dist/src/generators/kotlin.js +325 -0
  110. package/dist/src/generators/kotlin.js.map +1 -0
  111. package/dist/src/generators/lua.d.ts +5 -0
  112. package/dist/src/generators/lua.js +192 -0
  113. package/dist/src/generators/lua.js.map +1 -0
  114. package/dist/src/generators/matlab/common.d.ts +16 -0
  115. package/dist/src/generators/matlab/common.js +238 -0
  116. package/dist/src/generators/matlab/common.js.map +1 -0
  117. package/dist/src/generators/matlab/httpinterface.d.ts +2 -0
  118. package/dist/src/generators/matlab/httpinterface.js +211 -0
  119. package/dist/src/generators/matlab/httpinterface.js.map +1 -0
  120. package/dist/src/generators/matlab/matlab.d.ts +5 -0
  121. package/dist/src/generators/matlab/matlab.js +29 -0
  122. package/dist/src/generators/matlab/matlab.js.map +1 -0
  123. package/dist/src/generators/matlab/webservices.d.ts +2 -0
  124. package/dist/src/generators/matlab/webservices.js +191 -0
  125. package/dist/src/generators/matlab/webservices.js.map +1 -0
  126. package/dist/src/generators/objectivec.d.ts +8 -0
  127. package/dist/src/generators/objectivec.js +269 -0
  128. package/dist/src/generators/objectivec.js.map +1 -0
  129. package/dist/src/generators/ocaml.d.ts +8 -0
  130. package/dist/src/generators/ocaml.js +150 -0
  131. package/dist/src/generators/ocaml.js.map +1 -0
  132. package/dist/src/generators/perl.d.ts +9 -0
  133. package/dist/src/generators/perl.js +209 -0
  134. package/dist/src/generators/perl.js.map +1 -0
  135. package/dist/src/generators/php/guzzle.d.ts +5 -0
  136. package/dist/src/generators/php/guzzle.js +328 -0
  137. package/dist/src/generators/php/guzzle.js.map +1 -0
  138. package/dist/src/generators/php/php.d.ts +8 -0
  139. package/dist/src/generators/php/php.js +217 -0
  140. package/dist/src/generators/php/php.js.map +1 -0
  141. package/dist/src/generators/php/requests.d.ts +5 -0
  142. package/dist/src/generators/php/requests.js +89 -0
  143. package/dist/src/generators/php/requests.js.map +1 -0
  144. package/dist/src/generators/powershell.d.ts +11 -0
  145. package/dist/src/generators/powershell.js +377 -0
  146. package/dist/src/generators/powershell.js.map +1 -0
  147. package/dist/src/generators/python/http.d.ts +5 -0
  148. package/dist/src/generators/python/http.js +128 -0
  149. package/dist/src/generators/python/http.js.map +1 -0
  150. package/dist/src/generators/python/python.d.ts +22 -0
  151. package/dist/src/generators/python/python.js +1693 -0
  152. package/dist/src/generators/python/python.js.map +1 -0
  153. package/dist/src/generators/r/httr.d.ts +10 -0
  154. package/dist/src/generators/r/httr.js +298 -0
  155. package/dist/src/generators/r/httr.js.map +1 -0
  156. package/dist/src/generators/r/httr2.d.ts +5 -0
  157. package/dist/src/generators/r/httr2.js +214 -0
  158. package/dist/src/generators/r/httr2.js.map +1 -0
  159. package/dist/src/generators/ruby/httparty.d.ts +6 -0
  160. package/dist/src/generators/ruby/httparty.js +308 -0
  161. package/dist/src/generators/ruby/httparty.js.map +1 -0
  162. package/dist/src/generators/ruby/ruby.d.ts +14 -0
  163. package/dist/src/generators/ruby/ruby.js +515 -0
  164. package/dist/src/generators/ruby/ruby.js.map +1 -0
  165. package/dist/src/generators/rust.d.ts +8 -0
  166. package/dist/src/generators/rust.js +177 -0
  167. package/dist/src/generators/rust.js.map +1 -0
  168. package/dist/src/generators/swift.d.ts +8 -0
  169. package/dist/src/generators/swift.js +219 -0
  170. package/dist/src/generators/swift.js.map +1 -0
  171. package/dist/src/generators/wget.d.ts +8 -0
  172. package/dist/src/generators/wget.js +472 -0
  173. package/dist/src/generators/wget.js.map +1 -0
  174. package/dist/src/index.d.ts +53 -0
  175. package/dist/src/index.js +54 -0
  176. package/dist/src/index.js.map +1 -0
  177. package/dist/src/parse.d.ts +13 -0
  178. package/dist/src/parse.js +43 -0
  179. package/dist/src/parse.js.map +1 -0
  180. package/dist/src/shell/Parser.d.ts +4 -0
  181. package/dist/src/shell/Parser.js +6 -0
  182. package/dist/src/shell/Parser.js.map +1 -0
  183. package/dist/src/shell/Word.d.ts +48 -0
  184. package/dist/src/shell/Word.js +486 -0
  185. package/dist/src/shell/Word.js.map +1 -0
  186. package/dist/src/shell/tokenizer.d.ts +3 -0
  187. package/dist/src/shell/tokenizer.js +610 -0
  188. package/dist/src/shell/tokenizer.js.map +1 -0
  189. package/dist/src/shell/webParser.d.ts +4 -0
  190. package/dist/src/shell/webParser.js +15 -0
  191. package/dist/src/shell/webParser.js.map +1 -0
  192. package/dist/src/utils.d.ts +6 -0
  193. package/dist/src/utils.js +21 -0
  194. package/dist/src/utils.js.map +1 -0
  195. package/dist/tools/compare-requests.d.ts +2 -0
  196. package/dist/tools/compare-requests.js +448 -0
  197. package/dist/tools/compare-requests.js.map +1 -0
  198. package/dist/tools/gen-test.d.ts +2 -0
  199. package/dist/tools/gen-test.js +114 -0
  200. package/dist/tools/gen-test.js.map +1 -0
  201. package/dist/tree-sitter-bash.wasm +0 -0
  202. package/index.d.ts +3 -0
  203. package/package.json +97 -0
  204. package/src/Headers.ts +334 -0
  205. package/src/Query.ts +147 -0
  206. package/src/Request.ts +1649 -0
  207. package/src/Warnings.ts +154 -0
  208. package/src/cli.ts +561 -0
  209. package/src/curl/auth.ts +45 -0
  210. package/src/curl/form.ts +297 -0
  211. package/src/curl/opts.ts +1417 -0
  212. package/src/curl/url.ts +241 -0
  213. package/src/generators/ansible.ts +364 -0
  214. package/src/generators/c.ts +1371 -0
  215. package/src/generators/cfml.ts +155 -0
  216. package/src/generators/clojure.ts +521 -0
  217. package/src/generators/csharp.ts +383 -0
  218. package/src/generators/dart.ts +293 -0
  219. package/src/generators/elixir.ts +349 -0
  220. package/src/generators/go.ts +269 -0
  221. package/src/generators/har.ts +147 -0
  222. package/src/generators/http.ts +194 -0
  223. package/src/generators/httpie.ts +506 -0
  224. package/src/generators/java/httpurlconnection.ts +151 -0
  225. package/src/generators/java/java.ts +280 -0
  226. package/src/generators/java/jsoup.ts +172 -0
  227. package/src/generators/java/okhttp.ts +257 -0
  228. package/src/generators/javascript/axios.ts +395 -0
  229. package/src/generators/javascript/got.ts +362 -0
  230. package/src/generators/javascript/http.ts +232 -0
  231. package/src/generators/javascript/javascript.ts +825 -0
  232. package/src/generators/javascript/jquery.ts +360 -0
  233. package/src/generators/javascript/ky.ts +315 -0
  234. package/src/generators/javascript/request.ts +152 -0
  235. package/src/generators/javascript/superagent.ts +373 -0
  236. package/src/generators/javascript/xhr.ts +192 -0
  237. package/src/generators/json.ts +293 -0
  238. package/src/generators/julia.ts +398 -0
  239. package/src/generators/kotlin.ts +355 -0
  240. package/src/generators/lua.ts +209 -0
  241. package/src/generators/matlab/common.ts +281 -0
  242. package/src/generators/matlab/httpinterface.ts +284 -0
  243. package/src/generators/matlab/matlab.ts +39 -0
  244. package/src/generators/matlab/webservices.ts +263 -0
  245. package/src/generators/objectivec.ts +297 -0
  246. package/src/generators/ocaml.ts +168 -0
  247. package/src/generators/perl.ts +237 -0
  248. package/src/generators/php/guzzle.ts +368 -0
  249. package/src/generators/php/php.ts +239 -0
  250. package/src/generators/php/requests.ts +103 -0
  251. package/src/generators/powershell.ts +430 -0
  252. package/src/generators/python/http.ts +167 -0
  253. package/src/generators/python/python.ts +1934 -0
  254. package/src/generators/r/httr.ts +328 -0
  255. package/src/generators/r/httr2.ts +268 -0
  256. package/src/generators/ruby/httparty.ts +351 -0
  257. package/src/generators/ruby/ruby.ts +564 -0
  258. package/src/generators/rust.ts +243 -0
  259. package/src/generators/swift.ts +235 -0
  260. package/src/generators/wget.ts +533 -0
  261. package/src/index.ts +89 -0
  262. package/src/parse.ts +72 -0
  263. package/src/shell/Parser.ts +8 -0
  264. package/src/shell/Word.ts +531 -0
  265. package/src/shell/tokenizer.ts +736 -0
  266. package/src/shell/webParser.ts +18 -0
  267. package/src/utils.ts +25 -0
  268. package/tools/compare-requests.ts +571 -0
  269. package/tools/extract_curl_args.py +511 -0
  270. package/tools/gen-test.ts +129 -0
@@ -0,0 +1,1693 @@
1
+ import { CCError, has, UTF8encoder } from "../../utils.js";
2
+ import { Word, eq } from "../../shell/Word.js";
3
+ import { parse, COMMON_SUPPORTED_ARGS } from "../../parse.js";
4
+ import { wordDecodeURIComponent, percentEncode } from "../../Query.js";
5
+ import { parse as jsonParseLossless, stringify as jsonStringifyLossless, isSafeNumber, isInteger, isLosslessNumber, } from "lossless-json";
6
+ // TODO: partiallySupportedArgs
7
+ export const supportedArgs = new Set([
8
+ ...COMMON_SUPPORTED_ARGS,
9
+ "compressed",
10
+ // "no-compressed",
11
+ // "anyauth",
12
+ // "no-anyauth",
13
+ "digest",
14
+ "no-digest",
15
+ "aws-sigv4",
16
+ "negotiate",
17
+ "no-negotiate",
18
+ "delegation", // GSS/kerberos
19
+ // "service-name", // GSS/kerberos, not supported
20
+ "ntlm",
21
+ "no-ntlm",
22
+ "ntlm-wb",
23
+ "no-ntlm-wb",
24
+ "http1.1",
25
+ // These are not supported, just better warning message
26
+ "http2",
27
+ "http2-prior-knowledge",
28
+ "http3",
29
+ "http3-only",
30
+ "cookie-jar",
31
+ "cert",
32
+ "cacert",
33
+ "key",
34
+ "capath",
35
+ "form",
36
+ "form-string",
37
+ "location",
38
+ "no-location",
39
+ "location-trusted", // not exactly supported, just better warning message
40
+ "no-location-trusted",
41
+ "max-redirs",
42
+ "max-time",
43
+ "connect-timeout",
44
+ "insecure",
45
+ "no-insecure",
46
+ "output",
47
+ "upload-file",
48
+ "next",
49
+ "proxy",
50
+ "proxy-user",
51
+ ]);
52
+ // https://peps.python.org/pep-3138/
53
+ // https://www.unicode.org/reports/tr44/#GC_Values_Table
54
+ // https://unicode.org/Public/UNIDATA/UnicodeData.txt
55
+ // https://en.wikipedia.org/wiki/Plane_(Unicode)#Overview
56
+ const regexSingleEscape = /'|\\|\p{C}|[^ \P{Z}]/gu;
57
+ const regexDoubleEscape = /"|\\|\p{C}|[^ \P{Z}]/gu;
58
+ // Also used for Go and R
59
+ export function reprStr(s, quote) {
60
+ if (quote === undefined) {
61
+ quote = "'";
62
+ if (s.includes("'") && !s.includes('"')) {
63
+ quote = '"';
64
+ }
65
+ }
66
+ const regex = quote === "'" ? regexSingleEscape : regexDoubleEscape;
67
+ return (quote +
68
+ s.replace(regex, (c) => {
69
+ switch (c) {
70
+ case "\x07":
71
+ return "\\a";
72
+ case "\b":
73
+ return "\\b";
74
+ case "\f":
75
+ return "\\f";
76
+ case "\n":
77
+ return "\\n";
78
+ case "\r":
79
+ return "\\r";
80
+ case "\t":
81
+ return "\\t";
82
+ case "\v":
83
+ return "\\v";
84
+ case "\\":
85
+ return "\\\\";
86
+ case "'":
87
+ return "\\'";
88
+ case '"':
89
+ return '\\"';
90
+ }
91
+ const hex = c.codePointAt(0).toString(16);
92
+ if (hex.length <= 2) {
93
+ return "\\x" + hex.padStart(2, "0");
94
+ }
95
+ if (hex.length <= 4) {
96
+ return "\\u" + hex.padStart(4, "0");
97
+ }
98
+ return "\\U" + hex.padStart(8, "0");
99
+ }) +
100
+ quote);
101
+ }
102
+ // TODO: use this if string contains unmatched surrogates?
103
+ // It just replaces them with the replacement character, but at least that code would run.
104
+ export function pybescComplex(s) {
105
+ let quote = "'";
106
+ if (s.includes("'") && !s.includes('"')) {
107
+ quote = '"';
108
+ }
109
+ const quoteCode = quote.charCodeAt(0);
110
+ // TODO: using UTF-8 here is overly simplistic and how to encode here
111
+ // is a pretty complicated decision.
112
+ // For starters, it would be more correct to use the same encoding as
113
+ // the terminal when running from the command line.
114
+ const bytes = UTF8encoder.encode(s);
115
+ return ("b" +
116
+ quote +
117
+ [...bytes]
118
+ .map((c) => {
119
+ switch (c) {
120
+ case 0x07:
121
+ return "\\a";
122
+ case 0x08:
123
+ return "\\b";
124
+ case 0x0c:
125
+ return "\\f";
126
+ case 0x0a:
127
+ return "\\n";
128
+ case 0x0d:
129
+ return "\\r";
130
+ case 0x09:
131
+ return "\\t";
132
+ case 0x0b:
133
+ return "\\v";
134
+ case 0x5c:
135
+ return "\\\\";
136
+ case quoteCode:
137
+ return "\\" + String.fromCharCode(c);
138
+ }
139
+ if (c >= 0x20 && c < 0x7f) {
140
+ return String.fromCharCode(c);
141
+ }
142
+ const hex = c.toString(16);
143
+ return "\\x" + hex.padStart(2, "0");
144
+ })
145
+ .join("") +
146
+ quote);
147
+ }
148
+ export function reprStrBinary(s) {
149
+ const sEsc = reprStr(s);
150
+ // We check until 0x7F instead of 0xFF because curl (running in a UTF-8 terminal) when it gets
151
+ // bytes sends them as is, but if we pass b'\x80' to Requests, it will encode that byte as
152
+ // Latin-1 (presumably for backwards compatibility) instead of UTF-8.
153
+ if (/^[\x00-\x7f]*$/.test(s)) {
154
+ return "b" + sEsc;
155
+ }
156
+ // TODO: unmatched surrogates will generate code that throws an error
157
+ // e.g.: '\uDC00'.encode()
158
+ return sEsc + ".encode()";
159
+ }
160
+ export function repr(word, osVars, imports, binary = false,
161
+ // os.getenv('MYVAR') returns None if MYVAR is not set
162
+ // os.getenv('MYVAR', '') returns '' if MYVAR is not set but it's a bit more verbose,
163
+ // so setting errorOk to true will use the shorter version
164
+ errorOk = false) {
165
+ const reprFn = binary ? reprStrBinary : reprStr;
166
+ const reprs = [];
167
+ for (const t of word.tokens) {
168
+ if (typeof t === "string") {
169
+ reprs.push(reprFn(t));
170
+ }
171
+ else if (t.type === "variable") {
172
+ // TODO: getenvb() is not available on Windows
173
+ const fn = binary ? "os.getenvb" : "os.getenv";
174
+ let getEnvCall = fn + "(" + reprFn(t.value);
175
+ if (!errorOk || word.tokens.length > 1) {
176
+ getEnvCall += ", " + reprFn("");
177
+ }
178
+ getEnvCall += ")";
179
+ reprs.push(getEnvCall);
180
+ // TODO: if the result of a repr() is discarded, this adds an unused import
181
+ imports.add("os");
182
+ }
183
+ else if (t.type === "command") {
184
+ // TODO: warn that shell=True is a bad idea
185
+ // or properly parse the subcommand and render it as an array
186
+ let subprocessCall = "subprocess.run(" +
187
+ reprStr(t.value) +
188
+ ", shell=True, capture_output=True";
189
+ if (!binary) {
190
+ subprocessCall += ", text=True";
191
+ }
192
+ subprocessCall += ").stdout";
193
+ // TODO: generate a descriptive command name with ChatGPT
194
+ // TODO: if there's only one command, name the variable "command" instead of "command1"
195
+ let i = 1;
196
+ let pyVar = "command" + i;
197
+ // We need to check because we often try to represent the same
198
+ // token twice and discard one of the attempts.
199
+ // This is linear time but hopefully there's not that many subcommands.
200
+ while (pyVar in osVars && osVars[pyVar] !== subprocessCall) {
201
+ i++;
202
+ pyVar = "command" + i;
203
+ if (i > Number.MAX_SAFE_INTEGER) {
204
+ throw new CCError("lol");
205
+ }
206
+ }
207
+ osVars[pyVar] = subprocessCall;
208
+ reprs.push(pyVar);
209
+ // TODO: if the result of a repr() is discarded, this adds an unused import
210
+ imports.add("subprocess");
211
+ }
212
+ }
213
+ return reprs.join(" + ");
214
+ }
215
+ function reprb(word, osVars, imports) {
216
+ return repr(word, osVars, imports, true);
217
+ }
218
+ export function asFloat(word, osVars, imports) {
219
+ if (word.isString()) {
220
+ // TODO: check it's actually a valid float
221
+ return word.toString();
222
+ }
223
+ return "float(" + repr(word, osVars, imports, false, true) + ")";
224
+ }
225
+ export function asInt(word, osVars, imports) {
226
+ if (word.isString()) {
227
+ // TODO: check it's actually a valid int
228
+ return word.toString();
229
+ }
230
+ return "int(" + repr(word, osVars, imports, false, true) + ")";
231
+ }
232
+ // Port of Python's json.dumps() with its default options, which is what Requests uses
233
+ // https://github.com/psf/requests/blob/b0e025ade7ed30ed53ab61f542779af7e024932e/requests/models.py#L473
234
+ // It's different from JSON.stringify(). Namely, it adds spaces after ',' and ':'
235
+ // and all non-ASCII characters in strings are escaped:
236
+ // > JSON.stringify('\xFF')
237
+ // '"ÿ"'
238
+ // >>> json.dumps('\xFF')
239
+ // '"\\u00ff"'
240
+ const pythonJsonEscape = /"|\\|[^\x20-\x7E]/g;
241
+ function jsonRepr(s) {
242
+ return ('"' +
243
+ s.replace(pythonJsonEscape, (c) => {
244
+ // https://tc39.es/ecma262/#table-json-single-character-escapes
245
+ switch (c) {
246
+ case "\b":
247
+ return "\\b";
248
+ case "\t":
249
+ return "\\t";
250
+ case "\n":
251
+ return "\\n";
252
+ case "\f":
253
+ return "\\f";
254
+ case "\r":
255
+ return "\\r";
256
+ case "\\":
257
+ return "\\\\";
258
+ case '"':
259
+ return '\\"';
260
+ }
261
+ const hex = c.charCodeAt(0).toString(16);
262
+ return "\\u" + hex.padStart(4, "0");
263
+ }) +
264
+ '"');
265
+ }
266
+ function ensure_minimum_exponent_length(n) {
267
+ // If there's only 1 exponent digit, add a leading 0 to it
268
+ // ensure_minimum_exponent_length('1e-7') => '1e-07'
269
+ const [mantissa, exponent] = n.split("e");
270
+ const exponentSign = exponent[0];
271
+ const exponentValue = exponent.slice(1);
272
+ if (exponentValue.length === 1) {
273
+ return mantissa + "e" + exponentSign + "0" + exponentValue;
274
+ }
275
+ return n;
276
+ }
277
+ function floatAsPython(value) {
278
+ // JSON.stringify() and lossless-json's stringify() don't stringify floats like Python.
279
+ // Notably, JavaScript doesn't add a trailing '.0' to floats that are integers but Python does
280
+ // JSON.stringify(10.0) => '10'
281
+ // str(10.0) => '10.0'
282
+ //
283
+ // Python adds a leading 0 to exponent notation numbers with 1 exponent digit
284
+ // JSON.stringify(1e-7) => '1e-7'
285
+ // str(1e-7) => '1e-07'
286
+ //
287
+ // Finally, Python will switch to scientific notation if the number has more than
288
+ // 17 digits not in scientific notation.
289
+ //
290
+ // Python's float formatting starts here:
291
+ // https://github.com/python/cpython/blob/bdc93b8a3563b4a3adb25fa902c0c879ccf427f6/Python/pystrtod.c#L915-L918
292
+ // and is ultimately this code:
293
+ // snprintf(buf, buf_size, "%.17g", val)
294
+ // change_decimal_from_locale_to_dot(buffer); // not important
295
+ // ensure_minimum_exponent_length(buffer, buf_size);
296
+ // ensure_decimal_point(buffer, buf_size, 17); // can switch to exponent notation
297
+ //
298
+ // And JavaScript's formatting is specified here:
299
+ // https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-number-tostring
300
+ let asJsStr = value.toString();
301
+ if (asJsStr.includes("e")) {
302
+ asJsStr = ensure_minimum_exponent_length(asJsStr);
303
+ }
304
+ else {
305
+ if (isInteger(asJsStr)) {
306
+ asJsStr += ".0";
307
+ }
308
+ // If there's more than 17 digits of precision, switch to scientific notation
309
+ const significantDigits = asJsStr
310
+ .replace(/^-/, "")
311
+ .replace(".", "")
312
+ .replace(/^0+/, "");
313
+ const asExponential = ensure_minimum_exponent_length(value.toExponential());
314
+ if (significantDigits.length > 17 ||
315
+ (asExponential.length < asJsStr.length &&
316
+ asJsStr.split(".")[1].length > 4)) {
317
+ asJsStr = asExponential;
318
+ }
319
+ }
320
+ return asJsStr;
321
+ }
322
+ function jsonDumps(obj) {
323
+ if (isLosslessNumber(obj)) {
324
+ const numAsStr = jsonStringifyLossless(obj);
325
+ if (isInteger(numAsStr)) {
326
+ return numAsStr;
327
+ }
328
+ if (!isSafeNumber(numAsStr)) {
329
+ throw new CCError("float unrepresentable in Python: " + numAsStr);
330
+ }
331
+ // Can't be bigint because it's !isInteger and isSafeNumber
332
+ return floatAsPython(obj.valueOf());
333
+ }
334
+ switch (typeof obj) {
335
+ case "string":
336
+ return jsonRepr(obj);
337
+ case "number":
338
+ // If the number in the JSON file is very large, it'll turn into Infinity
339
+ if (!isFinite(obj)) {
340
+ throw new CCError("found Infitiny in JSON");
341
+ }
342
+ // TODO: If the number in the JSON file is too big for JavaScript, we will lose information
343
+ // TODO: JavaScript and Python serialize floats differently.
344
+ // JSON.stringify(2e2) => 200
345
+ // json.dumps(2e2) => 200.0
346
+ return obj.toString();
347
+ case "boolean":
348
+ return obj.toString();
349
+ case "object":
350
+ if (obj === null) {
351
+ return "null";
352
+ }
353
+ if (Array.isArray(obj)) {
354
+ return "[" + obj.map(jsonDumps).join(", ") + "]";
355
+ }
356
+ return ("{" +
357
+ Object.entries(obj)
358
+ .map((e) => jsonRepr(e[0]) + ": " + jsonDumps(e[1]))
359
+ .join(", ") +
360
+ "}");
361
+ default:
362
+ throw new CCError("unexpected object type that shouldn't appear in JSON: " + typeof obj);
363
+ }
364
+ }
365
+ function objToPython(obj, indent = 0) {
366
+ if (isLosslessNumber(obj)) {
367
+ const numAsStr = jsonStringifyLossless(obj);
368
+ // If the number is a large float, it might not be representable in Python
369
+ // Both JavaScript and Python use f64 so we check if the float
370
+ // is representable in JavaScript.
371
+ if (!isInteger(numAsStr) && !isSafeNumber(numAsStr)) {
372
+ throw new CCError("float unrepresentable in Python: " + numAsStr);
373
+ }
374
+ // Displaying floats as they will be serialized in Python would help users
375
+ // understand why they're getting the "JSON won't be serialized as it was originally"
376
+ // message, but I think displaying them as they appear in the JSON is likely
377
+ // to be more convenient if you need to edit the value.
378
+ return numAsStr;
379
+ }
380
+ switch (typeof obj) {
381
+ case "string":
382
+ return reprStr(obj);
383
+ case "number":
384
+ // TODO: there are differences in number serialization between Python and JavaScript
385
+ // TODO: if the number in the JSON file is too big for JavaScript, we will lose information
386
+ return obj.toString();
387
+ case "boolean":
388
+ return obj ? "True" : "False";
389
+ case "object":
390
+ if (obj === null) {
391
+ return "None";
392
+ }
393
+ if (Array.isArray(obj)) {
394
+ if (obj.length === 0) {
395
+ return "[]";
396
+ }
397
+ let s = "[\n";
398
+ for (const item of obj) {
399
+ s += " ".repeat(indent + 4) + objToPython(item, indent + 4) + ",\n";
400
+ }
401
+ s += " ".repeat(indent) + "]";
402
+ return s;
403
+ }
404
+ if (Object.keys(obj).length === 0) {
405
+ return "{}";
406
+ }
407
+ {
408
+ let s = "{\n";
409
+ for (const [k, v] of Object.entries(obj)) {
410
+ // repr() because JSON keys must be strings.
411
+ s +=
412
+ " ".repeat(indent + 4) +
413
+ reprStr(k) +
414
+ ": " +
415
+ objToPython(v, indent + 4) +
416
+ ",\n";
417
+ }
418
+ s += " ".repeat(indent) + "}";
419
+ return s;
420
+ }
421
+ default:
422
+ throw new CCError("unexpected object type that shouldn't appear in JSON: " + typeof obj);
423
+ }
424
+ }
425
+ export function formatHeaders(headers, commentedOutHeaders, osVars, imports) {
426
+ // TODO: what if there are repeat headers
427
+ let headerDict = "headers = {\n";
428
+ for (const [headerName, headerValue] of headers) {
429
+ if (headerValue === null) {
430
+ continue;
431
+ }
432
+ let lineStart;
433
+ const headerNameLower = headerName.toLowerCase().toString();
434
+ if (has(commentedOutHeaders, headerNameLower)) {
435
+ if (commentedOutHeaders[headerNameLower]) {
436
+ headerDict += " # " + commentedOutHeaders[headerNameLower] + "\n";
437
+ }
438
+ lineStart = " # ";
439
+ }
440
+ else {
441
+ lineStart = " ";
442
+ }
443
+ headerDict +=
444
+ lineStart +
445
+ repr(headerName, osVars, imports) +
446
+ ": " +
447
+ repr(headerValue, osVars, imports) +
448
+ ",\n";
449
+ }
450
+ headerDict += "}\n";
451
+ return headerDict;
452
+ }
453
+ function decodePercentEncoding(s) {
454
+ let decoded;
455
+ try {
456
+ // https://url.spec.whatwg.org/#urlencoded-parsing recommends replacing + with space
457
+ // before decoding.
458
+ decoded = wordDecodeURIComponent(s.replace(/\+/g, " "));
459
+ }
460
+ catch (e) {
461
+ if (e instanceof URIError) {
462
+ // String contains invalid percent encoded characters
463
+ return null;
464
+ }
465
+ throw e;
466
+ }
467
+ // If the query string doesn't round-trip, we cannot properly convert it.
468
+ const roundTripKey = percentEncode(decoded);
469
+ // If the original data used %20 instead of + (what requests will send), that's close enough
470
+ if (!eq(roundTripKey, s) && !eq(roundTripKey.replace(/%20/g, "+"), s)) {
471
+ return null;
472
+ }
473
+ return decoded;
474
+ }
475
+ function dataEntriesToDict(dataEntries) {
476
+ // Group keys
477
+ // TODO: because keys can be code that reads from a file, those should not be considered the
478
+ // same key, for example what if that file is /dev/urandom.
479
+ // TODO: would we need to distinguish if /dev/urandom came from @/dev/urandom or from @-?
480
+ const asDict = {};
481
+ let prevKey = null;
482
+ for (const [key, val] of dataEntries) {
483
+ if (prevKey === key) {
484
+ asDict[key].push(val);
485
+ }
486
+ else {
487
+ if (!Object.prototype.hasOwnProperty.call(asDict, key)) {
488
+ asDict[key] = [val];
489
+ }
490
+ else {
491
+ // A repeated key with a different key between one of its repetitions
492
+ // means we can't represent these entries as a dictionary.
493
+ return null;
494
+ }
495
+ }
496
+ prevKey = key;
497
+ }
498
+ return asDict;
499
+ }
500
+ function dataEntriesToPython(dataEntries) {
501
+ if (dataEntries.length === 0) {
502
+ return "''"; // This shouldn't happen
503
+ }
504
+ const entriesDict = dataEntriesToDict(dataEntries);
505
+ if (entriesDict !== null) {
506
+ if (Object.keys(entriesDict).length === 0) {
507
+ return "''"; // This shouldn't happen
508
+ }
509
+ let s = "{\n";
510
+ for (const [key, vals] of Object.entries(entriesDict)) {
511
+ s += " " + key + ": ";
512
+ if (vals.length === 0) {
513
+ s += "''"; // This shouldn't happen
514
+ }
515
+ else if (vals.length === 1) {
516
+ s += vals[0];
517
+ }
518
+ else {
519
+ s += "[\n";
520
+ for (const val of vals) {
521
+ s += " " + val + ",\n";
522
+ }
523
+ s += " ]";
524
+ }
525
+ s += ",\n";
526
+ }
527
+ s += "}";
528
+ return s;
529
+ }
530
+ let s = "[\n";
531
+ for (const entry of dataEntries) {
532
+ const [key, val] = entry;
533
+ s += " (" + key + ", " + val + "),\n";
534
+ }
535
+ s += "]";
536
+ return s;
537
+ }
538
+ function formatDataAsEntries(dataArray, osVars, imports, variableName = "data") {
539
+ // This code is more complicated than you might expect because it needs
540
+ // to handle a --data-urlencode that reads from a file followed by --json
541
+ // because --json doesn't add an '&' before its value. Specifically, we
542
+ // have these cases:
543
+ //
544
+ // --data-urlencode @filename --json =value
545
+ // {open('filename').read(): 'value'}
546
+ //
547
+ // --data-urlencode @filename --json key=value
548
+ // {open('filename').read() + 'key': 'value'}
549
+ //
550
+ // --data-urlencode @filename --json key
551
+ // error
552
+ //
553
+ // --data-urlencode name@filename --json value
554
+ // {'name': open('filename').read() + 'value'}
555
+ //
556
+ // --data-urlencode name@filename --json key=value
557
+ // error
558
+ //
559
+ // --data-urlencode name@filename --json =blah
560
+ // error
561
+ //
562
+ // --data-urlencode adds an '&' before its value, so we don't have to worry about
563
+ // --json <foo> --data-urlencode <bar>
564
+ for (const d of dataArray) {
565
+ if (!(d instanceof Word) && d.filetype !== "urlencode") {
566
+ return null;
567
+ }
568
+ }
569
+ const dataEntries = [];
570
+ let percentWarn = new Word();
571
+ for (const [i, d] of dataArray.entries()) {
572
+ if (d instanceof Word) {
573
+ let newEntries = d.split("&");
574
+ const prevEntry = i > 0 ? dataEntries[dataEntries.length - 1] : null;
575
+ if (prevEntry !== null) {
576
+ // If there's a prevEntry, we can assume it came from --data-urlencode
577
+ // because it would be part of the current `d` string if it didn't.
578
+ const [first, ...rest] = newEntries;
579
+ if (first.includes("=") && prevEntry[1] === null) {
580
+ const [key, val] = first.split("=", 2);
581
+ const decodedKey = decodePercentEncoding(key);
582
+ if (decodedKey === null) {
583
+ return null;
584
+ }
585
+ const decodedVal = decodePercentEncoding(val);
586
+ if (decodedVal === null) {
587
+ return null;
588
+ }
589
+ if (key.toBool()) {
590
+ prevEntry[0] += " + " + repr(decodedKey, osVars, imports);
591
+ }
592
+ prevEntry[1] = repr(decodedVal, osVars, imports);
593
+ if (!percentWarn.toBool()) {
594
+ if (key.includes("%20")) {
595
+ percentWarn = key;
596
+ }
597
+ if (val.includes("%20")) {
598
+ percentWarn = val;
599
+ }
600
+ }
601
+ }
602
+ else if (!first.includes("=") && prevEntry[1] !== null) {
603
+ if (first.toBool()) {
604
+ const decodedVal = decodePercentEncoding(first);
605
+ if (decodedVal === null) {
606
+ return null;
607
+ }
608
+ prevEntry[1] += " + " + repr(decodedVal, osVars, imports);
609
+ if (!percentWarn.toBool() && first.includes("%20")) {
610
+ percentWarn = first;
611
+ }
612
+ }
613
+ }
614
+ else {
615
+ return null;
616
+ }
617
+ newEntries = rest;
618
+ }
619
+ for (const [j, entry] of newEntries.entries()) {
620
+ if (entry.isEmpty() &&
621
+ j === newEntries.length - 1 &&
622
+ i !== dataArray.length - 1) {
623
+ // A --data-urlencoded should come next
624
+ continue;
625
+ }
626
+ if (!entry.includes("=")) {
627
+ return null;
628
+ }
629
+ const [key, val] = entry.split("=", 2);
630
+ const decodedKey = decodePercentEncoding(key);
631
+ if (decodedKey === null) {
632
+ return null;
633
+ }
634
+ const decodedVal = decodePercentEncoding(val);
635
+ if (decodedVal === null) {
636
+ return null;
637
+ }
638
+ dataEntries.push([
639
+ repr(decodedKey, osVars, imports),
640
+ repr(decodedVal, osVars, imports),
641
+ ]);
642
+ if (!percentWarn.toBool()) {
643
+ if (key.includes("%20")) {
644
+ percentWarn = key;
645
+ }
646
+ if (val.includes("%20")) {
647
+ percentWarn = val;
648
+ }
649
+ }
650
+ }
651
+ continue;
652
+ }
653
+ // TODO: I bet Python doesn't treat file paths identically to curl
654
+ const readFile = eq(d.filename, "-")
655
+ ? "sys.stdin.read()"
656
+ : "open(" + repr(d.filename, osVars, imports, false, true) + ").read()";
657
+ if (!d.name) {
658
+ dataEntries.push([readFile, null]);
659
+ }
660
+ else {
661
+ // Curl doesn't percent encode the name but Requests will
662
+ if (!eq(d.name, percentEncode(d.name))) {
663
+ return null;
664
+ }
665
+ dataEntries.push([repr(d.name, osVars, imports), readFile]);
666
+ }
667
+ }
668
+ if (dataEntries.some((e) => e[1] === null)) {
669
+ return null;
670
+ }
671
+ return [
672
+ variableName +
673
+ " = " +
674
+ dataEntriesToPython(dataEntries) +
675
+ "\n",
676
+ percentWarn.toString(),
677
+ ];
678
+ }
679
+ function formatDataAsStr(dataArray, imports, osVars, variableName = "data") {
680
+ // If one of the arguments has to be binary, then they all have to be binary
681
+ // because we can't mix bytes and str.
682
+ // An argument has to be binary when the input command has
683
+ // --data-binary @filename
684
+ // otherwise we could generate code that will try to read an image file as text and error.
685
+ const binary = dataArray.some((d) => !(d instanceof Word) && d.filetype === "binary");
686
+ const reprFunc = binary ? reprb : repr;
687
+ const prefix = binary ? "b" : "";
688
+ const mode = binary ? ", 'rb'" : "";
689
+ // If we see a string with non-ASCII characters, or read from a file (which may contain
690
+ // non-ASCII characters), we convert the entire string to bytes at the end.
691
+ // curl sends bytes as-is, which is presumably in UTF-8, whereas Requests sends
692
+ // 0x80-0xFF as Latin-1 (as-is) and throws an error if it sees codepoints
693
+ // above 0xFF.
694
+ // TODO: is this actually helpful?
695
+ let encode = false;
696
+ let encodeOnSeparateLine = false;
697
+ const lines = [];
698
+ let extra = new Word();
699
+ let i, d;
700
+ for ([i, d] of dataArray.entries()) {
701
+ const op = i === 0 ? "=" : "+=";
702
+ let line = variableName + " " + op + " ";
703
+ if (i < dataArray.length - 1 && d instanceof Word && d.endsWith("&")) {
704
+ // Put the trailing '&' on the next line so that we don't have single '&'s on their own lines
705
+ extra = new Word("&");
706
+ d = d.slice(0, -1);
707
+ }
708
+ if (d instanceof Word) {
709
+ if (d.length) {
710
+ line += reprFunc(d, osVars, imports);
711
+ lines.push(line);
712
+ encode || (encode = d.test(/[^\x00-\x7F]/));
713
+ }
714
+ continue;
715
+ }
716
+ const { filetype, name, filename } = d;
717
+ if (filetype === "urlencode" && name) {
718
+ line += reprFunc(extra.add(name).append("="), osVars, imports) + " + ";
719
+ encodeOnSeparateLine = true; // we would need to add parentheses because of the +
720
+ }
721
+ else if (extra.toBool()) {
722
+ line += reprFunc(extra, osVars, imports) + " + ";
723
+ encodeOnSeparateLine = true;
724
+ }
725
+ if (extra.toBool()) {
726
+ encodeOnSeparateLine = true; // we would need to add parentheses because of the +
727
+ }
728
+ let readFile = "";
729
+ if (eq(filename, "-")) {
730
+ readFile += binary ? "sys.stdin.buffer" : "sys.stdin";
731
+ imports.add("sys");
732
+ }
733
+ else {
734
+ // TODO: if filename is a command, this won't work because unlike bash,
735
+ // Python won't remove the trailing newline from the result of a command
736
+ // we need to add .trim()
737
+ line =
738
+ "with open(" +
739
+ repr(filename, osVars, imports, false, true) +
740
+ mode +
741
+ ") as f:\n " +
742
+ line;
743
+ readFile += "f";
744
+ }
745
+ readFile += ".read()";
746
+ if (!["binary", "json", "urlencode"].includes(filetype)) {
747
+ readFile += `.replace(${prefix}'\\n', ${prefix}'').replace(${prefix}'\\r', ${prefix}'')`;
748
+ }
749
+ if (filetype === "urlencode") {
750
+ readFile = "quote_plus(" + readFile + ")";
751
+ if (binary) {
752
+ // quote_plus() always returns a string
753
+ readFile += ".encode()";
754
+ }
755
+ imports.add("urllib.parse.quote_plus");
756
+ }
757
+ else {
758
+ // --data-urlencode files don't need to be encoded because
759
+ // they'll be percent-encoded and therefore ASCII-only
760
+ encode = true;
761
+ }
762
+ line += readFile;
763
+ lines.push(line);
764
+ extra = new Word();
765
+ }
766
+ if (binary) {
767
+ encode = false;
768
+ }
769
+ else if (encode && lines.length === 1 && !encodeOnSeparateLine) {
770
+ lines[lines.length - 1] += ".encode()";
771
+ encode = false;
772
+ }
773
+ return [lines.join("\n") + "\n", encode];
774
+ }
775
+ export function formatDataAsJson(d, imports, osVars) {
776
+ if (d instanceof Word) {
777
+ if (!d.isString()) {
778
+ // TODO: a JSON parser that allows shell variables
779
+ return [null, false];
780
+ }
781
+ // Try to parse using lossless-json first, then fall back to JSON.parse
782
+ // TODO: repeated dictionary keys are discarded
783
+ // https://github.com/josdejong/lossless-json/issues/244
784
+ let dataAsJson;
785
+ try {
786
+ // TODO: types
787
+ // https://github.com/josdejong/lossless-json/issues/245
788
+ dataAsJson = jsonParseLossless(d.toString());
789
+ }
790
+ catch (_a) {
791
+ try {
792
+ dataAsJson = JSON.parse(d.toString());
793
+ }
794
+ catch (_b) {
795
+ return [null, false];
796
+ }
797
+ }
798
+ try {
799
+ const jsonDataString = "json_data = " + objToPython(dataAsJson) + "\n";
800
+ // JSON might not be serialized by Python exactly as it was originally
801
+ // due to different whitespace, float formatting like extra + in exponent
802
+ // (1e100 vs 1e+100), different escape sequences in strings
803
+ // ("\/" vs "/" or "\u0008" vs "\b") or duplicate object keys.
804
+ let jsonRoundtrips = false;
805
+ try {
806
+ jsonRoundtrips = jsonDumps(dataAsJson) === d.tokens[0];
807
+ }
808
+ catch (_c) { }
809
+ return [jsonDataString, jsonRoundtrips];
810
+ }
811
+ catch (_d) { }
812
+ }
813
+ else if (d.filetype === "json") {
814
+ let jsonDataString = "";
815
+ jsonDataString +=
816
+ "with open(" +
817
+ repr(d.filename, osVars, imports, false, true) +
818
+ ") as f:\n";
819
+ jsonDataString += " json_data = json.load(f)\n";
820
+ imports.add("json");
821
+ return [jsonDataString, false];
822
+ }
823
+ return [null, false];
824
+ }
825
+ function getDataString(request, osVars, warnings) {
826
+ const imports = new Set();
827
+ if (!request.data || !request.dataArray) {
828
+ return [null, false, null, imports];
829
+ }
830
+ // There's 4 ways to pass data to Requests (in descending order of preference):
831
+ // a or dictionary/list as the json= argument
832
+ // a dictionary, or a list of tuples (if the dictionary would have duplicate keys) as the data= argument
833
+ // a string as data=
834
+ // bytes as data=
835
+ // We can pass json= if the data is valid JSON and we've specified json in the
836
+ // Content-Type header because passing json= will set that header.
837
+ //
838
+ // However, if there will be a mismatch between how the JSON is formatted
839
+ // we need to output a commented out version of the request with data= as well.
840
+ // This can happen when there's extra whitespace in the original data or
841
+ // because the JSON contains numbers that are too big to be stored in
842
+ // JavaScript or because there's objects with duplicate keys.
843
+ const contentType = request.headers.get("content-type");
844
+ let dataAsJson = null;
845
+ let jsonRoundtrips = false;
846
+ if (request.dataArray.length === 1 &&
847
+ contentType &&
848
+ contentType.split(";")[0].toString().trim() === "application/json") {
849
+ [dataAsJson, jsonRoundtrips] = formatDataAsJson(request.dataArray[0], imports, osVars);
850
+ }
851
+ if (jsonRoundtrips) {
852
+ return [null, false, dataAsJson, imports];
853
+ }
854
+ // data= can't be a dict or a list of tuples (i.e. entries) when
855
+ // there is a @file from --data, --data-binary or --json (because they can contain an '&' which would get escaped)
856
+ // there is a --data-urlencode without a name= or name@
857
+ // if you split the input on & and there's a value that doesn't contain an = (e.g. --data "foo=bar&" or simply --data "&")
858
+ // there is a name or value that doesn't roundtrip through percent encoding
859
+ const dataAsEntries = formatDataAsEntries(request.dataArray, osVars, imports);
860
+ if (dataAsEntries !== null) {
861
+ const [dataEntries, percentWarn] = dataAsEntries;
862
+ if (eq(request.headers.get("content-type"), "application/x-www-form-urlencoded") &&
863
+ request.headers.length === 1) {
864
+ // Requests adds the header when you include a body
865
+ // so if it's the only header, don't print the headers
866
+ request.headers.delete("content-type");
867
+ }
868
+ if (percentWarn) {
869
+ warnings.push([
870
+ "percent-encoded-spaces-in-data",
871
+ 'data contains spaces encoded by curl as "%20" which will be sent as "+" instead: ' +
872
+ JSON.stringify(percentWarn),
873
+ ]);
874
+ }
875
+ return [dataEntries, false, dataAsJson, imports];
876
+ }
877
+ const [dataAsString, shouldEncode] = formatDataAsStr(request.dataArray, imports, osVars);
878
+ return [dataAsString, shouldEncode, dataAsJson, imports];
879
+ }
880
+ function getFilesString(request, osVars, imports) {
881
+ let usesStdin = false;
882
+ if (!request.multipartUploads) {
883
+ return ["", usesStdin];
884
+ }
885
+ const multipartUploads = request.multipartUploads.map((m) => {
886
+ // https://github.com/psf/requests/blob/2d5517682b3b38547634d153cea43d48fbc8cdb5/requests/models.py#L117
887
+ //
888
+ // Requests's multipart syntax looks like this:
889
+ // name/filename: content
890
+ // name: open(filename/contentFile)
891
+ // name: (filename, open(contentFile)[, contentType[, headers]]))
892
+ const name = m.name ? repr(m.name, osVars, imports) : "None";
893
+ if (!("contentType" in m) && !("headers" in m) && !("encoder" in m)) {
894
+ if ("contentFile" in m &&
895
+ eq(m.contentFile, m.filename) &&
896
+ !eq(m.contentFile, "-")) {
897
+ return [
898
+ name,
899
+ "open(" +
900
+ repr(m.contentFile, osVars, imports, false, true) +
901
+ ", 'rb')",
902
+ ];
903
+ }
904
+ if ("content" in m && "filename" in m && eq(m.name, m.filename)) {
905
+ return [name, repr(m.content, osVars, imports)];
906
+ }
907
+ }
908
+ const sentFilename = "filename" in m && m.filename
909
+ ? repr(m.filename, osVars, imports)
910
+ : "None";
911
+ const tuple = [sentFilename];
912
+ if ("contentFile" in m) {
913
+ if (eq(m.contentFile, "-")) {
914
+ // TODO: use piped stdin if we have it
915
+ usesStdin = true;
916
+ tuple.push("sys.stdin.buffer.read())");
917
+ }
918
+ else {
919
+ tuple.push("open(" +
920
+ repr(m.contentFile, osVars, imports, false, true) +
921
+ ", 'rb')");
922
+ }
923
+ }
924
+ else {
925
+ tuple.push(repr(m.content, osVars, imports));
926
+ }
927
+ let addedContentType = false;
928
+ if ("contentType" in m && m.contentType) {
929
+ tuple.push(repr(m.contentType, osVars, imports));
930
+ addedContentType = true;
931
+ }
932
+ if (("headers" in m && m.headers) ||
933
+ ("headerFiles" in m && m.headerFiles)) {
934
+ if (!addedContentType) {
935
+ tuple.push("None");
936
+ }
937
+ const headerArg = [];
938
+ if (m.headers) {
939
+ let headerDict = "{";
940
+ for (const header of m.headers) {
941
+ // TODO: can have multiple headers in one headers= argument?
942
+ const [name, value] = header.split(":", 2);
943
+ headerDict +=
944
+ repr(name, osVars, imports) +
945
+ ": " +
946
+ repr(value.trimStart() || new Word(), osVars, imports) +
947
+ ", ";
948
+ }
949
+ if (headerDict.endsWith(", ")) {
950
+ headerDict = headerDict.slice(0, -2);
951
+ }
952
+ headerDict += "}";
953
+ headerArg.push(headerDict);
954
+ }
955
+ if (m.headerFiles && m.headerFiles.length) {
956
+ for (const headerFile of m.headerFiles) {
957
+ headerArg.push("read_headerfile(" +
958
+ repr(headerFile, osVars, imports, false, true) +
959
+ ")");
960
+ }
961
+ imports.add("read_headerfile");
962
+ }
963
+ tuple.push(headerArg.join(" | "));
964
+ }
965
+ return [name, "(" + tuple.join(", ") + ")"];
966
+ });
967
+ const multipartUploadsAsDict = Object.fromEntries(multipartUploads);
968
+ let filesString = "files = ";
969
+ if (Object.keys(multipartUploadsAsDict).length === multipartUploads.length) {
970
+ filesString += "{\n";
971
+ for (const [multipartKey, multipartValue] of multipartUploads) {
972
+ filesString += " " + multipartKey + ": " + multipartValue + ",\n";
973
+ }
974
+ filesString += "}\n";
975
+ }
976
+ else {
977
+ filesString += "[\n";
978
+ for (const [multipartKey, multipartValue] of multipartUploads) {
979
+ filesString += " (" + multipartKey + ", " + multipartValue + "),\n";
980
+ }
981
+ filesString += "]\n";
982
+ }
983
+ return [filesString, usesStdin];
984
+ }
985
+ // Don't add indent/comment characters to empty lines, most importantly the last line
986
+ // which will be empty when there's a trailing newline.
987
+ function indent(s, level) {
988
+ if (level === 0) {
989
+ return s;
990
+ }
991
+ const begin = " ".repeat(level);
992
+ return s
993
+ .split("\n")
994
+ .map((l) => (l.trim() ? begin + l : l))
995
+ .join("\n");
996
+ }
997
+ function commentOut(s) {
998
+ return s
999
+ .split("\n")
1000
+ .map((l) => (l.trim() ? "#" + l : l))
1001
+ .join("\n");
1002
+ }
1003
+ function uniqueWarn(seenWarnings, warnings, warning) {
1004
+ if (!seenWarnings.has(warning[0])) {
1005
+ seenWarnings.add(warning[0]);
1006
+ warnings.push(warning);
1007
+ }
1008
+ }
1009
+ function joinArgs(args) {
1010
+ let s = "(";
1011
+ if (args.join("").length < 100) {
1012
+ s += args.join(", ");
1013
+ }
1014
+ else {
1015
+ s += "\n";
1016
+ for (const arg of args) {
1017
+ s += " " + arg + ",\n";
1018
+ }
1019
+ }
1020
+ return s + ")";
1021
+ }
1022
+ function requestToPython(request, warnings = [], imports, thirdPartyImports) {
1023
+ const osVars = {};
1024
+ const commentedOutHeaders = {
1025
+ // TODO: add a warning why this should be commented out?
1026
+ "accept-encoding": "",
1027
+ "content-length": "",
1028
+ };
1029
+ // https://github.com/icing/blog/blob/main/curl_on_a_weekend.md
1030
+ if (eq(request.headers.get("te"), "trailers")) {
1031
+ commentedOutHeaders.te = "Requests doesn't support trailers";
1032
+ }
1033
+ let cookieStr;
1034
+ let cookieFile = null;
1035
+ if (request.cookies) {
1036
+ // TODO: handle duplicate cookie names
1037
+ cookieStr = "cookies = {\n";
1038
+ for (const [cookieName, cookieValue] of request.cookies) {
1039
+ cookieStr +=
1040
+ " " +
1041
+ repr(cookieName, osVars, imports) +
1042
+ ": " +
1043
+ repr(cookieValue, osVars, imports) +
1044
+ ",\n";
1045
+ }
1046
+ cookieStr += "}\n";
1047
+ // Before Python 3.11, cookies= was sorted alphabetically
1048
+ // https://github.com/python/cpython/issues/86232
1049
+ commentedOutHeaders.cookie = "";
1050
+ if (request.cookieFiles) {
1051
+ warnings.push([
1052
+ "cookie-files",
1053
+ "passing both cookies and cookie files with --cookie/-b is not supported",
1054
+ ]);
1055
+ }
1056
+ if (request.cookieJar) {
1057
+ warnings.push([
1058
+ "cookie-files",
1059
+ "passing both cookies and --cookie-jar/-c is not supported",
1060
+ ]);
1061
+ }
1062
+ }
1063
+ else if ((request.cookieFiles && request.cookieFiles.length) ||
1064
+ request.cookieJar) {
1065
+ imports.add("http.cookiejar.MozillaCookieJar");
1066
+ if (request.cookieFiles && request.cookieFiles.length) {
1067
+ // TODO: what if user passes multiple cookie files?
1068
+ // TODO: what if user passes cookies and cookie files?
1069
+ cookieFile = request.cookieFiles[request.cookieFiles.length - 1];
1070
+ if (request.cookieFiles.length > 1) {
1071
+ warnings.push([
1072
+ "cookie-files",
1073
+ // TODO: curl reads all of them.
1074
+ "multiple cookie files are not supported, using the last one: " +
1075
+ JSON.stringify(cookieFile.toString()),
1076
+ ]);
1077
+ }
1078
+ // TODO: do we need to .load()?
1079
+ cookieStr =
1080
+ "cookies = MozillaCookieJar(" +
1081
+ repr(cookieFile, osVars, imports) +
1082
+ ")\n";
1083
+ }
1084
+ else if (request.cookieJar) {
1085
+ cookieStr = "cookies = MozillaCookieJar()\n";
1086
+ }
1087
+ }
1088
+ let proxyDict;
1089
+ if (request.proxy) {
1090
+ let proxy = request.proxy.includes("://")
1091
+ ? request.proxy
1092
+ : request.proxy.prepend("http://");
1093
+ const protocol = proxy.split("://")[0].toLowerCase();
1094
+ if (eq(protocol, "socks")) {
1095
+ // https://github.com/curl/curl/blob/curl-7_86_0/lib/url.c#L2418-L2419
1096
+ proxy = proxy.replace("socks", "socks4");
1097
+ }
1098
+ proxyDict = "proxies = {\n";
1099
+ proxyDict += " 'http': " + repr(proxy, osVars, imports) + ",\n";
1100
+ // TODO: if (protocol !== "http") { ?
1101
+ proxyDict += " 'https': " + repr(proxy, osVars, imports) + ",\n";
1102
+ proxyDict += "}\n";
1103
+ }
1104
+ let certStr;
1105
+ if (request.cert || request.key) {
1106
+ certStr = "cert = ";
1107
+ let certPart = "None";
1108
+ if (request.cert) {
1109
+ const [cert, password] = request.cert;
1110
+ certPart = repr(cert, osVars, imports);
1111
+ if (password) {
1112
+ warnings.push([
1113
+ "cert-password",
1114
+ "passing a password to --cert is not supported: " +
1115
+ JSON.stringify(password.toString()),
1116
+ ]);
1117
+ }
1118
+ }
1119
+ if (request.key) {
1120
+ certStr +=
1121
+ "(" + certPart + ", " + repr(request.key, osVars, imports) + ")";
1122
+ }
1123
+ else {
1124
+ certStr += certPart;
1125
+ }
1126
+ certStr += "\n";
1127
+ }
1128
+ // if there's only 1 URL, put params all together here, unless it's just one string.
1129
+ // if there's more than 1, if we have params that are added to each URL from
1130
+ // --get --data or --url-query that need to read a file, put just the shared part
1131
+ // here, then keep the query in the URL, in the URL.
1132
+ // If there's no --get --data or --url-query, then
1133
+ // put params (if it can be rendered as a list or dict) right before the requests line
1134
+ // Otherwise, keep the query in the URL.
1135
+ let paramsStr;
1136
+ let shouldEncodeParams; // TODO: necessary?
1137
+ function readsFile(paramArray) {
1138
+ return paramArray.some((p) => !(p instanceof Word));
1139
+ }
1140
+ const paramArray = request.urls.length === 1 ? request.urls[0].queryArray : request.queryArray;
1141
+ if (paramArray &&
1142
+ (request.urls.length === 1 ||
1143
+ (request.urls.length > 1 && readsFile(paramArray)))) {
1144
+ const queryAsEntries = formatDataAsEntries(paramArray, osVars, imports, "params");
1145
+ if (queryAsEntries !== null) {
1146
+ let percentWarn;
1147
+ [paramsStr, percentWarn] = queryAsEntries;
1148
+ if (percentWarn) {
1149
+ warnings.push([
1150
+ "percent-encoded-spaces-in-query",
1151
+ // TODO: will they?
1152
+ 'URL querystring contains spaces encoded as "%20" which will be sent by Requests as "+" instead: ' +
1153
+ JSON.stringify(percentWarn),
1154
+ ]);
1155
+ }
1156
+ }
1157
+ else if (readsFile(paramArray)) {
1158
+ [paramsStr, shouldEncodeParams] = formatDataAsStr(paramArray, imports, osVars, "params");
1159
+ }
1160
+ }
1161
+ const contentType = request.headers.get("content-type");
1162
+ let dataString;
1163
+ let jsonDataString;
1164
+ let filesString;
1165
+ let shouldEncode;
1166
+ if (request.urls[0].uploadFile && request.urls.length === 1) {
1167
+ // TODO: https://docs.python-requests.org/en/latest/user/advanced/#streaming-uploads
1168
+ if (eq(request.urls[0].uploadFile, "-") ||
1169
+ eq(request.urls[0].uploadFile, ".")) {
1170
+ dataString = "data = sys.stdin.buffer.read()\n";
1171
+ imports.add("sys");
1172
+ }
1173
+ else {
1174
+ dataString =
1175
+ "with open(" +
1176
+ repr(request.urls[0].uploadFile, osVars, imports, false, true) +
1177
+ ", 'rb') as f:\n";
1178
+ dataString += " data = f.read()\n";
1179
+ }
1180
+ }
1181
+ else if (request.multipartUploads) {
1182
+ let usesStdin = false;
1183
+ [filesString, usesStdin] = getFilesString(request, osVars, imports);
1184
+ if (usesStdin) {
1185
+ imports.add("sys");
1186
+ }
1187
+ // If you pass files= then Requests adds this header and a `boundary`
1188
+ // If you manually pass a Content-Type header it won't set a `boundary`
1189
+ // wheras curl does, so the request will fail.
1190
+ // https://github.com/curlconverter/curlconverter/issues/248
1191
+ if (filesString &&
1192
+ contentType &&
1193
+ eq(contentType.trim(), "multipart/form-data") &&
1194
+ !contentType.includes("boundary=")) {
1195
+ // TODO: better wording
1196
+ commentedOutHeaders["content-type"] =
1197
+ "requests won't add a boundary if this header is set when you pass files=";
1198
+ }
1199
+ }
1200
+ else if (request.data && !request.data.isEmpty()) {
1201
+ // !isEmpty() because passing data='' is the same as not passing data=
1202
+ // We need to set the Content-Type header in headers= and not set data=
1203
+ let dataImports;
1204
+ [dataString, shouldEncode, jsonDataString, dataImports] = getDataString(request, osVars, warnings);
1205
+ dataImports.forEach(imports.add, imports);
1206
+ // Remove "Content-Type" from the headers dict
1207
+ // because Requests adds it automatically when you use json=
1208
+ if (jsonDataString &&
1209
+ !dataString &&
1210
+ contentType &&
1211
+ eq(contentType.trim(), "application/json")) {
1212
+ commentedOutHeaders["content-type"] = "Already added when you pass json=";
1213
+ }
1214
+ }
1215
+ let headerDict;
1216
+ if (request.headers.length) {
1217
+ headerDict = formatHeaders(request.headers, commentedOutHeaders, osVars, imports);
1218
+ }
1219
+ let pythonCode = "";
1220
+ if (proxyDict) {
1221
+ pythonCode += proxyDict + "\n";
1222
+ }
1223
+ if (cookieStr) {
1224
+ pythonCode += cookieStr + "\n";
1225
+ }
1226
+ if (headerDict) {
1227
+ pythonCode += headerDict + "\n";
1228
+ }
1229
+ if (paramsStr) {
1230
+ pythonCode += paramsStr + "\n";
1231
+ }
1232
+ if (certStr) {
1233
+ pythonCode += certStr + "\n";
1234
+ }
1235
+ if (jsonDataString) {
1236
+ pythonCode += jsonDataString + "\n";
1237
+ }
1238
+ else if (dataString) {
1239
+ pythonCode += dataString + "\n";
1240
+ }
1241
+ else if (filesString) {
1242
+ pythonCode += filesString + "\n";
1243
+ }
1244
+ // By default, curl doesn't follow redirects and Requests does.
1245
+ // Unless redirect behavior has been explicitly set with -L/--location/--no-location
1246
+ // or --max-redirs 0 we pretend generate code that follows redirects,
1247
+ // because adding allow_redirects=False to almost every command would be ugly
1248
+ // and it only matters when the server responds with a redirect, which isn't
1249
+ // that common.
1250
+ let followRedirects = request.followRedirects;
1251
+ let maxRedirects = undefined;
1252
+ if (request.maxRedirects !== undefined) {
1253
+ maxRedirects = asInt(request.maxRedirects, osVars, imports);
1254
+ }
1255
+ if (followRedirects === undefined) {
1256
+ followRedirects = true;
1257
+ // Users would see this warning for most commands
1258
+ // warnings.push([
1259
+ // "--location",
1260
+ // "Requests defaults to following redirects, curl doesn't",
1261
+ // ]);
1262
+ }
1263
+ const hasMaxRedirects = followRedirects &&
1264
+ maxRedirects &&
1265
+ maxRedirects !== "0" &&
1266
+ maxRedirects !== "30"; // Requests default
1267
+ // Things that vary per-url:
1268
+ // method (because --upload-file can make it PUT)
1269
+ // data= (because of --upload-file)
1270
+ // --output file
1271
+ // params= (because of the query string)
1272
+ // auth= (because the URL can have an auth string)
1273
+ const seenWarnings = new Set();
1274
+ const requestLines = [];
1275
+ let extraEmptyLine = false;
1276
+ for (const [urlObjIndex, urlObj] of request.urls.entries()) {
1277
+ const requestsMethods = [
1278
+ "GET",
1279
+ "HEAD",
1280
+ "POST",
1281
+ "PATCH",
1282
+ "PUT",
1283
+ "DELETE",
1284
+ "OPTIONS", // undocumented
1285
+ ];
1286
+ let fn;
1287
+ const args = [];
1288
+ const methodAsString = urlObj.method.toString();
1289
+ if (urlObj.method.isString() && requestsMethods.includes(methodAsString)) {
1290
+ fn = methodAsString.toLowerCase();
1291
+ }
1292
+ else {
1293
+ fn = "request";
1294
+ args.push(repr(urlObj.method, osVars, imports));
1295
+ if (urlObj.method.isString() &&
1296
+ methodAsString !== methodAsString.toUpperCase()) {
1297
+ warnings.push([
1298
+ "method",
1299
+ "Requests will uppercase the HTTP method: " +
1300
+ JSON.stringify(methodAsString),
1301
+ ]);
1302
+ }
1303
+ }
1304
+ let urlParamsStr;
1305
+ let url = urlObj.url;
1306
+ if (request.urls.length === 1) {
1307
+ if (paramsStr) {
1308
+ url = urlObj.urlWithoutQueryArray;
1309
+ }
1310
+ else {
1311
+ url = urlObj.url;
1312
+ }
1313
+ }
1314
+ else {
1315
+ if (paramsStr) {
1316
+ url = urlObj.urlWithOriginalQuery;
1317
+ }
1318
+ else {
1319
+ if (urlObj.queryArray && urlObj.queryArray.length > 0) {
1320
+ const urlQueryAsEntries = formatDataAsEntries(urlObj.queryArray, osVars, imports, "params");
1321
+ if (urlQueryAsEntries !== null) {
1322
+ let percentWarn;
1323
+ [urlParamsStr, percentWarn] = urlQueryAsEntries;
1324
+ url = urlObj.urlWithoutQueryArray;
1325
+ if (percentWarn) {
1326
+ warnings.push([
1327
+ "percent-encoded-spaces-in-query",
1328
+ // TODO: will they?
1329
+ 'query contains spaces encoded by curl as "%20" which will be sent as "+" instead: ' +
1330
+ JSON.stringify(percentWarn),
1331
+ ]);
1332
+ }
1333
+ }
1334
+ else if (readsFile(urlObj.queryArray)) {
1335
+ [urlParamsStr, shouldEncodeParams] = formatDataAsStr(urlObj.queryArray, imports, osVars, "params");
1336
+ url = urlObj.urlWithoutQueryArray;
1337
+ }
1338
+ }
1339
+ // url = urlObj.url
1340
+ }
1341
+ }
1342
+ args.push(repr(url, osVars, imports));
1343
+ if (paramsStr || urlParamsStr) {
1344
+ args.push("params=params" + (shouldEncodeParams ? ".encode()" : ""));
1345
+ }
1346
+ if (cookieStr && !request.cookieJar) {
1347
+ args.push("cookies=cookies");
1348
+ }
1349
+ if (headerDict) {
1350
+ args.push("headers=headers");
1351
+ }
1352
+ if (urlObj.uploadFile) {
1353
+ if (request.urls.length > 1) {
1354
+ // If there's more than one URL we could have --data for all
1355
+ // of them and --upload-file for just one of them and we can't
1356
+ // overwrite the `data` variable in that case.
1357
+ args.push("data=file_contents");
1358
+ }
1359
+ else {
1360
+ args.push("data=data");
1361
+ }
1362
+ }
1363
+ else if (filesString) {
1364
+ args.push("files=files");
1365
+ }
1366
+ else if (request.data && !request.data.isEmpty()) {
1367
+ if (jsonDataString) {
1368
+ args.push("json=json_data");
1369
+ }
1370
+ else {
1371
+ args.push("data=data" + (shouldEncode ? ".encode()" : ""));
1372
+ }
1373
+ }
1374
+ if (proxyDict) {
1375
+ args.push("proxies=proxies");
1376
+ }
1377
+ if (certStr) {
1378
+ args.push("cert=cert");
1379
+ }
1380
+ const certOrPath = request.cacert || request.capath;
1381
+ if (request.insecure) {
1382
+ args.push("verify=False");
1383
+ }
1384
+ else if (certOrPath) {
1385
+ args.push("verify=" + repr(certOrPath, osVars, imports));
1386
+ }
1387
+ // TODO: does this header check apply to all auth methods?
1388
+ if (urlObj.auth && !request.headers.has("Authorization")) {
1389
+ const [user, password] = urlObj.auth;
1390
+ let auth = "(" +
1391
+ repr(user, osVars, imports) +
1392
+ ", " +
1393
+ repr(password, osVars, imports) +
1394
+ ")";
1395
+ switch (request.authType) {
1396
+ case "basic":
1397
+ break;
1398
+ case "digest":
1399
+ thirdPartyImports.add("requests.auth.HTTPDigestAuth");
1400
+ auth = "HTTPDigestAuth" + auth;
1401
+ break;
1402
+ case "ntlm":
1403
+ case "ntlm-wb":
1404
+ thirdPartyImports.add("requests_ntlm.HttpNtlmAuth");
1405
+ auth = "HttpNtlmAuth" + auth;
1406
+ // TODO: this could stop being true
1407
+ uniqueWarn(seenWarnings, warnings, [
1408
+ "ntlm",
1409
+ "requests-ntlm is unmaintained",
1410
+ ]);
1411
+ break;
1412
+ case "negotiate":
1413
+ thirdPartyImports.add("requests_gssapi.HTTPSPNEGOAuth");
1414
+ auth = "HTTPSPNEGOAuth(";
1415
+ if (request.delegation) {
1416
+ if (eq(request.delegation, "always")) {
1417
+ auth += "delegate=True";
1418
+ }
1419
+ else if (eq(request.delegation, "none")) {
1420
+ auth += "delegate=False";
1421
+ }
1422
+ else {
1423
+ uniqueWarn(seenWarnings, warnings, [
1424
+ "delegation",
1425
+ "--delegation value not supported: " +
1426
+ JSON.stringify(request.delegation.toString()),
1427
+ ]);
1428
+ }
1429
+ }
1430
+ auth += ")";
1431
+ // TODO: use requests-kerberos instead?
1432
+ // https://star-history.com/#pythongssapi/requests-gssapi&requests/requests-kerberos
1433
+ uniqueWarn(seenWarnings, warnings, [
1434
+ "negotiate",
1435
+ "requests-gssapi is a fork of requests-kerberos",
1436
+ ]);
1437
+ break;
1438
+ case "aws-sigv4":
1439
+ thirdPartyImports.add("aws_requests_auth.aws_auth.AWSRequestsAuth");
1440
+ // TODO: move this "auth = " to separate line
1441
+ // TODO: try this.
1442
+ auth =
1443
+ "AWSRequestsAuth(aws_access_key=" +
1444
+ repr(user, osVars, imports) +
1445
+ ", aws_secret_access_key=" +
1446
+ repr(password, osVars, imports) +
1447
+ // TODO: parse
1448
+ ", aws_host=" +
1449
+ repr(request.awsSigV4 || new Word(), osVars, imports) +
1450
+ ", aws_region=" +
1451
+ repr(request.awsSigV4 || new Word(), osVars, imports) +
1452
+ ", aws_service=" +
1453
+ repr(request.awsSigV4 || new Word(), osVars, imports) +
1454
+ ")";
1455
+ uniqueWarn(seenWarnings, warnings, [
1456
+ "--aws-sigv4",
1457
+ "--aws-sigv4 value isn't parsed: " +
1458
+ JSON.stringify(request.awsSigV4 ? request.awsSigV4.toString() : ""),
1459
+ ]);
1460
+ break;
1461
+ case "bearer":
1462
+ // Shouldn't happen because hasHeader(Authorization) should be true
1463
+ // TODO: use requests-oauthlib
1464
+ break;
1465
+ }
1466
+ args.push("auth=" + auth);
1467
+ }
1468
+ if (request.timeout || request.connectTimeout) {
1469
+ let connectTimeout = null;
1470
+ if (request.connectTimeout) {
1471
+ connectTimeout = asFloat(request.connectTimeout, osVars, imports);
1472
+ }
1473
+ let timeout = null;
1474
+ if (request.timeout) {
1475
+ if (request.timeout.isString()) {
1476
+ timeout = request.timeout.toString();
1477
+ }
1478
+ else {
1479
+ timeout =
1480
+ "float(" +
1481
+ repr(request.timeout, osVars, imports, false, true) +
1482
+ ")";
1483
+ }
1484
+ }
1485
+ if (timeout && connectTimeout && timeout !== connectTimeout) {
1486
+ args.push("timeout=(" + connectTimeout + ", " + timeout + ")");
1487
+ }
1488
+ else if (timeout) {
1489
+ args.push("timeout=" + timeout);
1490
+ }
1491
+ else if (connectTimeout) {
1492
+ args.push("timeout=(" + connectTimeout + ", None)");
1493
+ }
1494
+ if (timeout) {
1495
+ uniqueWarn(seenWarnings, warnings, [
1496
+ "--max-time",
1497
+ // https://requests.readthedocs.io/en/latest/user/advanced/#timeouts
1498
+ "unlike --max-time, Requests doesn't have a timeout for the whole request, only for the connect and the read",
1499
+ ]);
1500
+ }
1501
+ }
1502
+ if (!followRedirects || maxRedirects === "0") {
1503
+ args.push("allow_redirects=False");
1504
+ }
1505
+ else if (maxRedirects) {
1506
+ if (maxRedirects === "-1") {
1507
+ imports.add("math");
1508
+ maxRedirects = "math.inf";
1509
+ }
1510
+ }
1511
+ if (followRedirects && request.followRedirectsTrusted) {
1512
+ uniqueWarn(seenWarnings, warnings, [
1513
+ "--location-trusted",
1514
+ "Requests doesn't have an easy way to disable removing the Authorization: header on redirect",
1515
+ ]);
1516
+ }
1517
+ let requestLine = "";
1518
+ const isSession = hasMaxRedirects || request.cookieJar;
1519
+ const indentLevel = isSession ? 1 : 0;
1520
+ if (isSession && urlObjIndex === 0) {
1521
+ requestLine += "with requests.Session() as session:\n";
1522
+ if (hasMaxRedirects) {
1523
+ requestLine += ` session.max_redirects = ${maxRedirects}\n`;
1524
+ }
1525
+ if (request.cookieJar) {
1526
+ requestLine += ` session.cookies = cookies\n`;
1527
+ }
1528
+ }
1529
+ if (request.urls.length > 1) {
1530
+ if (urlParamsStr) {
1531
+ requestLine += indent(urlParamsStr, indentLevel);
1532
+ }
1533
+ if (urlObj.uploadFile) {
1534
+ let uploadFileLine = "";
1535
+ // TODO: https://docs.python-requests.org/en/latest/user/advanced/#streaming-uploads
1536
+ if (eq(urlObj.uploadFile, "-") || eq(urlObj.uploadFile, ".")) {
1537
+ uploadFileLine += "file_contents = sys.stdin.buffer.read()\n";
1538
+ imports.add("sys");
1539
+ }
1540
+ else {
1541
+ uploadFileLine +=
1542
+ "with open(" +
1543
+ repr(urlObj.uploadFile, osVars, imports, false, true) +
1544
+ ", 'rb') as f:\n";
1545
+ uploadFileLine += " file_contents = f.read()\n";
1546
+ }
1547
+ requestLine += indent(uploadFileLine, indentLevel);
1548
+ }
1549
+ }
1550
+ const fnToCall = "response = " + (isSession ? "session" : "requests") + "." + fn;
1551
+ requestLine += indent(fnToCall + joinArgs(args) + "\n", indentLevel);
1552
+ if (jsonDataString && dataString && !urlObj.uploadFile) {
1553
+ // Adding empty lines to a "with" block breaks the code when pasted in the REPL
1554
+ requestLine += isSession || request.urls.length > 1 ? "" : "\n";
1555
+ // Should never be -1
1556
+ args[args.indexOf("json=json_data")] = shouldEncode
1557
+ ? "data=data.encode()"
1558
+ : "data=data";
1559
+ let dataAlternative = "# Note: json_data will not be serialized by requests\n" +
1560
+ "# exactly as it was in the original request.\n";
1561
+ dataAlternative += commentOut(dataString);
1562
+ dataAlternative += commentOut(fnToCall + joinArgs(args) + "\n");
1563
+ requestLine += indent(dataAlternative, indentLevel);
1564
+ }
1565
+ if (urlObj.output && !eq(urlObj.output, "/dev/null")) {
1566
+ let outputLine = "";
1567
+ if (eq(request.urls[0].output, "-")) {
1568
+ outputLine += "print(response.text)\n";
1569
+ }
1570
+ else {
1571
+ outputLine += isSession || request.urls.length > 1 ? "" : "\n";
1572
+ outputLine +=
1573
+ "with open(" +
1574
+ repr(urlObj.output, osVars, imports, false, true) +
1575
+ ", 'wb') as f:\n";
1576
+ outputLine += " f.write(response.content)\n";
1577
+ }
1578
+ requestLine += indent(outputLine, indentLevel);
1579
+ }
1580
+ if (!isSession &&
1581
+ // request.urls.length > 1 &&
1582
+ (urlParamsStr ||
1583
+ (dataString && jsonDataString) ||
1584
+ urlObj.uploadFile ||
1585
+ (urlObj.output && !eq(urlObj.output, "/dev/null")))) {
1586
+ extraEmptyLine = true;
1587
+ }
1588
+ requestLines.push(requestLine);
1589
+ }
1590
+ pythonCode += requestLines.join(extraEmptyLine ? "\n" : "");
1591
+ if (request.cookieJar) {
1592
+ let cookieSaveLine = "cookies.save(";
1593
+ if (!eq(request.cookieJar, cookieFile)) {
1594
+ cookieSaveLine += repr(request.cookieJar, osVars, imports) + ", ";
1595
+ }
1596
+ cookieSaveLine += "ignore_discard=True, ignore_expires=True)\n"; // TODO: necessary?
1597
+ pythonCode += indent(cookieSaveLine, 1);
1598
+ }
1599
+ if (request.http3) {
1600
+ warnings.push([
1601
+ "http3",
1602
+ "this was an HTTP/3 request but requests only supports HTTP/1.1",
1603
+ ]);
1604
+ }
1605
+ else if (request.http2) {
1606
+ warnings.push([
1607
+ "http2",
1608
+ "this was an HTTP/2 request but requests only supports HTTP/1.1",
1609
+ ]);
1610
+ }
1611
+ else if (request.headers.lowercase && request.headers.length > 1) {
1612
+ warnings.push([
1613
+ "--header",
1614
+ "all the --header/-H names are lowercase, which means this may have been an HTTP/2 or HTTP/3 request. Requests only sends HTTP/1.1",
1615
+ ]);
1616
+ }
1617
+ // It would be more correct to run the commands whenever we need
1618
+ // their values instead of running them all at the beginning.
1619
+ let variableCode = "";
1620
+ if (Object.keys(osVars).length) {
1621
+ for (const [varName, expr] of Object.entries(osVars)) {
1622
+ variableCode += `${varName} = ${expr}\n`;
1623
+ }
1624
+ variableCode += "\n";
1625
+ }
1626
+ return variableCode + pythonCode;
1627
+ }
1628
+ export function printImports(imps) {
1629
+ let s = "";
1630
+ for (const imp of Array.from(imps).sort()) {
1631
+ if (imp === "read_headerfile") {
1632
+ continue;
1633
+ }
1634
+ if (imp.includes(".")) {
1635
+ const pos = imp.lastIndexOf(".");
1636
+ const module = imp.slice(0, pos);
1637
+ const name = imp.slice(pos + 1);
1638
+ s += "from " + module + " import " + name + "\n";
1639
+ }
1640
+ else {
1641
+ s += "import " + imp + "\n";
1642
+ }
1643
+ }
1644
+ return s;
1645
+ }
1646
+ export function _toPython(requests, warnings = []) {
1647
+ const code = [];
1648
+ let joinTwoLines = false;
1649
+ const imports = new Set();
1650
+ const thirdPartyImports = new Set();
1651
+ for (const request of requests) {
1652
+ const requestCode = requestToPython(request, warnings, imports, thirdPartyImports);
1653
+ code.push(requestCode);
1654
+ // If one of the requests defines variables (or its URL is very long),
1655
+ // separate all the configs with two empty lines.
1656
+ // curl example.com --next example.com
1657
+ // vs.
1658
+ // curl --data "foo=bar" example.com --next example.com
1659
+ // (+1 for the trailing newline)
1660
+ joinTwoLines || (joinTwoLines = requestCode.split("\n").length > request.urls.length + 1);
1661
+ }
1662
+ let importCode = "";
1663
+ importCode += printImports(imports);
1664
+ if (imports.size > 1) {
1665
+ importCode += "\n";
1666
+ }
1667
+ importCode += "import requests\n";
1668
+ importCode += printImports(thirdPartyImports);
1669
+ importCode += "\n";
1670
+ if (imports.has("read_headerfile")) {
1671
+ importCode += "def read_headerfile(filename):\n";
1672
+ importCode += " headers = {}\n";
1673
+ importCode += " with open(filename) as f:\n";
1674
+ importCode += " for line in f:\n";
1675
+ importCode += " line = line.split('#', 1)[0].strip()\n";
1676
+ importCode += " if line:\n";
1677
+ importCode += " [key, value] = line.split(':', 1)\n";
1678
+ importCode += " value = value or ''\n";
1679
+ importCode += " headers[key.strip()] = value.strip()\n";
1680
+ importCode += " return headers\n";
1681
+ importCode += "\n";
1682
+ }
1683
+ return importCode + code.join(joinTwoLines ? "\n\n" : "\n");
1684
+ }
1685
+ export function toPythonWarn(curlCommand, warnings = []) {
1686
+ const requests = parse(curlCommand, supportedArgs, warnings);
1687
+ const python = _toPython(requests, warnings);
1688
+ return [python, warnings];
1689
+ }
1690
+ export function toPython(curlCommand) {
1691
+ return toPythonWarn(curlCommand)[0];
1692
+ }
1693
+ //# sourceMappingURL=python.js.map