@lovrozagar/honey 0.6.8 → 0.7.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 (674) hide show
  1. package/README.md +514 -192
  2. package/bin/honey.js +25 -0
  3. package/dist/accepts.d.ts +7 -1
  4. package/dist/accepts.d.ts.map +1 -1
  5. package/dist/accepts.js +126 -0
  6. package/dist/accepts.js.map +1 -0
  7. package/dist/body-kind.d.ts +26 -0
  8. package/dist/body-kind.d.ts.map +1 -0
  9. package/dist/body-kind.js +70 -0
  10. package/dist/body-kind.js.map +1 -0
  11. package/dist/body-limit.d.ts +5 -1
  12. package/dist/body-limit.d.ts.map +1 -1
  13. package/dist/body-limit.js +126 -0
  14. package/dist/body-limit.js.map +1 -0
  15. package/dist/build/index.d.ts +19 -3
  16. package/dist/build/index.d.ts.map +1 -1
  17. package/dist/build/index.js +173 -0
  18. package/dist/build/index.js.map +1 -0
  19. package/dist/cli-args.d.ts +33 -0
  20. package/dist/cli-args.d.ts.map +1 -0
  21. package/dist/cli-args.js +102 -0
  22. package/dist/cli-args.js.map +1 -0
  23. package/dist/cli-go/auth.go +26 -0
  24. package/dist/cli-go/config.go +192 -0
  25. package/dist/cli-go/errors.go +75 -0
  26. package/dist/cli-go/multipart.go +79 -0
  27. package/dist/cli-go/output.go +163 -0
  28. package/dist/cli-go/root.go +30 -0
  29. package/dist/cli-go/stream.go +88 -0
  30. package/dist/cli-go/version.go +5 -0
  31. package/dist/cli.d.ts +1 -1
  32. package/dist/cli.js +209 -0
  33. package/dist/cli.js.map +1 -0
  34. package/dist/client/defaults.js +48 -0
  35. package/dist/client/defaults.js.map +1 -0
  36. package/dist/client/error.d.ts +35 -0
  37. package/dist/client/error.d.ts.map +1 -1
  38. package/dist/client/error.js +71 -0
  39. package/dist/client/error.js.map +1 -0
  40. package/dist/client/http.d.ts +67 -8
  41. package/dist/client/http.d.ts.map +1 -1
  42. package/dist/client/http.js +571 -0
  43. package/dist/client/http.js.map +1 -0
  44. package/dist/client/index.d.ts +3 -2
  45. package/dist/client/index.d.ts.map +1 -1
  46. package/dist/client/index.js +95 -0
  47. package/dist/client/index.js.map +1 -0
  48. package/dist/client/path.d.ts +26 -0
  49. package/dist/client/path.d.ts.map +1 -0
  50. package/dist/client/path.js +163 -0
  51. package/dist/client/path.js.map +1 -0
  52. package/dist/client/redirect-policy.d.ts +12 -0
  53. package/dist/client/redirect-policy.d.ts.map +1 -0
  54. package/dist/client/redirect-policy.js +20 -0
  55. package/dist/client/redirect-policy.js.map +1 -0
  56. package/dist/client/sdk.d.ts +36 -0
  57. package/dist/client/sdk.d.ts.map +1 -1
  58. package/dist/client/sdk.js +292 -0
  59. package/dist/client/sdk.js.map +1 -0
  60. package/dist/client/sse.d.ts +13 -0
  61. package/dist/client/sse.d.ts.map +1 -1
  62. package/dist/client/sse.js +129 -0
  63. package/dist/client/sse.js.map +1 -0
  64. package/dist/client/types.js +2 -0
  65. package/dist/client/types.js.map +1 -0
  66. package/dist/client/ws.d.ts +9 -2
  67. package/dist/client/ws.d.ts.map +1 -1
  68. package/dist/client/ws.js +112 -0
  69. package/dist/client/ws.js.map +1 -0
  70. package/dist/client-go/errors.go +236 -0
  71. package/dist/client-go/export.go +44 -0
  72. package/dist/client-go/invalidation.go +371 -0
  73. package/dist/client-go/nullable.go +62 -0
  74. package/dist/client-go/realtime.go +354 -0
  75. package/dist/client-go/result.go +20 -0
  76. package/dist/client-go/runtime.go +975 -0
  77. package/dist/client-go/sse.go +159 -0
  78. package/dist/client-go/transport.go +495 -0
  79. package/dist/client-go/ws.go +128 -0
  80. package/dist/client-info.d.ts +36 -0
  81. package/dist/client-info.d.ts.map +1 -0
  82. package/dist/client-info.js +53 -0
  83. package/dist/client-info.js.map +1 -0
  84. package/dist/client-mcp/README.md +10 -0
  85. package/dist/client-mcp/server.ts +221 -0
  86. package/dist/client-mcp/types.ts +19 -0
  87. package/dist/client-python/_errors.py +115 -0
  88. package/dist/client-python/_invalidation.py +494 -0
  89. package/dist/client-python/_realtime.py +179 -0
  90. package/dist/client-python/_runtime.py +860 -0
  91. package/dist/client-python/_sse.py +155 -0
  92. package/dist/client-python/_transport.py +366 -0
  93. package/dist/client-python/_ws.py +159 -0
  94. package/dist/client-python/py.typed +0 -0
  95. package/dist/client-rust/errors.rs +220 -0
  96. package/dist/client-rust/invalidation.rs +412 -0
  97. package/dist/client-rust/invalidation_sync.rs +93 -0
  98. package/dist/client-rust/nullable.rs +17 -0
  99. package/dist/client-rust/realtime.rs +702 -0
  100. package/dist/client-rust/result.rs +22 -0
  101. package/dist/client-rust/runtime.rs +980 -0
  102. package/dist/client-rust/runtime_sync.rs +484 -0
  103. package/dist/client-rust/sse.rs +188 -0
  104. package/dist/client-rust/ws.rs +167 -0
  105. package/dist/codegen-go-cli.d.ts.map +1 -1
  106. package/dist/codegen-go-cli.js +964 -0
  107. package/dist/codegen-go-cli.js.map +1 -0
  108. package/dist/codegen-go.d.ts +15 -3
  109. package/dist/codegen-go.d.ts.map +1 -1
  110. package/dist/codegen-go.js +872 -0
  111. package/dist/codegen-go.js.map +1 -0
  112. package/dist/codegen-hash.js +9 -0
  113. package/dist/codegen-hash.js.map +1 -0
  114. package/dist/codegen-ir.d.ts +69 -2
  115. package/dist/codegen-ir.d.ts.map +1 -1
  116. package/dist/codegen-ir.js +661 -0
  117. package/dist/codegen-ir.js.map +1 -0
  118. package/dist/codegen-lang.d.ts +65 -0
  119. package/dist/codegen-lang.d.ts.map +1 -0
  120. package/dist/codegen-lang.js +526 -0
  121. package/dist/codegen-lang.js.map +1 -0
  122. package/dist/codegen-load.d.ts +31 -0
  123. package/dist/codegen-load.d.ts.map +1 -0
  124. package/dist/codegen-load.js +138 -0
  125. package/dist/codegen-load.js.map +1 -0
  126. package/dist/codegen-loaders.js +55 -0
  127. package/dist/codegen-loaders.js.map +1 -0
  128. package/dist/codegen-mcp.d.ts.map +1 -1
  129. package/dist/codegen-mcp.js +368 -0
  130. package/dist/codegen-mcp.js.map +1 -0
  131. package/dist/codegen-python.d.ts +13 -1
  132. package/dist/codegen-python.d.ts.map +1 -1
  133. package/dist/codegen-python.js +828 -0
  134. package/dist/codegen-python.js.map +1 -0
  135. package/dist/codegen-route-tree-intern.d.ts +15 -5
  136. package/dist/codegen-route-tree-intern.d.ts.map +1 -1
  137. package/dist/codegen-route-tree-intern.js +221 -0
  138. package/dist/codegen-route-tree-intern.js.map +1 -0
  139. package/dist/codegen-rust.d.ts.map +1 -1
  140. package/dist/codegen-rust.js +1293 -0
  141. package/dist/codegen-rust.js.map +1 -0
  142. package/dist/codegen-sanitize.d.ts.map +1 -1
  143. package/dist/codegen-sanitize.js +56 -0
  144. package/dist/codegen-sanitize.js.map +1 -0
  145. package/dist/codegen-schema-naming.d.ts +5 -0
  146. package/dist/codegen-schema-naming.d.ts.map +1 -1
  147. package/dist/codegen-schema-naming.js +150 -0
  148. package/dist/codegen-schema-naming.js.map +1 -0
  149. package/dist/codegen-sdk-model.d.ts +71 -0
  150. package/dist/codegen-sdk-model.d.ts.map +1 -0
  151. package/dist/codegen-sdk-model.js +179 -0
  152. package/dist/codegen-sdk-model.js.map +1 -0
  153. package/dist/codegen.d.ts +41 -23
  154. package/dist/codegen.d.ts.map +1 -1
  155. package/dist/codegen.js +4610 -0
  156. package/dist/codegen.js.map +1 -0
  157. package/dist/context.d.ts +36 -6
  158. package/dist/context.d.ts.map +1 -1
  159. package/dist/context.js +242 -0
  160. package/dist/context.js.map +1 -0
  161. package/dist/cookie-sign.d.ts +16 -2
  162. package/dist/cookie-sign.d.ts.map +1 -1
  163. package/dist/cookie-sign.js +104 -0
  164. package/dist/cookie-sign.js.map +1 -0
  165. package/dist/cookie.d.ts +12 -0
  166. package/dist/cookie.d.ts.map +1 -1
  167. package/dist/cookie.js +138 -0
  168. package/dist/cookie.js.map +1 -0
  169. package/dist/cors.d.ts +1 -0
  170. package/dist/cors.d.ts.map +1 -1
  171. package/dist/cors.js +87 -0
  172. package/dist/cors.js.map +1 -0
  173. package/dist/crypto.d.ts +6 -1
  174. package/dist/crypto.d.ts.map +1 -1
  175. package/dist/crypto.js +38 -0
  176. package/dist/crypto.js.map +1 -0
  177. package/dist/csrf.d.ts +19 -0
  178. package/dist/csrf.d.ts.map +1 -1
  179. package/dist/csrf.js +79 -0
  180. package/dist/csrf.js.map +1 -0
  181. package/dist/curl-logger.d.ts +14 -3
  182. package/dist/curl-logger.d.ts.map +1 -1
  183. package/dist/curl-logger.js +199 -0
  184. package/dist/curl-logger.js.map +1 -0
  185. package/dist/detect-runtime.js +13 -0
  186. package/dist/detect-runtime.js.map +1 -0
  187. package/dist/dict.d.ts +11 -0
  188. package/dist/dict.d.ts.map +1 -0
  189. package/dist/dict.js +13 -0
  190. package/dist/dict.js.map +1 -0
  191. package/dist/error.d.ts.map +1 -1
  192. package/dist/error.js +44 -0
  193. package/dist/error.js.map +1 -0
  194. package/dist/errors.js +39 -0
  195. package/dist/errors.js.map +1 -0
  196. package/dist/etag.d.ts +2 -0
  197. package/dist/etag.d.ts.map +1 -1
  198. package/dist/etag.js +82 -0
  199. package/dist/etag.js.map +1 -0
  200. package/dist/feature-detect.d.ts +25 -0
  201. package/dist/feature-detect.d.ts.map +1 -0
  202. package/dist/feature-detect.js +144 -0
  203. package/dist/feature-detect.js.map +1 -0
  204. package/dist/feature-load.js +36 -0
  205. package/dist/feature-load.js.map +1 -0
  206. package/dist/feature-slots.d.ts +44 -0
  207. package/dist/feature-slots.d.ts.map +1 -0
  208. package/dist/feature-slots.js +48 -0
  209. package/dist/feature-slots.js.map +1 -0
  210. package/dist/fetch-request.d.ts +10 -0
  211. package/dist/fetch-request.d.ts.map +1 -0
  212. package/dist/fetch-request.js +13 -0
  213. package/dist/fetch-request.js.map +1 -0
  214. package/dist/gen-process.d.ts +15 -0
  215. package/dist/gen-process.d.ts.map +1 -0
  216. package/dist/gen-process.js +43 -0
  217. package/dist/gen-process.js.map +1 -0
  218. package/dist/gen-write.d.ts +19 -0
  219. package/dist/gen-write.d.ts.map +1 -0
  220. package/dist/gen-write.js +120 -0
  221. package/dist/gen-write.js.map +1 -0
  222. package/dist/glob.d.ts +21 -0
  223. package/dist/glob.d.ts.map +1 -0
  224. package/dist/glob.js +115 -0
  225. package/dist/glob.js.map +1 -0
  226. package/dist/go-type-emitter.d.ts +18 -4
  227. package/dist/go-type-emitter.d.ts.map +1 -1
  228. package/dist/go-type-emitter.js +594 -0
  229. package/dist/go-type-emitter.js.map +1 -0
  230. package/dist/honey-response.d.ts.map +1 -1
  231. package/dist/honey-response.js +338 -0
  232. package/dist/honey-response.js.map +1 -0
  233. package/dist/i18n-slot.d.ts +8 -0
  234. package/dist/i18n-slot.d.ts.map +1 -1
  235. package/dist/i18n-slot.js +14 -0
  236. package/dist/i18n-slot.js.map +1 -0
  237. package/dist/i18n.d.ts +3 -0
  238. package/dist/i18n.d.ts.map +1 -1
  239. package/dist/i18n.js +250 -0
  240. package/dist/i18n.js.map +1 -0
  241. package/dist/index.d.ts +408 -148
  242. package/dist/index.d.ts.map +1 -1
  243. package/dist/index.js +2324 -0
  244. package/dist/index.js.map +1 -0
  245. package/dist/init.d.ts +3 -5
  246. package/dist/init.d.ts.map +1 -1
  247. package/dist/init.js +140 -0
  248. package/dist/init.js.map +1 -0
  249. package/dist/input.js +4 -0
  250. package/dist/input.js.map +1 -0
  251. package/dist/invalidate-check.d.ts +11 -0
  252. package/dist/invalidate-check.d.ts.map +1 -1
  253. package/dist/invalidate-check.js +163 -0
  254. package/dist/invalidate-check.js.map +1 -0
  255. package/dist/invoke-user.d.ts +35 -0
  256. package/dist/invoke-user.d.ts.map +1 -0
  257. package/dist/invoke-user.js +73 -0
  258. package/dist/invoke-user.js.map +1 -0
  259. package/dist/ip-restrict.d.ts +13 -1
  260. package/dist/ip-restrict.d.ts.map +1 -1
  261. package/dist/ip-restrict.js +60 -0
  262. package/dist/ip-restrict.js.map +1 -0
  263. package/dist/ip.d.ts +39 -0
  264. package/dist/ip.d.ts.map +1 -0
  265. package/dist/ip.js +209 -0
  266. package/dist/ip.js.map +1 -0
  267. package/dist/logger.d.ts.map +1 -1
  268. package/dist/logger.js +113 -0
  269. package/dist/logger.js.map +1 -0
  270. package/dist/media-type.d.ts +30 -0
  271. package/dist/media-type.d.ts.map +1 -0
  272. package/dist/media-type.js +97 -0
  273. package/dist/media-type.js.map +1 -0
  274. package/dist/meta-spec-merge.d.ts +32 -0
  275. package/dist/meta-spec-merge.d.ts.map +1 -0
  276. package/dist/meta-spec-merge.js +68 -0
  277. package/dist/meta-spec-merge.js.map +1 -0
  278. package/dist/meta-spec.d.ts +16 -3
  279. package/dist/meta-spec.d.ts.map +1 -1
  280. package/dist/meta-spec.js +535 -0
  281. package/dist/meta-spec.js.map +1 -0
  282. package/dist/middleware.d.ts +23 -6
  283. package/dist/middleware.d.ts.map +1 -1
  284. package/dist/middleware.js +253 -0
  285. package/dist/middleware.js.map +1 -0
  286. package/dist/node-request.d.ts +57 -12
  287. package/dist/node-request.d.ts.map +1 -1
  288. package/dist/node-request.js +407 -0
  289. package/dist/node-request.js.map +1 -0
  290. package/dist/node.d.ts +20 -3
  291. package/dist/node.d.ts.map +1 -1
  292. package/dist/node.js +396 -0
  293. package/dist/node.js.map +1 -0
  294. package/dist/openapi/collect.d.ts +6 -3
  295. package/dist/openapi/collect.d.ts.map +1 -1
  296. package/dist/openapi/collect.js +30 -0
  297. package/dist/openapi/collect.js.map +1 -0
  298. package/dist/openapi/docs-page.d.ts +42 -0
  299. package/dist/openapi/docs-page.d.ts.map +1 -0
  300. package/dist/openapi/docs-page.js +80 -0
  301. package/dist/openapi/docs-page.js.map +1 -0
  302. package/dist/openapi/document.d.ts +4 -0
  303. package/dist/openapi/document.d.ts.map +1 -1
  304. package/dist/openapi/document.js +785 -0
  305. package/dist/openapi/document.js.map +1 -0
  306. package/dist/openapi/json-schema-slot.d.ts +7 -0
  307. package/dist/openapi/json-schema-slot.d.ts.map +1 -1
  308. package/dist/openapi/json-schema-slot.js +38 -0
  309. package/dist/openapi/json-schema-slot.js.map +1 -0
  310. package/dist/openapi/register.d.ts.map +1 -1
  311. package/dist/openapi/register.js +20 -0
  312. package/dist/openapi/register.js.map +1 -0
  313. package/dist/openapi/scalar.d.ts +3 -1
  314. package/dist/openapi/scalar.d.ts.map +1 -1
  315. package/dist/openapi/scalar.js +21 -0
  316. package/dist/openapi/scalar.js.map +1 -0
  317. package/dist/openapi/serve.d.ts +9 -0
  318. package/dist/openapi/serve.d.ts.map +1 -0
  319. package/dist/openapi/serve.js +36 -0
  320. package/dist/openapi/serve.js.map +1 -0
  321. package/dist/openapi/spec-factory.d.ts +30 -1
  322. package/dist/openapi/spec-factory.d.ts.map +1 -1
  323. package/dist/openapi/spec-factory.js +52 -0
  324. package/dist/openapi/spec-factory.js.map +1 -0
  325. package/dist/openapi/spec.d.ts +14 -5
  326. package/dist/openapi/spec.d.ts.map +1 -1
  327. package/dist/openapi/spec.js +50 -0
  328. package/dist/openapi/spec.js.map +1 -0
  329. package/dist/openapi/swagger.d.ts +3 -1
  330. package/dist/openapi/swagger.d.ts.map +1 -1
  331. package/dist/openapi/swagger.js +22 -0
  332. package/dist/openapi/swagger.js.map +1 -0
  333. package/dist/pattern.d.ts +71 -0
  334. package/dist/pattern.d.ts.map +1 -0
  335. package/dist/pattern.js +239 -0
  336. package/dist/pattern.js.map +1 -0
  337. package/dist/peer.d.ts +17 -0
  338. package/dist/peer.d.ts.map +1 -0
  339. package/dist/peer.js +50 -0
  340. package/dist/peer.js.map +1 -0
  341. package/dist/plugin.d.ts +67 -16
  342. package/dist/plugin.d.ts.map +1 -1
  343. package/dist/plugin.js +649 -0
  344. package/dist/plugin.js.map +1 -0
  345. package/dist/powered-by.d.ts.map +1 -1
  346. package/dist/powered-by.js +11 -0
  347. package/dist/powered-by.js.map +1 -0
  348. package/dist/pretty-json.d.ts.map +1 -1
  349. package/dist/pretty-json.js +47 -0
  350. package/dist/pretty-json.js.map +1 -0
  351. package/dist/producer-stream.d.ts +47 -0
  352. package/dist/producer-stream.d.ts.map +1 -0
  353. package/dist/producer-stream.js +267 -0
  354. package/dist/producer-stream.js.map +1 -0
  355. package/dist/proxy.d.ts +47 -8
  356. package/dist/proxy.d.ts.map +1 -1
  357. package/dist/proxy.js +274 -0
  358. package/dist/proxy.js.map +1 -0
  359. package/dist/python-type-emitter.d.ts +14 -1
  360. package/dist/python-type-emitter.d.ts.map +1 -1
  361. package/dist/python-type-emitter.js +157 -0
  362. package/dist/python-type-emitter.js.map +1 -0
  363. package/dist/realtime/bus.d.ts +17 -1
  364. package/dist/realtime/bus.d.ts.map +1 -1
  365. package/dist/realtime/bus.js +112 -0
  366. package/dist/realtime/bus.js.map +1 -0
  367. package/dist/realtime/register.d.ts +3 -0
  368. package/dist/realtime/register.d.ts.map +1 -0
  369. package/dist/realtime/register.js +10 -0
  370. package/dist/realtime/register.js.map +1 -0
  371. package/dist/realtime/route.d.ts +65 -26
  372. package/dist/realtime/route.d.ts.map +1 -1
  373. package/dist/realtime/route.js +72 -0
  374. package/dist/realtime/route.js.map +1 -0
  375. package/dist/realtime/server.d.ts +43 -0
  376. package/dist/realtime/server.d.ts.map +1 -0
  377. package/dist/realtime/server.js +255 -0
  378. package/dist/realtime/server.js.map +1 -0
  379. package/dist/request-id.d.ts +7 -0
  380. package/dist/request-id.d.ts.map +1 -1
  381. package/dist/request-id.js +16 -0
  382. package/dist/request-id.js.map +1 -0
  383. package/dist/request-limits.d.ts +3 -0
  384. package/dist/request-limits.d.ts.map +1 -0
  385. package/dist/request-limits.js +3 -0
  386. package/dist/request-limits.js.map +1 -0
  387. package/dist/request-path.d.ts +33 -0
  388. package/dist/request-path.d.ts.map +1 -0
  389. package/dist/request-path.js +178 -0
  390. package/dist/request-path.js.map +1 -0
  391. package/dist/request-to-curl.d.ts +7 -1
  392. package/dist/request-to-curl.d.ts.map +1 -1
  393. package/dist/request-to-curl.js +48 -0
  394. package/dist/request-to-curl.js.map +1 -0
  395. package/dist/response.d.ts +25 -2
  396. package/dist/response.d.ts.map +1 -1
  397. package/dist/response.js +313 -0
  398. package/dist/response.js.map +1 -0
  399. package/dist/rust-type-emitter.d.ts +14 -2
  400. package/dist/rust-type-emitter.d.ts.map +1 -1
  401. package/dist/rust-type-emitter.js +463 -0
  402. package/dist/rust-type-emitter.js.map +1 -0
  403. package/dist/secure-headers.d.ts.map +1 -1
  404. package/dist/secure-headers.js +53 -0
  405. package/dist/secure-headers.js.map +1 -0
  406. package/dist/serve-register.d.ts +4 -0
  407. package/dist/serve-register.d.ts.map +1 -1
  408. package/dist/serve-register.js +9 -0
  409. package/dist/serve-register.js.map +1 -0
  410. package/dist/serve-slot.d.ts +3 -0
  411. package/dist/serve-slot.d.ts.map +1 -1
  412. package/dist/serve-slot.js +52 -0
  413. package/dist/serve-slot.js.map +1 -0
  414. package/dist/serve.d.ts +18 -1
  415. package/dist/serve.d.ts.map +1 -1
  416. package/dist/serve.js +157 -0
  417. package/dist/serve.js.map +1 -0
  418. package/dist/server-timing.d.ts.map +1 -1
  419. package/dist/server-timing.js +51 -0
  420. package/dist/server-timing.js.map +1 -0
  421. package/dist/static.d.ts +9 -0
  422. package/dist/static.d.ts.map +1 -1
  423. package/dist/static.js +102 -0
  424. package/dist/static.js.map +1 -0
  425. package/dist/telemetry/otel.d.ts +2 -0
  426. package/dist/telemetry/otel.d.ts.map +1 -1
  427. package/dist/telemetry/otel.js +135 -0
  428. package/dist/telemetry/otel.js.map +1 -0
  429. package/dist/testing.d.ts +12 -0
  430. package/dist/testing.d.ts.map +1 -1
  431. package/dist/testing.js +124 -0
  432. package/dist/testing.js.map +1 -0
  433. package/dist/timeout.d.ts +6 -0
  434. package/dist/timeout.d.ts.map +1 -1
  435. package/dist/timeout.js +36 -0
  436. package/dist/timeout.js.map +1 -0
  437. package/dist/tree.d.ts +133 -36
  438. package/dist/tree.d.ts.map +1 -1
  439. package/dist/tree.js +437 -0
  440. package/dist/tree.js.map +1 -0
  441. package/dist/trust.d.ts +21 -0
  442. package/dist/trust.d.ts.map +1 -0
  443. package/dist/trust.js +100 -0
  444. package/dist/trust.js.map +1 -0
  445. package/dist/ts-type-emitter.d.ts.map +1 -1
  446. package/dist/ts-type-emitter.js +150 -0
  447. package/dist/ts-type-emitter.js.map +1 -0
  448. package/dist/type-emitter.d.ts +16 -2
  449. package/dist/type-emitter.d.ts.map +1 -1
  450. package/dist/type-emitter.js +729 -0
  451. package/dist/type-emitter.js.map +1 -0
  452. package/dist/type-extractor.d.ts +8 -4
  453. package/dist/type-extractor.d.ts.map +1 -1
  454. package/dist/type-extractor.js +776 -0
  455. package/dist/type-extractor.js.map +1 -0
  456. package/dist/types.d.ts +7 -0
  457. package/dist/types.d.ts.map +1 -1
  458. package/dist/types.js +96 -0
  459. package/dist/types.js.map +1 -0
  460. package/dist/validation.d.ts +14 -2
  461. package/dist/validation.d.ts.map +1 -1
  462. package/dist/validation.js +335 -0
  463. package/dist/validation.js.map +1 -0
  464. package/dist/with-headers.d.ts +15 -0
  465. package/dist/with-headers.d.ts.map +1 -0
  466. package/dist/with-headers.js +45 -0
  467. package/dist/with-headers.js.map +1 -0
  468. package/dist/ws/bun.d.ts +11 -5
  469. package/dist/ws/bun.d.ts.map +1 -1
  470. package/dist/ws/bun.js +82 -0
  471. package/dist/ws/bun.js.map +1 -0
  472. package/dist/ws/cloudflare.d.ts +25 -3
  473. package/dist/ws/cloudflare.d.ts.map +1 -1
  474. package/dist/ws/cloudflare.js +117 -0
  475. package/dist/ws/cloudflare.js.map +1 -0
  476. package/dist/ws/deno.d.ts +4 -4
  477. package/dist/ws/deno.d.ts.map +1 -1
  478. package/dist/ws/deno.js +133 -0
  479. package/dist/ws/deno.js.map +1 -0
  480. package/dist/ws/node.d.ts +20 -3
  481. package/dist/ws/node.d.ts.map +1 -1
  482. package/dist/ws/node.js +173 -0
  483. package/dist/ws/node.js.map +1 -0
  484. package/dist/ws/session.d.ts +11 -0
  485. package/dist/ws/session.d.ts.map +1 -0
  486. package/dist/ws/session.js +60 -0
  487. package/dist/ws/session.js.map +1 -0
  488. package/dist/ws/shared.d.ts +65 -0
  489. package/dist/ws/shared.d.ts.map +1 -0
  490. package/dist/ws/shared.js +103 -0
  491. package/dist/ws/shared.js.map +1 -0
  492. package/dist/ws-origin.d.ts +20 -0
  493. package/dist/ws-origin.d.ts.map +1 -0
  494. package/dist/ws-origin.js +41 -0
  495. package/dist/ws-origin.js.map +1 -0
  496. package/dist/yaml.d.ts +15 -1
  497. package/dist/yaml.d.ts.map +1 -1
  498. package/dist/yaml.js +101 -0
  499. package/dist/yaml.js.map +1 -0
  500. package/docs/invalidate-spec.md +13 -1
  501. package/docs/meta-spec.md +36 -12
  502. package/docs/packaging.md +43 -24
  503. package/docs/regression-matrix/README.md +32 -0
  504. package/docs/regression-matrix/review.md +44 -0
  505. package/docs/regression-matrix/ws1-3.md +97 -0
  506. package/docs/regression-matrix/ws4-8.md +107 -0
  507. package/docs/regression-matrix/ws9-12.md +266 -0
  508. package/docs/sdk.md +102 -52
  509. package/package.json +183 -52
  510. package/src/accepts.ts +91 -33
  511. package/src/body-kind.ts +67 -0
  512. package/src/body-limit.ts +102 -87
  513. package/src/build/index.ts +104 -54
  514. package/src/cli-args.ts +123 -0
  515. package/src/cli-go/auth.go +12 -1
  516. package/src/cli-go/config.go +29 -2
  517. package/src/cli-go/errors.go +28 -10
  518. package/src/cli-go/multipart.go +51 -27
  519. package/src/cli-go/output.go +58 -4
  520. package/src/cli-go/root.go +1 -1
  521. package/src/cli-go/version.go +2 -2
  522. package/src/cli.ts +154 -148
  523. package/src/client/error.ts +56 -1
  524. package/src/client/http.ts +479 -256
  525. package/src/client/index.ts +55 -17
  526. package/src/client/path.ts +169 -0
  527. package/src/client/redirect-policy.ts +19 -0
  528. package/src/client/sdk.ts +180 -101
  529. package/src/client/sse.ts +92 -66
  530. package/src/client/ws.ts +67 -31
  531. package/src/client-go/errors.go +41 -12
  532. package/src/client-go/export.go +44 -0
  533. package/src/client-go/invalidation.go +52 -54
  534. package/src/client-go/nullable.go +62 -0
  535. package/src/client-go/realtime.go +147 -110
  536. package/src/client-go/runtime.go +646 -88
  537. package/src/client-go/sse.go +104 -94
  538. package/src/client-go/transport.go +67 -27
  539. package/src/client-go/ws.go +49 -25
  540. package/src/client-info.ts +79 -0
  541. package/src/client-mcp/server.ts +133 -3
  542. package/src/client-python/_invalidation.py +207 -291
  543. package/src/client-python/_realtime.py +63 -40
  544. package/src/client-python/_runtime.py +501 -119
  545. package/src/client-python/_sse.py +130 -74
  546. package/src/client-python/_transport.py +37 -9
  547. package/src/client-python/_ws.py +50 -22
  548. package/src/client-rust/errors.rs +21 -1
  549. package/src/client-rust/invalidation.rs +202 -231
  550. package/src/client-rust/invalidation_sync.rs +16 -229
  551. package/src/client-rust/nullable.rs +17 -0
  552. package/src/client-rust/realtime.rs +217 -217
  553. package/src/client-rust/runtime.rs +602 -177
  554. package/src/client-rust/runtime_sync.rs +169 -145
  555. package/src/client-rust/sse.rs +142 -121
  556. package/src/client-rust/ws.rs +61 -37
  557. package/src/codegen-go-cli.ts +607 -728
  558. package/src/codegen-go.ts +710 -770
  559. package/src/codegen-ir.ts +381 -75
  560. package/src/codegen-lang.ts +517 -0
  561. package/src/codegen-load.ts +150 -0
  562. package/src/codegen-mcp.ts +207 -108
  563. package/src/codegen-python.ts +684 -816
  564. package/src/codegen-route-tree-intern.ts +51 -25
  565. package/src/codegen-rust.ts +946 -1240
  566. package/src/codegen-sanitize.ts +0 -4
  567. package/src/codegen-schema-naming.ts +19 -6
  568. package/src/codegen-sdk-model.ts +228 -0
  569. package/src/codegen.ts +1488 -1140
  570. package/src/context.ts +130 -11
  571. package/src/cookie-sign.ts +85 -16
  572. package/src/cookie.ts +66 -11
  573. package/src/cors.ts +55 -54
  574. package/src/crypto.ts +27 -5
  575. package/src/csrf.ts +57 -25
  576. package/src/curl-logger.ts +155 -114
  577. package/src/dict.ts +12 -0
  578. package/src/error.ts +4 -2
  579. package/src/etag.ts +58 -34
  580. package/src/feature-detect.ts +147 -0
  581. package/src/feature-slots.ts +70 -0
  582. package/src/fetch-request.ts +13 -0
  583. package/src/gen-process.ts +42 -0
  584. package/src/gen-write.ts +127 -0
  585. package/src/glob.ts +109 -0
  586. package/src/go-type-emitter.ts +406 -155
  587. package/src/honey-response.ts +36 -9
  588. package/src/i18n-slot.ts +10 -0
  589. package/src/i18n.ts +98 -23
  590. package/src/index.ts +1887 -1756
  591. package/src/init.ts +40 -28
  592. package/src/invalidate-check.ts +42 -4
  593. package/src/invoke-user.ts +95 -0
  594. package/src/ip-restrict.ts +47 -161
  595. package/src/ip.ts +194 -0
  596. package/src/logger.ts +65 -36
  597. package/src/media-type.ts +104 -0
  598. package/src/meta-spec-merge.ts +80 -0
  599. package/src/meta-spec.ts +0 -0
  600. package/src/middleware.ts +112 -12
  601. package/src/node-request.ts +242 -52
  602. package/src/node.ts +318 -75
  603. package/src/openapi/collect.ts +19 -44
  604. package/src/openapi/docs-page.ts +89 -0
  605. package/src/openapi/document.ts +503 -224
  606. package/src/openapi/json-schema-slot.ts +40 -0
  607. package/src/openapi/register.ts +7 -2
  608. package/src/openapi/scalar.ts +11 -10
  609. package/src/openapi/serve.ts +39 -0
  610. package/src/openapi/spec-factory.ts +50 -1
  611. package/src/openapi/spec.ts +53 -30
  612. package/src/openapi/swagger.ts +12 -11
  613. package/src/pattern.ts +245 -0
  614. package/src/peer.ts +53 -0
  615. package/src/plugin.ts +270 -185
  616. package/src/powered-by.ts +2 -2
  617. package/src/pretty-json.ts +25 -9
  618. package/src/producer-stream.ts +296 -0
  619. package/src/proxy.ts +253 -55
  620. package/src/python-type-emitter.ts +68 -56
  621. package/src/realtime/bus.ts +56 -9
  622. package/src/realtime/register.ts +13 -0
  623. package/src/realtime/route.ts +129 -80
  624. package/src/realtime/server.ts +298 -0
  625. package/src/request-id.ts +13 -3
  626. package/src/request-limits.ts +2 -0
  627. package/src/request-path.ts +165 -0
  628. package/src/request-to-curl.ts +28 -7
  629. package/src/response.ts +143 -103
  630. package/src/rust-type-emitter.ts +265 -170
  631. package/src/secure-headers.ts +8 -4
  632. package/src/serve-register.ts +4 -0
  633. package/src/serve-slot.ts +38 -0
  634. package/src/serve.ts +113 -25
  635. package/src/server-timing.ts +11 -7
  636. package/src/static.ts +85 -32
  637. package/src/telemetry/otel.ts +29 -7
  638. package/src/testing.ts +76 -14
  639. package/src/timeout.ts +15 -7
  640. package/src/tree.ts +405 -333
  641. package/src/trust.ts +117 -0
  642. package/src/ts-type-emitter.ts +18 -12
  643. package/src/type-emitter.ts +227 -89
  644. package/src/type-extractor.ts +562 -424
  645. package/src/types.ts +8 -0
  646. package/src/validation.ts +221 -96
  647. package/src/with-headers.ts +45 -0
  648. package/src/ws/bun.ts +53 -15
  649. package/src/ws/cloudflare.ts +81 -19
  650. package/src/ws/deno.ts +124 -36
  651. package/src/ws/node.ts +173 -79
  652. package/src/ws/session.ts +73 -0
  653. package/src/ws/shared.ts +140 -0
  654. package/src/ws-origin.ts +53 -0
  655. package/src/yaml.ts +45 -22
  656. package/dist/client-mcp/server.d.ts +0 -10
  657. package/dist/client-mcp/server.d.ts.map +0 -1
  658. package/dist/client-mcp/types.d.ts +0 -16
  659. package/dist/client-mcp/types.d.ts.map +0 -1
  660. package/dist/cloudflare/realtime.d.ts +0 -30
  661. package/dist/cloudflare/realtime.d.ts.map +0 -1
  662. package/dist/realtime/buffer.d.ts +0 -18
  663. package/dist/realtime/buffer.d.ts.map +0 -1
  664. package/dist/realtime/longpoll.d.ts +0 -23
  665. package/dist/realtime/longpoll.d.ts.map +0 -1
  666. package/dist/realtime/protocol.d.ts +0 -50
  667. package/dist/realtime/protocol.d.ts.map +0 -1
  668. package/dist/realtime/runtime.d.ts +0 -74
  669. package/dist/realtime/runtime.d.ts.map +0 -1
  670. package/src/cloudflare/realtime.ts +0 -234
  671. package/src/realtime/buffer.ts +0 -101
  672. package/src/realtime/longpoll.ts +0 -148
  673. package/src/realtime/protocol.ts +0 -128
  674. package/src/realtime/runtime.ts +0 -427
@@ -1,10 +1,19 @@
1
- /** Python SDK code generator. Mirrors TS SDK emitter in codegen.ts. */
1
+ /** Python SDK code generator.
2
+ *
3
+ * Input is the SDK request model (codegen-sdk-model.ts). Identifiers come from per-scope
4
+ * NameScopes (module, class, method) and every spec string reaches source through pyString,
5
+ * docstring or comment writers (codegen-lang.ts).
6
+ */
2
7
 
3
8
  import { readFileSync } from "node:fs"
4
- import { extractOpenApiPathParams, isSSEOperation } from "./codegen.ts"
5
- import { methodsOf, namespacesOf, toIR } from "./codegen-ir.ts"
6
- import type { IRNamespace } from "./codegen-ir.ts"
7
- import { jsonSchemaToPy, pyIdent } from "./python-type-emitter.ts"
9
+ import type { OpenApiSpecInput } from "./codegen.ts"
10
+ import { methodsOf, namespacesOf } from "./codegen-ir.ts"
11
+ import type { IRNamespace, IRSchema } from "./codegen-ir.ts"
12
+ import { NameScope, cmpCodeUnit, pyClassName, pyDocLines, pyIdentifier, pyString } from "./codegen-lang.ts"
13
+ import { buildSdkModel } from "./codegen-sdk-model.ts"
14
+ import type { SdkModel, SdkOp } from "./codegen-sdk-model.ts"
15
+ import { irToPythonNamed, isPyAttributeKey, pyIdent } from "./python-type-emitter.ts"
16
+ import type { PyTypeNames } from "./python-type-emitter.ts"
8
17
 
9
18
  interface PySDKOptions {
10
19
  name?: string
@@ -25,33 +34,6 @@ interface PySDKResult {
25
34
  serviceMap: Record<string, ServiceMapEntry>
26
35
  }
27
36
 
28
- interface OperationInfo {
29
- operationId: string
30
- method: string
31
- path: string
32
- summary?: string
33
- description?: string
34
- pathParams: string[]
35
- queryParams: ParamInfo[]
36
- bodySchema: Record<string, unknown> | undefined
37
- bodyRequired: boolean
38
- bodyIsStream: boolean
39
- successType: string
40
- errorStatuses: number[]
41
- isSSE: boolean
42
- isWS: boolean
43
- isRealtime: boolean
44
- isIdempotent: boolean
45
- invalidate: string[]
46
- parameters: Array<{ in: string; name: string; required: boolean; schema: Record<string, unknown> }>
47
- }
48
-
49
- interface ParamInfo {
50
- name: string
51
- required: boolean
52
- schema: Record<string, unknown>
53
- }
54
-
55
37
  const PYTHON_FUTURE = "from __future__ import annotations"
56
38
  const STATIC_FILE_NAMES = [
57
39
  "_runtime.py",
@@ -63,11 +45,6 @@ const STATIC_FILE_NAMES = [
63
45
  "_realtime.py",
64
46
  ] as const
65
47
  const RUNTIME_TEMPLATE_CACHE = new Map<string, string>()
66
- const WS_EXTENSION = "x-websocket"
67
- const REALTIME_EXTENSION = "x-realtime"
68
- const INVALIDATE_EXTENSION = "x-invalidate"
69
- const IDEMPOTENCY_EXTENSION = "x-idempotency-key"
70
- const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"] as const
71
48
 
72
49
  const ERROR_NAMES: Record<number, string> = {
73
50
  400: "BadRequestError",
@@ -83,6 +60,69 @@ const ERROR_NAMES: Record<number, string> = {
83
60
  504: "GatewayTimeoutError",
84
61
  }
85
62
 
63
+ const ERROR_CLASSES = [
64
+ "APIError",
65
+ "APIStatusError",
66
+ "BadGatewayError",
67
+ "BadRequestError",
68
+ "ConflictError",
69
+ "ForbiddenError",
70
+ "GatewayTimeoutError",
71
+ "InternalServerError",
72
+ "NotFoundError",
73
+ "RateLimitError",
74
+ "ServiceUnavailableError",
75
+ "UnauthorizedError",
76
+ "UnprocessableEntityError",
77
+ ]
78
+
79
+ const TRANSPORT_EXPORTS = [
80
+ "LongpollAdapter",
81
+ "SseAdapter",
82
+ "TransportAdapter",
83
+ "TransportConn",
84
+ "TransportKind",
85
+ "TransportOpts",
86
+ "WsAdapter",
87
+ ]
88
+
89
+ const REALTIME_EXPORTS = ["ConnectionState", "ResumableConnection"]
90
+
91
+ /** Names a generated module-level type must not take: package exports, typing and builtins. */
92
+ const PY_MODULE_RESERVED = [
93
+ "AsyncSDK",
94
+ "SDK",
95
+ ...ERROR_CLASSES,
96
+ ...TRANSPORT_EXPORTS,
97
+ ...REALTIME_EXPORTS,
98
+ "ClientConfig",
99
+ "InvalidationConfig",
100
+ "SDKResult",
101
+ "SSEEvent",
102
+ "PathParamError",
103
+ "FileInput",
104
+ "Any",
105
+ "AsyncIterator",
106
+ "Literal",
107
+ "NotRequired",
108
+ "TypeAlias",
109
+ "TypedDict",
110
+ "bool",
111
+ "bytes",
112
+ "dict",
113
+ "float",
114
+ "int",
115
+ "list",
116
+ "object",
117
+ "str",
118
+ "tuple",
119
+ "type",
120
+ "httpx",
121
+ "asyncio",
122
+ "threading",
123
+ "uuid",
124
+ ]
125
+
86
126
  export function loadPythonRuntimeTemplates(): Map<string, string> {
87
127
  if (RUNTIME_TEMPLATE_CACHE.size > 0) return RUNTIME_TEMPLATE_CACHE
88
128
  for (const name of STATIC_FILE_NAMES) {
@@ -93,845 +133,702 @@ export function loadPythonRuntimeTemplates(): Map<string, string> {
93
133
  return RUNTIME_TEMPLATE_CACHE
94
134
  }
95
135
 
96
- function resolveSchema(
97
- spec: Record<string, unknown>,
98
- schema: Record<string, unknown> | undefined,
99
- ): Record<string, unknown> | undefined {
100
- if (!schema) return undefined
101
- if (!schema.$ref) return schema
102
- const ref = schema.$ref as string
103
- const parts = ref.replace(/^#\//, "").split("/")
104
- let cur: unknown = spec
105
- for (const part of parts) {
106
- cur = (cur as Record<string, unknown>)[part]
107
- }
108
- return cur as Record<string, unknown> | undefined
109
- }
136
+ /* ── types ── */
110
137
 
111
- function getSuccessType(spec: Record<string, unknown>, operation: Record<string, unknown>): string {
112
- const responses = operation.responses as Record<string, Record<string, unknown>> | undefined
113
- if (!responses) return "None"
114
- const successCodes = ["200", "201", "202", "204"]
115
- for (const code of successCodes) {
116
- const resp = responses[code]
117
- if (!resp) continue
118
- if (code === "204") return "None"
119
- const content = resp.content as Record<string, Record<string, unknown>> | undefined
120
- if (!content) continue
121
- const jsonContent = content["application/json"]
122
- if (!jsonContent) continue
123
- const schema = resolveSchema(spec, jsonContent.schema as Record<string, unknown> | undefined)
124
- if (!schema) continue
125
- const rawRef = (jsonContent.schema as Record<string, unknown> | undefined)?.$ref as string | undefined
126
- if (rawRef) {
127
- const parts = rawRef.split("/")
128
- return parts[parts.length - 1]
129
- }
130
- return jsonSchemaToPy(schema, 0)
131
- }
132
- return "None"
138
+ type PyNames = {
139
+ module: NameScope
140
+ types: PyTypeNames
141
+ /** class name → source, for every emitted type (schemas first, then hoisted). */
142
+ decls: Map<string, string>
143
+ declOrder: string[]
144
+ schemaNames: Map<string, string>
133
145
  }
134
146
 
135
- function getErrorStatuses(operation: Record<string, unknown>): number[] {
136
- const responses = operation.responses as Record<string, Record<string, unknown>> | undefined
137
- if (!responses) return []
138
- return Object.keys(responses)
139
- .map(Number)
140
- .filter((n) => n >= 400)
141
- .sort()
147
+ function typedDictSource(name: string, ir: Extract<IRSchema, { kind: "object" }>, names: PyTypeNames): string {
148
+ const fields = ir.fields.slice().sort((a, b) => cmpCodeUnit(a.name, b.name))
149
+ const rendered = fields.map((f) => {
150
+ const t = irToPythonNamed(f.schema, names, name, f.name)
151
+ return { annotation: f.required ? t : `NotRequired[${t}]`, field: f }
152
+ })
153
+ if (fields.every((f) => isPyAttributeKey(f.name))) {
154
+ const l = [`class ${name}(TypedDict, total=False):`]
155
+ for (const r of rendered) l.push(` ${r.field.name}: ${r.annotation}`)
156
+ if (rendered.length === 0) l.push(` pass`)
157
+ return l.join("\n")
158
+ }
159
+ /* functional syntax: any JSON key is allowed; annotations are strings, so forward refs work */
160
+ const l = [`${name} = TypedDict(`, ` ${pyString(name)},`, ` {`]
161
+ for (const r of rendered) l.push(` ${pyString(r.field.name)}: ${pyString(r.annotation)},`)
162
+ l.push(` },`, ` total=False,`, `)`)
163
+ return l.join("\n")
142
164
  }
143
165
 
144
- function collectOperations(spec: Record<string, unknown>): OperationInfo[] {
145
- const paths = spec.paths as Record<string, Record<string, unknown>> | undefined
146
- if (!paths) return []
147
- const ops: OperationInfo[] = []
148
-
149
- for (const [path, pathItem] of Object.entries(paths)) {
150
- for (const httpMethod of HTTP_METHODS) {
151
- const operation = pathItem[httpMethod] as Record<string, unknown> | undefined
152
- if (!operation) continue
153
- const operationId = operation.operationId as string | undefined
154
- if (!operationId) continue
155
-
156
- const rawParams = (operation.parameters ?? []) as Array<Record<string, unknown>>
157
- const pathParams = extractOpenApiPathParams(path)
158
- const queryParams: ParamInfo[] = rawParams
159
- .filter((p) => p.in === "query")
160
- .map((p) => ({
161
- name: p.name as string,
162
- required: p.required === true,
163
- schema: (p.schema ?? {}) as Record<string, unknown>,
164
- }))
165
-
166
- const requestBody = operation.requestBody as Record<string, unknown> | undefined
167
- let bodySchema: Record<string, unknown> | undefined
168
- let bodyIsStream = false
169
- const bodyRequired = requestBody?.required === true
170
- if (requestBody) {
171
- const content = requestBody.content as Record<string, Record<string, unknown>> | undefined
172
- const jsonContent = content?.["application/json"]
173
- const octetContent = content?.["application/octet-stream"]
174
- if (octetContent) {
175
- bodyIsStream = true
176
- } else if (jsonContent) {
177
- const rawSchema = jsonContent.schema as Record<string, unknown> | undefined
178
- bodySchema = resolveSchema(spec, rawSchema)
179
- if (rawSchema?.$ref) {
180
- const refParts = (rawSchema.$ref as string).split("/")
181
- bodySchema = { $ref: rawSchema.$ref, _refName: refParts[refParts.length - 1] }
182
- }
183
- }
184
- }
166
+ function buildPyNames(model: SdkModel): PyNames {
167
+ const module = new NameScope(PY_MODULE_RESERVED)
168
+ const schemaNames = new Map<string, string>()
169
+ for (const name of model.schemaNames) {
170
+ const base = pyClassName(name)
171
+ schemaNames.set(name, module.has(base) ? module.claim(`${base}Model`) : module.claim(base))
172
+ }
173
+ const decls = new Map<string, string>()
174
+ const declOrder: string[] = []
175
+ const hoistKeys = new Map<string, string>()
176
+ const names: PyNames = { declOrder, decls, module, schemaNames, types: undefined as unknown as PyTypeNames }
177
+ names.types = {
178
+ hoist: (key, base, schema) => {
179
+ const hit = hoistKeys.get(key)
180
+ if (hit) return hit
181
+ const name = module.claim(pyClassName(base))
182
+ hoistKeys.set(key, name)
183
+ decls.set(name, "")
184
+ declOrder.push(name)
185
+ decls.set(name, typedDictSource(name, schema, names.types))
186
+ return name
187
+ },
188
+ ref: (name) => schemaNames.get(name) ?? pyClassName(name),
189
+ }
190
+ return names
191
+ }
185
192
 
186
- const isRealtime = operation[REALTIME_EXTENSION] === true
187
- ops.push({
188
- bodyIsStream,
189
- bodyRequired,
190
- bodySchema,
191
- description: operation.description as string | undefined,
192
- errorStatuses: getErrorStatuses(operation),
193
- invalidate: (operation[INVALIDATE_EXTENSION] as string[] | undefined) ?? [],
194
- isIdempotent: operation[IDEMPOTENCY_EXTENSION] === true,
195
- /* realtime wins over SSE / WS when extensions conflict — higher-level protocol */
196
- isRealtime,
197
- isSSE: !isRealtime && isSSEOperation(operation),
198
- isWS: !isRealtime && operation[WS_EXTENSION] === true,
199
- method: httpMethod.toUpperCase(),
200
- operationId,
201
- parameters: rawParams as Array<{
202
- in: string
203
- name: string
204
- required: boolean
205
- schema: Record<string, unknown>
206
- }>,
207
- path,
208
- pathParams,
209
- queryParams,
210
- successType: getSuccessType(spec, operation),
211
- summary: operation.summary as string | undefined,
212
- })
193
+ function buildPyTypes(model: SdkModel, names: PyNames): string {
194
+ const body: string[] = []
195
+ for (const name of model.schemaNames) {
196
+ const ir = model.ir.schemas[name]
197
+ const pyName = names.types.ref(name)
198
+ if (ir.kind === "object" && ir.fields.length > 0) {
199
+ body.push(typedDictSource(pyName, ir, names.types))
200
+ } else {
201
+ /* string alias: lazy, so the order of definitions does not matter */
202
+ const t = irToPythonNamed(ir, names.types, pyName, "")
203
+ body.push(`${pyName}: TypeAlias = ${pyString(t)}`)
213
204
  }
205
+ body.push("")
214
206
  }
215
-
216
- return ops
217
- }
218
-
219
- function nestedClassName(path: string[]): string {
220
- const pascal = path
221
- .map((seg) => {
222
- const safe = pyIdent(seg)
223
- return safe.charAt(0).toUpperCase() + safe.slice(1)
224
- })
225
- .join("")
226
- return `_${pascal}Resource`
207
+ return body.join("\n")
227
208
  }
228
209
 
229
- function collectSDKMethods(spec: Record<string, unknown>): {
230
- tree: IRNamespace
231
- topLevel: OperationInfo[]
232
- opsByOpId: Map<string, OperationInfo>
233
- } {
234
- const ops = collectOperations(spec)
235
- const ir = toIR(spec as Parameters<typeof toIR>[0])
236
- const topLevel: OperationInfo[] = []
237
- const opsByOpId = new Map<string, OperationInfo>()
238
-
239
- for (const op of ops) {
240
- opsByOpId.set(op.operationId, op)
241
- if (!op.operationId.includes(".")) {
242
- topLevel.push(op)
243
- }
210
+ function typesFile(schemaSource: string, names: PyNames): string {
211
+ const hoisted = names.declOrder.map((n) => `${names.decls.get(n)}\n`).join("\n")
212
+ const bodyText = `${schemaSource}\n${hoisted}`
213
+ const typing = ["Any", "Literal", "NotRequired", "TypeAlias", "TypedDict"].filter((sym) =>
214
+ new RegExp(`\\b${sym}\\b`).test(bodyText),
215
+ )
216
+ const fileInput = /\bFileInput\b/.test(bodyText)
217
+ /* string aliases mention `Any`/`Literal` only inside quotes; keep them importable for checkers */
218
+ if (/TypeAlias = /.test(bodyText)) {
219
+ for (const sym of ["Any", "Literal"]) if (!typing.includes(sym)) typing.push(sym)
244
220
  }
245
-
246
- return { opsByOpId, topLevel, tree: ir.tree }
221
+ const l: string[] = []
222
+ l.push("# ruff: noqa: E501")
223
+ l.push(PYTHON_FUTURE)
224
+ l.push("")
225
+ if (typing.length > 0) l.push(`from typing import ${typing.sort(cmpCodeUnit).join(", ")}`)
226
+ if (fileInput) l.push(`from ._runtime import FileInput`)
227
+ if (typing.length > 0 || fileInput) l.push("")
228
+ l.push(bodyText)
229
+ const exported = [...names.schemaNames.values(), ...names.declOrder]
230
+ if (exported.length === 0) l.push("__all__: list[str] = []")
231
+ else l.push(`__all__ = [${exported.map(pyString).join(", ")}]`)
232
+ l.push("")
233
+ return l.join("\n")
247
234
  }
248
235
 
249
- function pyParamName(name: string): string {
250
- const safe = name.replace(/[^a-zA-Z0-9_]/g, "_").replace(/^([0-9])/, "_$1")
251
- return pyIdent(safe)
252
- }
236
+ /* ── client ── */
253
237
 
254
- function bodyTypeName(bodySchema: Record<string, unknown> | undefined): string {
255
- if (!bodySchema) return "Any"
256
- const refName = bodySchema._refName as string | undefined
257
- if (refName) return refName
258
- return "Any"
259
- }
238
+ type PyParam = { local: string; wire: string }
260
239
 
261
- function buildMethodSignature(op: OperationInfo, throwOnError: boolean, isAsync: boolean): string[] {
262
- const lines: string[] = []
263
- /* WS methods just construct the _TypedWebSocket context-manager — no async
264
- * work happens until __aenter__; emit as sync `def` matching TS / Go / Rust
265
- * surface (caller awaits __aenter__ via `async with`, not the constructor). */
266
- let defKw: string
267
- if (op.isWS) defKw = "def"
268
- else if (isAsync) defKw = "async def"
269
- else defKw = "def"
270
- const segments = op.operationId.split(".")
271
- const actionPart = segments[segments.length - 1] ?? op.operationId
272
- /* convert camelCase to snake_case for Python naming convention */
273
- const snakeAction = actionPart.replace(/([A-Z])/g, (m) => `_${m.toLowerCase()}`).replace(/^_/, "")
274
- const ident = pyIdent(snakeAction)
275
- /* realtime method always named `connect<X>` — follow camelCase convention used elsewhere
276
- * in the emitted SDK (connectWs, createUser). If operationId already begins with "connect",
277
- * use verbatim; otherwise prepend "connect" + capitalize first char of action. */
278
- const alreadyConnectPrefixed = /^connect([A-Z_]|$)/.test(ident)
279
- const realtimeName = alreadyConnectPrefixed ? ident : `connect${ident.charAt(0).toUpperCase()}${ident.slice(1)}`
280
- const methodName = op.isRealtime ? realtimeName : ident
281
-
282
- const posArgs: string[] = ["self"]
283
-
284
- for (const pp of op.pathParams) {
285
- posArgs.push(`${pyParamName(pp)}: str`)
286
- }
240
+ type PyOpPlan = {
241
+ op: SdkOp
242
+ methodName: string
243
+ pathParams: PyParam[]
244
+ queryParams: Array<PyParam & { required: boolean; type: string }>
245
+ headerParams: Array<PyParam & { required: boolean; type: string }>
246
+ body?: { local: string; type: string; required: boolean }
247
+ multipart?: { files: string[]; listFiles: string[] }
248
+ returnType: string
249
+ }
287
250
 
288
- if (op.isRealtime) {
289
- const kwArgsRt: string[] = [
290
- `reconnect_token: str | None = None`,
291
- `last_event_id: str | None = None`,
292
- `headers: dict[str, str] | None = None`,
293
- ]
294
- const posLineRt = posArgs.join(", ")
295
- /* realtime method is sync — returns ResumableConnection (async-iterable).
296
- * matches TS: `sdk.connectRealtime()` returns object, not a Promise. */
297
- lines.push(` def ${methodName}(${posLineRt},`)
298
- lines.push(` *,`)
299
- for (let i = 0; i < kwArgsRt.length; i++) {
300
- const comma = i < kwArgsRt.length - 1 ? "," : ""
301
- lines.push(` ${kwArgsRt[i]}${comma}`)
251
+ /** kwargs the generated methods always take; params must not reuse them. */
252
+ /** What client.py may import from the runtime modules; each name only when the bodies use it. */
253
+ const PY_CLIENT_IMPORTS: ReadonlyArray<readonly [string, readonly string[]]> = [
254
+ [
255
+ "._runtime",
256
+ [
257
+ "ClientConfig",
258
+ "SDKResult",
259
+ "_AuthState",
260
+ "_build_headers",
261
+ "_build_url",
262
+ "_do_request_async",
263
+ "_do_request_sync",
264
+ "_expand_path",
265
+ "_form_content",
266
+ "_format_value",
267
+ "_has_header",
268
+ "_multipart_parts",
269
+ "_open_stream",
270
+ "_parse_body",
271
+ "_raise_for_status",
272
+ "_set_header",
273
+ "_to_ws_url",
274
+ ],
275
+ ],
276
+ ["._invalidation", ["_StaleTracker", "_StaleTrackerSync"]],
277
+ ["._sse", ["SSEEvent", "parse_sse_stream"]],
278
+ ["._ws", ["_TypedWebSocket"]],
279
+ ["._transport", ["LongpollAdapter", "SseAdapter", "TransportOpts", "WsAdapter"]],
280
+ ["._realtime", ["ResumableConnection"]],
281
+ ]
282
+
283
+ /**
284
+ * Module-scope names a generated method body evaluates at runtime (calls and attribute access, not
285
+ * annotations, which `from __future__ import annotations` never evaluates). A parameter local must
286
+ * never shadow one: a path param named `uuid` would turn `str(uuid.uuid4())` into an
287
+ * AttributeError. Underscore names cannot collide, since parameter locals never start with `_`.
288
+ */
289
+ export const PY_BODY_GLOBALS: readonly string[] = [
290
+ "httpx",
291
+ "str",
292
+ "threading",
293
+ "uuid",
294
+ ...PY_CLIENT_IMPORTS.flatMap(([, syms]) => syms.filter((n) => !n.startsWith("_"))),
295
+ ]
296
+
297
+ const PY_METHOD_RESERVED = [
298
+ "self",
299
+ "timeout",
300
+ "headers",
301
+ "cancel_token",
302
+ "idempotency_key",
303
+ "last_event_id",
304
+ "protocols",
305
+ "reconnect_token",
306
+ ...PY_BODY_GLOBALS,
307
+ ]
308
+
309
+ function planPyOp(op: SdkOp, methodName: string, names: PyNames, model: SdkModel): PyOpPlan {
310
+ const locals = new NameScope(PY_METHOD_RESERVED)
311
+ const typeParent = pyClassName(op.id)
312
+ const plan: PyOpPlan = { headerParams: [], methodName, op, pathParams: [], queryParams: [], returnType: "None" }
313
+ const b = op.body
314
+ const hasBodyArg = b !== undefined && op.stream !== "ws" && op.stream !== "realtime"
315
+ const bodyLocal = hasBodyArg ? locals.claim("body") : ""
316
+ for (const p of op.pathParams) plan.pathParams.push({ local: locals.claim(pyIdentifier(p.name)), wire: p.name })
317
+ if (op.stream !== "realtime") {
318
+ for (const q of op.query) {
319
+ if (op.stream === "sse" && /^last[-_]?event[-_]?id$/i.test(q.name)) continue
320
+ plan.queryParams.push({
321
+ local: locals.claim(pyIdentifier(q.name)),
322
+ required: q.required === true,
323
+ type: irToPythonNamed(q.schema, names.types, typeParent, q.name),
324
+ wire: q.name,
325
+ })
326
+ }
327
+ for (const h of op.headers) {
328
+ plan.headerParams.push({
329
+ local: locals.claim(pyIdentifier(h.name)),
330
+ required: h.required === true,
331
+ type: irToPythonNamed(h.schema, names.types, typeParent, h.name),
332
+ wire: h.name,
333
+ })
302
334
  }
303
- lines.push(` ) -> ResumableConnection:`)
304
- return lines
305
335
  }
306
-
307
- if (op.bodyIsStream) {
308
- posArgs.push(`body: AsyncIterator[bytes] | bytes | bytearray`)
309
- } else if (op.bodySchema && op.bodyRequired) {
310
- posArgs.push(`body: ${bodyTypeName(op.bodySchema)}`)
336
+ if (hasBodyArg && b) {
337
+ let type: string
338
+ switch (b.kind) {
339
+ case "json":
340
+ case "form":
341
+ type = irToPythonNamed(b.schema, names.types, typeParent, "Body")
342
+ break
343
+ case "binary":
344
+ type = "bytes | AsyncIterator[bytes]"
345
+ break
346
+ case "text":
347
+ type = "str"
348
+ break
349
+ case "multipart": {
350
+ const files: string[] = []
351
+ const listFiles: string[] = []
352
+ const fields = b.parts.map((part) => {
353
+ const schema = part.schema ? model.resolve(part.schema) : ({ kind: "unknown" } as IRSchema)
354
+ if (part.type === "file") {
355
+ if (schema.kind === "array") listFiles.push(part.name)
356
+ else files.push(part.name)
357
+ return {
358
+ name: part.name,
359
+ required: false,
360
+ schema: { kind: "unknown" } as IRSchema,
361
+ file: true,
362
+ list: schema.kind === "array",
363
+ }
364
+ }
365
+ return {
366
+ file: false,
367
+ list: false,
368
+ name: part.name,
369
+ required: false,
370
+ schema: part.schema ?? ({ kind: "unknown" } as IRSchema),
371
+ }
372
+ })
373
+ const objectIR: Extract<IRSchema, { kind: "object" }> = {
374
+ fields: fields.map((f) => ({ name: f.name, required: f.required, schema: f.schema })),
375
+ kind: "object",
376
+ }
377
+ type = names.types.hoist(`${op.id}.multipart`, `${typeParent}Body`, objectIR)
378
+ /* file parts: re-render with the FileInput alias instead of Any */
379
+ const decl = names.decls.get(type) ?? ""
380
+ let patched = decl
381
+ for (const f of fields.filter((x) => x.file)) {
382
+ const key = pyString(f.name)
383
+ const t = f.list ? "list[FileInput]" : "FileInput"
384
+ patched = patched
385
+ .replace(` ${f.name}: NotRequired[Any]`, ` ${f.name}: NotRequired[${t}]`)
386
+ .replace(` ${key}: "NotRequired[Any]",`, ` ${key}: ${pyString(`NotRequired[${t}]`)},`)
387
+ }
388
+ names.decls.set(type, patched)
389
+ plan.multipart = { files, listFiles }
390
+ break
391
+ }
392
+ }
393
+ plan.body = { local: bodyLocal, required: b.required, type }
311
394
  }
312
-
313
- const kwArgs: string[] = []
314
-
315
- if (!op.bodyIsStream && op.bodySchema && !op.bodyRequired) {
316
- kwArgs.push(`body: ${bodyTypeName(op.bodySchema)} | None = None`)
395
+ if (op.stream === null) {
396
+ const s = op.success
397
+ if (s.kind === "json")
398
+ plan.returnType = s.schema ? irToPythonNamed(s.schema, names.types, typeParent, "Response") : "Any"
399
+ else if (s.kind === "text") plan.returnType = "str"
400
+ else if (s.kind === "binary") plan.returnType = "bytes"
401
+ else plan.returnType = "None"
317
402
  }
403
+ return plan
404
+ }
318
405
 
319
- if (op.isSSE) {
320
- kwArgs.push(`last_event_id: str | None = None`)
321
- }
406
+ function signature(plan: PyOpPlan, throwOnError: boolean, isAsync: boolean): string[] {
407
+ const { op } = plan
408
+ const defKw = op.stream === "ws" || op.stream === "realtime" || !isAsync ? "def" : "async def"
409
+ const pos: string[] = ["self", ...plan.pathParams.map((p) => `${p.local}: str`)]
410
+ const kw: string[] = []
322
411
 
323
- if (op.isWS) {
324
- kwArgs.push(`protocols: list[str] | None = None`)
412
+ if (op.stream === "realtime") {
413
+ kw.push(
414
+ `reconnect_token: str | None = None`,
415
+ `last_event_id: str | None = None`,
416
+ `headers: dict[str, str] | None = None`,
417
+ )
418
+ return emitSignature(defKw, plan.methodName, pos, kw, "ResumableConnection")
325
419
  }
326
420
 
327
- for (const qp of op.queryParams) {
328
- const pname = pyParamName(qp.name)
329
- const pyType = jsonSchemaToPy(qp.schema, 0)
330
- if (qp.required) {
331
- posArgs.push(`${pname}: ${pyType}`)
332
- } else {
333
- /* skip last-event-id as already handled above */
334
- if (op.isSSE && (qp.name === "last-event-id" || qp.name === "last_event_id")) continue
335
- kwArgs.push(`${pname}: ${pyType} | None = None`)
336
- }
421
+ if (plan.body) {
422
+ if (plan.body.required) pos.push(`${plan.body.local}: ${plan.body.type}`)
423
+ else kw.push(`${plan.body.local}: ${optional(plan.body.type)} = None`)
337
424
  }
338
-
339
- /* SSE/WS use config-level timeout/headers, not per-call */
340
- if (!op.isSSE && !op.isWS) {
341
- if (op.isIdempotent) kwArgs.push(`idempotency_key: str | None = None`)
342
- kwArgs.push(`timeout: float | None = None`)
343
- kwArgs.push(`headers: dict[str, str] | None = None`)
344
- kwArgs.push(`cancel_token: "threading.Event | None" = None`)
425
+ for (const q of [...plan.queryParams, ...plan.headerParams]) {
426
+ if (q.required) pos.push(`${q.local}: ${q.type}`)
427
+ else kw.push(`${q.local}: ${optional(q.type)} = None`)
345
428
  }
346
-
347
- let returnType: string
348
- if (op.isSSE) {
349
- returnType = "AsyncIterator[SSEEvent]"
350
- } else if (op.isWS) {
351
- returnType = "_TypedWebSocket"
352
- } else if (throwOnError) {
353
- returnType = op.successType
354
- } else {
355
- returnType = `SDKResult[${op.successType}]`
429
+ if (op.stream === "sse") kw.push(`last_event_id: str | None = None`, `headers: dict[str, str] | None = None`)
430
+ if (op.stream === "ws") {
431
+ kw.push(
432
+ `protocols: list[str] | None = None`,
433
+ `reconnect_token: str | None = None`,
434
+ `headers: dict[str, str] | None = None`,
435
+ )
356
436
  }
357
-
358
- if (kwArgs.length === 0) {
359
- lines.push(` ${defKw} ${methodName}(${posArgs.join(", ")}) -> ${returnType}:`)
360
- } else {
361
- const posLine = posArgs.join(", ")
362
- lines.push(` ${defKw} ${methodName}(${posLine},`)
363
- lines.push(` *,`)
364
- for (let i = 0; i < kwArgs.length; i++) {
365
- const comma = i < kwArgs.length - 1 ? "," : ""
366
- lines.push(` ${kwArgs[i]}${comma}`)
367
- }
368
- lines.push(` ) -> ${returnType}:`)
437
+ if (op.stream === null) {
438
+ if (op.idempotent) kw.push(`idempotency_key: str | None = None`)
439
+ kw.push(`timeout: float | None = None`, `headers: dict[str, str] | None = None`)
440
+ kw.push(`cancel_token: "threading.Event | None" = None`)
369
441
  }
370
442
 
371
- return lines
443
+ let ret: string
444
+ if (op.stream === "sse") ret = "AsyncIterator[SSEEvent]"
445
+ else if (op.stream === "ws") ret = "_TypedWebSocket"
446
+ else if (throwOnError) ret = plan.returnType
447
+ else ret = `SDKResult[${plan.returnType}]`
448
+ return emitSignature(defKw, plan.methodName, pos, kw, ret)
372
449
  }
373
450
 
374
- function buildMethodDocstring(op: OperationInfo): string[] {
375
- const lines: string[] = []
376
- const text = op.summary ?? op.description ?? ""
377
- if (!text && op.errorStatuses.length === 0) return lines
378
-
379
- if (op.errorStatuses.length === 0) {
380
- /* single-line docstring when no Raises section */
381
- lines.push(` """${text}"""`)
382
- return lines
383
- }
384
-
385
- lines.push(` """${text}`)
386
- lines.push(``)
387
- lines.push(` Raises:`)
388
- for (const status of op.errorStatuses) {
389
- const name = ERROR_NAMES[status] ?? "APIStatusError"
390
- lines.push(` ${name}: HTTP ${status}`)
391
- }
392
- lines.push(` """`)
393
- return lines
451
+ function optional(t: string): string {
452
+ return t.endsWith("| None") ? t : `${t} | None`
394
453
  }
395
454
 
396
- function buildUrlLine(op: OperationInfo, isTopLevel: boolean): string {
397
- const configRef = isTopLevel ? "self._config" : "self._client._config"
398
- if (op.pathParams.length > 0) {
399
- const urlExpr = op.path.replace(/\{(\w+)\}/g, (_, p) => `{quote(${pyParamName(p)}, safe='')}`)
400
- return ` url = _build_url(${configRef}.base_url, f"${urlExpr}")`
401
- }
402
- return ` url = _build_url(${configRef}.base_url, "${op.path}")`
455
+ function emitSignature(defKw: string, name: string, pos: string[], kw: string[], ret: string): string[] {
456
+ if (kw.length === 0) return [` ${defKw} ${name}(${pos.join(", ")}) -> ${ret}:`]
457
+ const l = [` ${defKw} ${name}(${pos.join(", ")},`, ` *,`]
458
+ kw.forEach((k, i) => l.push(` ${k}${i < kw.length - 1 ? "," : ""}`))
459
+ l.push(` ) -> ${ret}:`)
460
+ return l
403
461
  }
404
462
 
405
- function buildResponseLines(throwOnError: boolean): string[] {
406
- if (throwOnError) {
407
- return [` _raise_for_status(_response)`, ` return _parse_body(_response)`]
408
- }
409
- return [
410
- ` _body = _parse_body(_response)`,
411
- ` if _response.status_code >= 400:`,
412
- ` return SDKResult(data=None, error=_body, status=_response.status_code, response=_response)`,
413
- ` return SDKResult(data=_body, error=None, status=_response.status_code, response=_response)`,
414
- ]
463
+ function docstring(op: SdkOp): string[] {
464
+ const text = op.summary || op.description
465
+ const lines = text ? pyDocLines(text) : []
466
+ if (lines.length === 0 && op.errorStatuses.length === 0) return []
467
+ const out: string[] = []
468
+ if (op.errorStatuses.length === 0 && lines.length === 1) return [` """${lines[0]}"""`]
469
+ out.push(` """${lines[0] ?? ""}`)
470
+ for (const line of lines.slice(1)) out.push(line === "" ? "" : ` ${line}`)
471
+ if (op.errorStatuses.length > 0) {
472
+ out.push(``)
473
+ out.push(` Raises:`)
474
+ for (const status of op.errorStatuses)
475
+ out.push(` ${ERROR_NAMES[status] ?? "APIStatusError"}: HTTP ${status}`)
476
+ }
477
+ out.push(` """`)
478
+ return out
415
479
  }
416
480
 
417
- /* Build path-params dict literal string used by mark_stale / interpolate_path:
418
- * { "id": id, "orgId": org_id } — keys are the OpenAPI placeholder names,
419
- * values are the python-safe local identifiers from the method signature. */
420
- function buildPathParamsDict(op: OperationInfo): string {
421
- if (op.pathParams.length === 0) return "None"
422
- const entries = op.pathParams.map((p) => `"${p}": ${pyParamName(p)}`)
423
- return `{${entries.join(", ")}}`
481
+ function pathExpr(plan: PyOpPlan): string {
482
+ if (plan.pathParams.length === 0) return pyString(plan.op.path)
483
+ const dict = plan.pathParams.map((p) => `${pyString(p.wire)}: ${p.local}`).join(", ")
484
+ return `_expand_path(${pyString(plan.op.path)}, {${dict}})`
424
485
  }
425
486
 
426
- function buildInvalidateListLiteral(op: OperationInfo): string {
427
- if (op.invalidate.length === 0) return "[]"
428
- return `[${op.invalidate.map((s) => `"${s}"`).join(", ")}]`
487
+ function paramsDict(plan: PyOpPlan): string {
488
+ const entries = plan.queryParams.map((q) => `${pyString(q.wire)}: ${q.local}`)
489
+ return `{${entries.join(", ")}}`
429
490
  }
430
491
 
431
- /* Emit Idempotency-Key injection block. Precedence:
432
- * explicit header (any casing) > idempotency_key kwarg > uuid.uuid4().
433
- * Runs after _headers is built so config.headers + per-call headers merge is visible. */
434
- function buildIdempotencyLines(op: OperationInfo): string[] {
435
- if (!op.isIdempotent || op.isSSE || op.isWS || op.isRealtime) return []
436
- return [
437
- ` _idem_existing = None`,
438
- ` for _k in list(_headers.keys()):`,
439
- ` if _k.lower() == "idempotency-key":`,
440
- ` _idem_existing = _headers[_k]`,
441
- ` break`,
442
- ` if _idem_existing is None:`,
443
- ` _headers["Idempotency-Key"] = idempotency_key if idempotency_key is not None else str(uuid.uuid4())`,
444
- ]
492
+ function headerLines(plan: PyOpPlan, configRef: string, authRef: string, base: string): string[] {
493
+ const l: string[] = []
494
+ l.push(` _extra: dict[str, str] = ${base}`)
495
+ for (const h of plan.headerParams) {
496
+ l.push(` if ${h.local} is not None:`)
497
+ l.push(` _extra[${pyString(h.wire)}] = _format_value(${h.local})`)
498
+ }
499
+ l.push(` _extra.update(headers or {})`)
500
+ l.push(` _headers = _build_headers(${configRef}, extra=_extra, bearer_token=${authRef}.token)`)
501
+ return l
445
502
  }
446
503
 
447
- function buildAsyncMethodBody(op: OperationInfo, throwOnError: boolean, isTopLevel = false): string[] {
448
- const lines: string[] = []
449
- const configRef = isTopLevel ? "self._config" : "self._client._config"
450
- const asyncClientRef = isTopLevel ? "self._async_client" : "self._client._async_client"
451
- const trackerRef = isTopLevel ? "self._stale_tracker" : "self._client._stale_tracker"
452
-
453
- if (op.isRealtime) {
454
- lines.push(` _transports = [WsAdapter(), SseAdapter(), LongpollAdapter()]`)
455
- lines.push(` _opts = TransportOpts(`)
456
- lines.push(` reconnect_token=reconnect_token,`)
457
- lines.push(` last_event_id=last_event_id,`)
458
- lines.push(` headers=headers,`)
459
- lines.push(` )`)
460
- lines.push(` _url = _build_url(${configRef}.base_url, "${op.path}")`)
461
- lines.push(` return ResumableConnection(_url, _transports, _opts)`)
462
- return lines
504
+ function bodyArgs(plan: PyOpPlan, l: string[]): string {
505
+ const b = plan.op.body
506
+ const local = plan.body?.local
507
+ if (!b || !local) return ""
508
+ switch (b.kind) {
509
+ case "json":
510
+ return `json=${local}`
511
+ case "form":
512
+ l.push(` _headers.setdefault("Content-Type", "application/x-www-form-urlencoded")`)
513
+ return `content=_form_content(${local})`
514
+ case "text":
515
+ l.push(` _set_header(_headers, "Content-Type", ${pyString(b.contentType)})`)
516
+ return `content=${local}.encode() if ${local} is not None else None`
517
+ case "binary":
518
+ l.push(` _set_header(_headers, "Content-Type", ${pyString(b.contentType)})`)
519
+ return `content=${local}`
520
+ case "multipart": {
521
+ const files = plan.multipart?.files ?? []
522
+ const listFiles = plan.multipart?.listFiles ?? []
523
+ l.push(
524
+ ` _data, _files = _multipart_parts(${local}, files=[${files.map(pyString).join(", ")}], list_files=[${listFiles.map(pyString).join(", ")}])`,
525
+ )
526
+ return `data=_data, files=_files`
527
+ }
463
528
  }
529
+ }
464
530
 
465
- lines.push(buildUrlLine(op, isTopLevel))
466
-
467
- if (op.isSSE) {
468
- lines.push(` _headers = _build_headers(${configRef}, extra={"Accept": "text/event-stream"})`)
469
- lines.push(` if last_event_id is not None: _headers["Last-Event-ID"] = last_event_id`)
470
- } else if (op.isWS) {
471
- lines.push(` _headers = _build_headers(${configRef}, extra=None)`)
472
- } else if (op.bodyIsStream) {
473
- lines.push(
474
- ` _headers = _build_headers(${configRef}, extra={"Content-Type": "application/octet-stream", **(headers or {})})`,
531
+ function methodBody(plan: PyOpPlan, throwOnError: boolean, isAsync: boolean, isTopLevel: boolean): string[] {
532
+ const l: string[] = []
533
+ const { op } = plan
534
+ const self = isTopLevel ? "self" : "self._client"
535
+ const configRef = `${self}._config`
536
+ const authRef = `${self}._auth`
537
+ const clientRef = isAsync ? `${self}._async_client` : `${self}._sync_client`
538
+ const trackerRef = `${self}._stale_tracker`
539
+ const aw = isAsync ? "await " : ""
540
+
541
+ if (op.stream === "realtime") {
542
+ l.push(` _url = _build_url(${configRef}.base_url, ${pathExpr(plan)})`)
543
+ l.push(` _opts = TransportOpts(`)
544
+ l.push(` reconnect_token=reconnect_token,`)
545
+ l.push(` last_event_id=last_event_id,`)
546
+ l.push(` headers=_build_headers(${configRef}, extra=headers, bearer_token=${authRef}.token),`)
547
+ l.push(` )`)
548
+ l.push(` _transports: list[Any] = [WsAdapter(), SseAdapter(), LongpollAdapter()]`)
549
+ l.push(` return ResumableConnection(_url, _transports, _opts)`)
550
+ return l
551
+ }
552
+
553
+ l.push(` _path = ${pathExpr(plan)}`)
554
+
555
+ if (op.stream === "ws") {
556
+ l.push(` _params: dict[str, Any] = ${paramsDict(plan)}`)
557
+ l.push(` if reconnect_token is not None:`)
558
+ l.push(` _params["reconnect_token"] = reconnect_token`)
559
+ l.push(...headerLines(plan, configRef, authRef, "{}"))
560
+ l.push(` _url = _build_url(${configRef}.base_url, _path, _params)`)
561
+ l.push(` _ws_url = _to_ws_url(_url)`)
562
+ l.push(` return _TypedWebSocket(_ws_url, protocols=protocols, extra_headers=_headers)`)
563
+ return l
564
+ }
565
+
566
+ if (op.stream === "sse") {
567
+ l.push(` _params: dict[str, Any] = ${paramsDict(plan)}`)
568
+ l.push(...headerLines(plan, configRef, authRef, `{"Accept": "text/event-stream"}`))
569
+ l.push(` if last_event_id is not None:`)
570
+ l.push(` _headers["Last-Event-ID"] = last_event_id`)
571
+ const args = bodyArgs(plan, l)
572
+ l.push(` _url = _build_url(${configRef}.base_url, _path, _params)`)
573
+ l.push(
574
+ ` async with _open_stream(${clientRef}, ${configRef}, ${pyString(op.method)}, _url, _headers${args ? `, ${args}` : ""}) as _response:`,
575
+ )
576
+ l.push(` async for _event in parse_sse_stream(_response):`)
577
+ l.push(` yield _event`)
578
+ return l
579
+ }
580
+
581
+ l.push(` _params: dict[str, Any] = ${paramsDict(plan)}`)
582
+ l.push(...headerLines(plan, configRef, authRef, "{}"))
583
+ if (op.idempotent) {
584
+ /* explicit header (any casing) > idempotency_key kwarg > uuid4 */
585
+ l.push(` if not _has_header(_headers, "Idempotency-Key"):`)
586
+ l.push(
587
+ ` _headers["Idempotency-Key"] = idempotency_key if idempotency_key is not None else str(uuid.uuid4())`,
475
588
  )
476
- } else {
477
- lines.push(` _headers = _build_headers(${configRef}, extra=headers)`)
478
589
  }
479
- lines.push(...buildIdempotencyLines(op))
480
-
481
- /* SSE skips last-event-id query param — sent as header instead */
482
- const qpNames = op.queryParams
483
- .filter((qp) => !(op.isSSE && (qp.name === "last-event-id" || qp.name === "last_event_id")))
484
- .map((qp) => `"${qp.name}": ${pyParamName(qp.name)}`)
485
- if (qpNames.length > 0) {
486
- lines.push(` _params: dict[str, Any] = {${qpNames.join(", ")}}`)
590
+ const args = bodyArgs(plan, l)
591
+ const dictEntries = plan.pathParams.map((p) => `${pyString(p.wire)}: ${p.local}`)
592
+ l.push(
593
+ ` _path_params: dict[str, str] | None = ${dictEntries.length > 0 ? `{${dictEntries.join(", ")}}` : "None"}`,
594
+ )
595
+ l.push(` _selector = ${pyString(`${op.method} `)} + _path`)
596
+ l.push(` _request_meta = ${aw}${trackerRef}.build_request_meta(_selector, _path, ${pyString(op.method)})`)
597
+ l.push(` _url = _build_url(${configRef}.base_url, _path, _params)`)
598
+ l.push(` _response = ${aw}${isAsync ? "_do_request_async" : "_do_request_sync"}(`)
599
+ l.push(` ${clientRef}, ${configRef}, ${pyString(op.method)}, _url,`)
600
+ l.push(` headers=_headers,${args ? ` ${args},` : ""} timeout=timeout,`)
601
+ l.push(` cancel_token=cancel_token, operation=${pyString(`${op.method} ${op.path}`)},`)
602
+ l.push(` request_meta=_request_meta, auth=${authRef},`)
603
+ l.push(` )`)
604
+ l.push(` if 200 <= _response.status_code < 300:`)
605
+ const targets = op.invalidates.map(pyString).join(", ")
606
+ l.push(` ${aw}${trackerRef}.mark_stale([${targets}], _path_params, _selector)`)
607
+ l.push(` if _request_meta is not None and _request_meta.is_stale:`)
608
+ l.push(
609
+ ` ${aw}${trackerRef}.clear_stale(_selector, _path, ${pyString(op.method)}, _request_meta.seq_snapshot)`,
610
+ )
611
+ const kind = op.success.kind
612
+ if (throwOnError) {
613
+ l.push(` _raise_for_status(_response)`)
614
+ l.push(` return _parse_body(_response, ${pyString(kind)})`)
487
615
  } else {
488
- lines.push(` _params: dict[str, Any] = {}`)
489
- }
490
-
491
- if (op.isWS) {
492
- lines.push(` ws_url = url.replace("https://", "wss://").replace("http://", "ws://")`)
493
- if (op.queryParams.some((qp) => qp.name === "reconnect_token")) {
494
- lines.push(` if reconnect_token is not None:`)
495
- lines.push(` _params["reconnect_token"] = reconnect_token`)
496
- }
497
- lines.push(` if _params:`)
498
- lines.push(` from urllib.parse import urlencode`)
499
- lines.push(` ws_url = f"{ws_url}?{urlencode({k: v for k, v in _params.items() if v is not None})}"`)
500
- lines.push(` return _TypedWebSocket(ws_url, protocols=protocols, extra_headers=_headers)`)
501
- return lines
502
- }
503
-
504
- if (op.isSSE) {
505
- lines.push(` async with ${asyncClientRef}.stream(`)
506
- lines.push(` "${op.method}", url, headers=_headers,`)
507
- lines.push(` params={k: v for k, v in _params.items() if v is not None} or None,`)
508
- lines.push(` ) as _response:`)
509
- lines.push(` _raise_for_status(_response)`)
510
- lines.push(` async for _event in parse_sse_stream(_response):`)
511
- lines.push(` yield _event`)
512
- return lines
616
+ l.push(` if not 200 <= _response.status_code < 300:`)
617
+ l.push(
618
+ ` return SDKResult(data=None, error=_parse_body(_response), status=_response.status_code, response=_response)`,
619
+ )
620
+ l.push(
621
+ ` return SDKResult(data=_parse_body(_response, ${pyString(kind)}), error=None, status=_response.status_code, response=_response)`,
622
+ )
513
623
  }
514
-
515
- const pathParamsDict = buildPathParamsDict(op)
516
- const invalidateLiteral = buildInvalidateListLiteral(op)
517
- lines.push(` _path_params: dict[str, str] | None = ${pathParamsDict}`)
518
- lines.push(` _concrete_path = interpolate_path("${op.path}", _path_params)`)
519
- lines.push(` _concrete_selector = "${op.method} " + _concrete_path`)
520
- lines.push(` _request_meta = await ${trackerRef}.build_request_meta(`)
521
- lines.push(` _concrete_selector, _concrete_path, "${op.method}",`)
522
- lines.push(` )`)
523
-
524
- let bodyArg: string
525
- if (op.bodyIsStream) bodyArg = "json=None, content=body"
526
- else if (op.bodySchema) bodyArg = "json=body"
527
- else bodyArg = "json=None"
528
- lines.push(` _response = await _do_request_async(`)
529
- lines.push(` ${asyncClientRef}, ${configRef}, "${op.method}", url,`)
530
- lines.push(` headers=_headers, ${bodyArg}, params=_params, timeout=timeout,`)
531
- lines.push(` cancel_token=cancel_token, operation="${op.method} ${op.path}",`)
532
- lines.push(` request_meta=_request_meta,`)
533
- lines.push(` )`)
534
- lines.push(` if _response.status_code < 400:`)
535
- lines.push(` await ${trackerRef}.mark_stale(`)
536
- lines.push(` ${invalidateLiteral}, _path_params, _concrete_selector,`)
537
- lines.push(` )`)
538
- lines.push(` if _request_meta is not None and _request_meta.is_stale:`)
539
- lines.push(` await ${trackerRef}.clear_stale(`)
540
- lines.push(` _concrete_selector, _concrete_path, "${op.method}",`)
541
- lines.push(` _request_meta.seq_snapshot,`)
542
- lines.push(` )`)
543
- lines.push(...buildResponseLines(throwOnError))
544
-
545
- return lines
624
+ return l
546
625
  }
547
626
 
548
- function buildSyncMethodBody(op: OperationInfo, throwOnError: boolean, isTopLevel = false): string[] {
549
- const lines: string[] = []
550
- const configRef = isTopLevel ? "self._config" : "self._client._config"
551
- const syncClientRef = isTopLevel ? "self._sync_client" : "self._client._sync_client"
552
- const trackerRef = isTopLevel ? "self._stale_tracker" : "self._client._stale_tracker"
553
-
554
- lines.push(buildUrlLine(op, isTopLevel))
555
- lines.push(` _headers = _build_headers(${configRef}, extra=headers)`)
556
- lines.push(...buildIdempotencyLines(op))
557
-
558
- const qpNames = op.queryParams.map((qp) => `"${qp.name}": ${pyParamName(qp.name)}`)
559
- if (qpNames.length > 0) {
560
- lines.push(` _params: dict[str, Any] = {${qpNames.join(", ")}}`)
561
- } else {
562
- lines.push(` _params: dict[str, Any] = {}`)
563
- }
564
-
565
- const pathParamsDict = buildPathParamsDict(op)
566
- const invalidateLiteral = buildInvalidateListLiteral(op)
567
- lines.push(` _path_params: dict[str, str] | None = ${pathParamsDict}`)
568
- lines.push(` _concrete_path = interpolate_path("${op.path}", _path_params)`)
569
- lines.push(` _concrete_selector = "${op.method} " + _concrete_path`)
570
- lines.push(` _request_meta = ${trackerRef}.build_request_meta(`)
571
- lines.push(` _concrete_selector, _concrete_path, "${op.method}",`)
572
- lines.push(` )`)
573
-
574
- const jsonArg = op.bodySchema ? "json=body" : "json=None"
575
- lines.push(` _response = _do_request_sync(`)
576
- lines.push(` ${syncClientRef}, ${configRef}, "${op.method}", url,`)
577
- lines.push(` headers=_headers, ${jsonArg}, params=_params, timeout=timeout,`)
578
- lines.push(` cancel_token=cancel_token, operation="${op.method} ${op.path}",`)
579
- lines.push(` request_meta=_request_meta,`)
580
- lines.push(` )`)
581
- lines.push(` if _response.status_code < 400:`)
582
- lines.push(` ${trackerRef}.mark_stale(`)
583
- lines.push(` ${invalidateLiteral}, _path_params, _concrete_selector,`)
584
- lines.push(` )`)
585
- lines.push(` if _request_meta is not None and _request_meta.is_stale:`)
586
- lines.push(` ${trackerRef}.clear_stale(`)
587
- lines.push(` _concrete_selector, _concrete_path, "${op.method}",`)
588
- lines.push(` _request_meta.seq_snapshot,`)
589
- lines.push(` )`)
590
- lines.push(...buildResponseLines(throwOnError))
591
-
592
- return lines
627
+ type PyClassNode = {
628
+ asyncName: string
629
+ syncName?: string
630
+ path: string[]
631
+ ns: IRNamespace
632
+ children: Array<{ attr: string; node: PyClassNode }>
633
+ plans: PyOpPlan[]
593
634
  }
594
635
 
595
- /* ── buildPyTypes ── */
596
-
597
- function buildPyTypes(spec: Record<string, unknown>): string {
598
- const schemas = (spec.components as Record<string, unknown> | undefined)?.schemas as
599
- | Record<string, Record<string, unknown>>
600
- | undefined
601
-
602
- const body: string[] = []
603
- const allNames: string[] = []
604
-
605
- if (schemas) {
606
- for (const [name, schema] of Object.entries(schemas).sort(([a], [b]) => a.localeCompare(b))) {
607
- allNames.push(name)
608
- const props = schema.properties as Record<string, Record<string, unknown>> | undefined
609
- const required = new Set((schema.required as string[] | undefined) ?? [])
610
-
611
- if (props && Object.keys(props).length > 0) {
612
- body.push(`class ${name}(TypedDict, total=False):`)
613
- for (const [key, val] of Object.entries(props).sort(([a], [b]) => a.localeCompare(b))) {
614
- const pyType = jsonSchemaToPy(val, 0)
615
- if (required.has(key)) {
616
- body.push(` ${key}: ${pyType}`)
617
- } else {
618
- body.push(` ${key}: NotRequired[${pyType}]`)
619
- }
620
- }
621
- body.push("")
622
- } else if (schema.type === "string" && schema.format === "uuid") {
623
- body.push(`${name} = NewType("${name}", str)`)
624
- body.push("")
625
- } else {
626
- body.push(`${name} = NewType("${name}", Any) # type: ignore[misc]`)
627
- body.push("")
628
- }
629
- }
630
- }
631
-
632
- const bodyText = body.join("\n")
633
- const typingImports = new Set<string>(["TypedDict", "Literal", "NotRequired", "NewType"])
634
- if (!bodyText.includes("NewType(")) typingImports.delete("NewType")
635
- if (!/\bAny\b/.test(bodyText)) typingImports.delete("Any")
636
- else typingImports.add("Any")
637
- if (!/\bLiteral\b/.test(bodyText)) typingImports.delete("Literal")
638
- if (!/\bNotRequired\b/.test(bodyText)) typingImports.delete("NotRequired")
639
- if (!/\bTypedDict\b/.test(bodyText)) typingImports.delete("TypedDict")
636
+ function isSyncCapable(op: SdkOp): boolean {
637
+ return op.stream === null && op.body?.kind !== "binary"
638
+ }
640
639
 
641
- const l: string[] = []
642
- l.push("# ruff: noqa: E501")
643
- l.push(PYTHON_FUTURE)
644
- l.push("")
645
- if (typingImports.size > 0) {
646
- const sorted = [...typingImports].sort()
647
- l.push(`from typing import ${sorted.join(", ")}`)
648
- l.push("")
649
- }
650
- if (bodyText.length > 0) {
651
- l.push(bodyText)
652
- }
640
+ function hasSyncContent(node: PyClassNode): boolean {
641
+ return node.plans.some((p) => isSyncCapable(p.op)) || node.children.some((c) => hasSyncContent(c.node))
642
+ }
653
643
 
654
- if (allNames.length === 0) {
655
- l.push("__all__: list[str] = []")
656
- } else {
657
- l.push(`__all__ = [${allNames.map((n) => `"${n}"`).join(", ")}]`)
658
- }
659
- return l.join("\n")
644
+ function trackerInit(cls: string): string[] {
645
+ return [
646
+ " _inv = config.invalidation",
647
+ ` self._stale_tracker = ${cls}(`,
648
+ " stale_time=_inv.stale_time if _inv is not None else 0.0,",
649
+ " stale_max_entries=(",
650
+ " _inv.stale_max_entries if _inv is not None else 1000",
651
+ " ),",
652
+ " max_sources_per_target=(",
653
+ " _inv.max_sources_per_target if _inv is not None else 16",
654
+ " ),",
655
+ " )",
656
+ ]
660
657
  }
661
658
 
662
659
  function buildPyClient(
663
- spec: Record<string, unknown>,
660
+ model: SdkModel,
661
+ names: PyNames,
664
662
  options: PySDKOptions,
665
663
  serviceMap: Record<string, ServiceMapEntry>,
666
664
  ): string {
667
665
  const throwOnError = options.throwOnError !== false
668
- const { opsByOpId, topLevel, tree } = collectSDKMethods(spec)
669
-
670
- const schemas = (spec.components as Record<string, unknown> | undefined)?.schemas as
671
- | Record<string, Record<string, unknown>>
672
- | undefined
673
- const schemaNames = schemas ? Object.keys(schemas) : []
666
+ function planNs(ns: IRNamespace, path: string[], reserved: string[]): PyClassNode {
667
+ const members = new NameScope(reserved)
668
+ const node: PyClassNode = {
669
+ asyncName: path.length === 0 ? "AsyncSDK" : names.module.claim(`_${path.map(pyClassName).join("")}Resource`),
670
+ children: [],
671
+ ns,
672
+ path,
673
+ plans: [],
674
+ }
675
+ if (path.length > 0) node.syncName = names.module.claim(`_Sync${path.map(pyClassName).join("")}Resource`)
676
+ for (const [seg, childNs] of namespacesOf(ns)) {
677
+ const attr = members.claim(pyIdent(seg))
678
+ node.children.push({ attr, node: planNs(childNs, [...path, seg], ["_client"]) })
679
+ }
680
+ for (const [seg, irOp] of methodsOf(ns)) {
681
+ const op = model.opsById.get(irOp.id)
682
+ if (!op) continue
683
+ let base = pyIdentifier(seg)
684
+ if (op.stream === "realtime") {
685
+ /* realtime methods keep the `connectX` name they always had */
686
+ const ident = pyIdentifier(seg)
687
+ base = /^connect(_|$)/.test(ident) ? ident : `connect${ident.charAt(0).toUpperCase()}${ident.slice(1)}`
688
+ }
689
+ node.plans.push(planPyOp(op, members.claim(base), names, model))
690
+ }
691
+ return node
692
+ }
693
+ const root = planNs(
694
+ model.ir.tree,
695
+ [],
696
+ ["_config", "_auth", "_async_client", "_sync_client", "_stale_tracker", "is_stale", "aclose", "close"],
697
+ )
674
698
 
675
699
  const body: string[] = []
676
-
677
700
  const invEntries = Object.entries(serviceMap).filter(([, v]) => v.invalidate && v.invalidate.length > 0)
678
-
679
701
  body.push("_INVALIDATION_MAP: dict[str, dict[str, Any]] = {")
680
702
  for (const [opId, entry] of invEntries) {
681
- const invItems = (entry.invalidate ?? []).map((s) => `"${s}"`).join(", ")
682
- body.push(` "${opId}": {`)
683
- body.push(` "method": "${entry.method}",`)
684
- body.push(` "path": "${entry.path}",`)
685
- body.push(` "invalidate": [${invItems}],`)
703
+ body.push(` ${pyString(opId)}: {`)
704
+ body.push(` "method": ${pyString(entry.method)},`)
705
+ body.push(` "path": ${pyString(entry.path)},`)
706
+ body.push(` "invalidate": [${(entry.invalidate ?? []).map(pyString).join(", ")}],`)
686
707
  body.push(` },`)
687
708
  }
688
709
  body.push("}")
689
710
  body.push("")
690
711
 
691
- /* emit async resource classes in post-order (children before parents) */
692
- function emitAsyncResourceClasses(ns: IRNamespace, path: string[]): void {
693
- /* recurse into sub-namespaces first (post-order so children declared before parents) */
694
- for (const [seg, childNs] of namespacesOf(ns)) {
695
- emitAsyncResourceClasses(childNs, [...path, seg])
696
- }
697
-
698
- if (path.length === 0) return
699
-
700
- const className = nestedClassName(path)
701
- const parentType = path.length === 1 ? '"AsyncSDK"' : nestedClassName(path.slice(0, -1))
702
- body.push(`class ${className}:`)
703
- body.push(` def __init__(self, _client: ${parentType}) -> None:`)
712
+ /* children before parents so class names resolve at definition time */
713
+ /* every resource holds the root SDK: nested resources reach config and clients directly */
714
+ function emitAsync(node: PyClassNode): void {
715
+ for (const c of node.children) emitAsync(c.node)
716
+ if (node.path.length === 0) return
717
+ body.push(`class ${node.asyncName}:`)
718
+ body.push(` def __init__(self, _client: "AsyncSDK") -> None:`)
704
719
  body.push(` self._client = _client`)
705
-
706
- for (const [seg] of namespacesOf(ns)) {
707
- const childClass = nestedClassName([...path, seg])
708
- body.push(` self.${pyIdent(seg)} = ${childClass}(self)`)
709
- }
720
+ for (const c of node.children) body.push(` self.${c.attr} = ${c.node.asyncName}(_client)`)
710
721
  body.push("")
711
-
712
- for (const [, op] of methodsOf(ns)) {
713
- const opInfo = opsByOpId.get(op.id)
714
- if (!opInfo) continue
715
- body.push(...buildMethodSignature(opInfo, throwOnError, true))
716
- body.push(...buildMethodDocstring(opInfo))
717
- body.push(...buildAsyncMethodBody(opInfo, throwOnError))
722
+ for (const plan of node.plans) {
723
+ body.push(...signature(plan, throwOnError, true))
724
+ body.push(...docstring(plan.op))
725
+ body.push(...methodBody(plan, throwOnError, true, false))
718
726
  body.push("")
719
727
  }
720
728
  body.push("")
721
729
  }
722
730
 
723
- /* emit sync resource classes in post-order */
724
- function emitSyncResourceClasses(ns: IRNamespace, path: string[]): void {
725
- for (const [seg, childNs] of namespacesOf(ns)) {
726
- emitSyncResourceClasses(childNs, [...path, seg])
727
- }
728
-
729
- if (path.length === 0) return
730
-
731
- const syncMethods = methodsOf(ns).filter(([, op]) => {
732
- const opInfo = opsByOpId.get(op.id)
733
- return opInfo && !opInfo.isSSE && !opInfo.isWS && !opInfo.isRealtime && !opInfo.bodyIsStream
734
- })
735
- /* check if any sub-namespace has sync methods */
736
- const hasAnySyncContent = syncMethods.length > 0 || namespacesOf(ns).length > 0
737
- if (!hasAnySyncContent) return
738
-
739
- const className = nestedClassName(path)
740
- const syncClassName = `_Sync${className.slice(1)}`
741
- const parentType = path.length === 1 ? '"SDK"' : `_Sync${nestedClassName(path.slice(0, -1)).slice(1)}`
742
- body.push(`class ${syncClassName}:`)
743
- body.push(` def __init__(self, _client: ${parentType}) -> None:`)
731
+ function emitSync(node: PyClassNode): void {
732
+ for (const c of node.children) emitSync(c.node)
733
+ if (node.path.length === 0 || !hasSyncContent(node)) return
734
+ body.push(`class ${node.syncName}:`)
735
+ body.push(` def __init__(self, _client: "SDK") -> None:`)
744
736
  body.push(` self._client = _client`)
745
-
746
- for (const [seg, childNs] of namespacesOf(ns)) {
747
- const childClass = `_Sync${nestedClassName([...path, seg]).slice(1)}`
748
- const childSyncMethods = methodsOf(childNs).filter(([, op]) => {
749
- const opInfo = opsByOpId.get(op.id)
750
- return opInfo && !opInfo.isSSE && !opInfo.isWS && !opInfo.isRealtime && !opInfo.bodyIsStream
751
- })
752
- if (childSyncMethods.length > 0 || namespacesOf(childNs).length > 0) {
753
- body.push(` self.${pyIdent(seg)} = ${childClass}(self)`)
754
- }
737
+ for (const c of node.children) {
738
+ if (hasSyncContent(c.node)) body.push(` self.${c.attr} = ${c.node.syncName}(_client)`)
755
739
  }
756
740
  body.push("")
757
-
758
- for (const [, op] of syncMethods) {
759
- const opInfo = opsByOpId.get(op.id)
760
- if (!opInfo) continue
761
- body.push(...buildMethodSignature(opInfo, throwOnError, false))
762
- body.push(...buildMethodDocstring(opInfo))
763
- body.push(...buildSyncMethodBody(opInfo, throwOnError))
741
+ for (const plan of node.plans.filter((p) => isSyncCapable(p.op))) {
742
+ body.push(...signature(plan, throwOnError, false))
743
+ body.push(...docstring(plan.op))
744
+ body.push(...methodBody(plan, throwOnError, false, false))
764
745
  body.push("")
765
746
  }
766
747
  body.push("")
767
748
  }
768
749
 
769
- emitAsyncResourceClasses(tree, [])
770
- emitSyncResourceClasses(tree, [])
750
+ emitAsync(root)
751
+ emitSync(root)
771
752
 
772
753
  body.push("class SDK:")
773
754
  body.push(" def __init__(self, config: ClientConfig) -> None:")
774
755
  body.push(" self._config = config")
756
+ body.push(" self._auth = _AuthState(config.bearer_token)")
775
757
  body.push(" self._sync_client = httpx.Client(")
776
758
  body.push(" transport=config.sync_transport,")
777
759
  body.push(" )")
778
- body.push(" _inv = config.invalidation")
779
- body.push(" self._stale_tracker = _StaleTrackerSync(")
780
- body.push(" stale_time=_inv.stale_time if _inv is not None else 0.0,")
781
- body.push(" stale_max_entries=(")
782
- body.push(" _inv.stale_max_entries if _inv is not None else 1000")
783
- body.push(" ),")
784
- body.push(" max_sources_per_target=(")
785
- body.push(" _inv.max_sources_per_target if _inv is not None else 16")
786
- body.push(" ),")
787
- body.push(" )")
788
- for (const [seg] of namespacesOf(tree)) {
789
- const className = `_Sync${nestedClassName([seg]).slice(1)}`
790
- body.push(` self.${pyIdent(seg)} = ${className}(self)`)
760
+ body.push(...trackerInit("_StaleTrackerSync"))
761
+ for (const c of root.children) {
762
+ if (hasSyncContent(c.node)) body.push(` self.${c.attr} = ${c.node.syncName}(self)`)
791
763
  }
792
764
  body.push("")
765
+ body.push(" def close(self) -> None:")
766
+ body.push(' """Closes the underlying HTTP client."""')
767
+ body.push(" self._sync_client.close()")
768
+ body.push("")
793
769
  body.push(" def is_stale(self, method: str, path: str) -> bool:")
794
770
  body.push(' """Returns True when (method, path) sits inside an active stale window."""')
795
771
  body.push(" return self._stale_tracker.is_stale(method, path)")
796
772
  body.push("")
797
-
798
- const syncTopLevel = topLevel.filter((op) => !op.isSSE && !op.isWS && !op.isRealtime && !op.bodyIsStream)
799
- for (const op of syncTopLevel.sort((a, b) => a.operationId.localeCompare(b.operationId))) {
800
- body.push(...buildMethodSignature(op, throwOnError, false))
801
- body.push(...buildMethodDocstring(op))
802
- const bodyLines = buildSyncMethodBody(op, throwOnError, true)
803
- body.push(...bodyLines)
773
+ for (const plan of root.plans.filter((p) => isSyncCapable(p.op))) {
774
+ body.push(...signature(plan, throwOnError, false))
775
+ body.push(...docstring(plan.op))
776
+ body.push(...methodBody(plan, throwOnError, false, true))
804
777
  body.push("")
805
778
  }
806
-
807
779
  body.push("")
808
780
 
809
781
  body.push("class AsyncSDK:")
810
782
  body.push(" def __init__(self, config: ClientConfig) -> None:")
811
783
  body.push(" self._config = config")
784
+ body.push(" self._auth = _AuthState(config.bearer_token)")
812
785
  body.push(" self._async_client = httpx.AsyncClient(")
813
786
  body.push(" transport=config.transport,")
814
787
  body.push(" )")
815
- body.push(" _inv = config.invalidation")
816
- body.push(" self._stale_tracker = _StaleTracker(")
817
- body.push(" stale_time=_inv.stale_time if _inv is not None else 0.0,")
818
- body.push(" stale_max_entries=(")
819
- body.push(" _inv.stale_max_entries if _inv is not None else 1000")
820
- body.push(" ),")
821
- body.push(" max_sources_per_target=(")
822
- body.push(" _inv.max_sources_per_target if _inv is not None else 16")
823
- body.push(" ),")
824
- body.push(" )")
825
- for (const [seg] of namespacesOf(tree)) {
826
- const className = nestedClassName([seg])
827
- body.push(` self.${pyIdent(seg)} = ${className}(self)`)
828
- }
788
+ body.push(...trackerInit("_StaleTracker"))
789
+ for (const c of root.children) body.push(` self.${c.attr} = ${c.node.asyncName}(self)`)
790
+ body.push("")
791
+ body.push(" async def aclose(self) -> None:")
792
+ body.push(' """Closes the underlying HTTP client."""')
793
+ body.push(" await self._async_client.aclose()")
829
794
  body.push("")
830
795
  body.push(" async def is_stale(self, method: str, path: str) -> bool:")
831
796
  body.push(' """Returns True when (method, path) sits inside an active stale window."""')
832
797
  body.push(" return await self._stale_tracker.is_stale(method, path)")
833
798
  body.push("")
834
-
835
- for (const op of topLevel.sort((a, b) => a.operationId.localeCompare(b.operationId))) {
836
- body.push(...buildMethodSignature(op, throwOnError, true))
837
- body.push(...buildMethodDocstring(op))
838
- const bodyLines = buildAsyncMethodBody(op, throwOnError, true)
839
- body.push(...bodyLines)
799
+ for (const plan of root.plans) {
800
+ body.push(...signature(plan, throwOnError, true))
801
+ body.push(...docstring(plan.op))
802
+ body.push(...methodBody(plan, throwOnError, true, true))
840
803
  body.push("")
841
804
  }
842
805
 
843
- const bodyText = body.join("\n")
806
+ return body.join("\n")
807
+ }
808
+
809
+ function clientFile(bodyText: string, names: PyNames): string {
810
+ const uses = (sym: string) => new RegExp(`(?<![\\w.])${sym.replace(/[.]/g, "\\.")}\\b`).test(bodyText)
844
811
  const imports: string[] = []
845
- if (/\basyncio\b/.test(bodyText)) imports.push("import asyncio")
846
812
  if (/\bthreading\b/.test(bodyText)) imports.push("import threading")
847
813
  if (/\buuid\.uuid4\b/.test(bodyText)) imports.push("import uuid")
848
814
  imports.push("import httpx")
849
- if (/\bquote\b/.test(bodyText)) imports.push("from urllib.parse import quote")
850
-
851
- const typingUsed: string[] = []
852
- if (/\bAny\b/.test(bodyText)) typingUsed.push("Any")
853
- if (/\bAsyncIterator\b/.test(bodyText)) typingUsed.push("AsyncIterator")
854
- if (/\bLiteral\b/.test(bodyText)) typingUsed.push("Literal")
855
- if (/\bNotRequired\b/.test(bodyText)) typingUsed.push("NotRequired")
856
- if (/\bTypedDict\b/.test(bodyText)) typingUsed.push("TypedDict")
857
- if (typingUsed.length > 0) imports.push(`from typing import ${typingUsed.join(", ")}`)
858
-
859
- const runtimeUsed: string[] = []
860
- for (const sym of [
861
- "ClientConfig",
862
- "InvalidationConfig",
863
- "SDKResult",
864
- "_build_headers",
865
- "_build_url",
866
- "_do_request_async",
867
- "_do_request_sync",
868
- "_parse_body",
869
- "_raise_for_status",
870
- ]) {
871
- if (new RegExp(`\\b${sym}\\b`).test(bodyText)) runtimeUsed.push(sym)
872
- }
873
- if (runtimeUsed.length > 0) {
874
- const line = `from ._runtime import ${runtimeUsed.join(", ")}`
875
- if (line.length <= 88) {
876
- imports.push(line)
877
- } else {
878
- imports.push("from ._runtime import (")
879
- for (const sym of runtimeUsed) imports.push(` ${sym},`)
880
- imports.push(")")
881
- }
882
- }
883
-
884
- const invalidationUsed: string[] = []
885
- for (const sym of ["_StaleTracker", "_StaleTrackerSync", "interpolate_path"]) {
886
- if (new RegExp(`\\b${sym}\\b`).test(bodyText)) invalidationUsed.push(sym)
887
- }
888
- if (invalidationUsed.length > 0) {
889
- imports.push(`from ._invalidation import ${invalidationUsed.join(", ")}`)
890
- }
891
-
892
- /* APIStatusError only appears in docstring Raises sections, never as a code symbol */
893
- const errorsUsed: string[] = []
894
- for (const sym of ["APIError", "_STATUS_ERROR_MAP"]) {
895
- if (new RegExp(`\\b${sym}\\b`).test(bodyText)) errorsUsed.push(sym)
896
- }
897
- if (errorsUsed.length > 0) {
898
- imports.push(`from ._errors import ${errorsUsed.join(", ")}`)
899
- }
900
-
901
- const sseUsed: string[] = []
902
- for (const sym of ["SSEEvent", "parse_sse_stream"]) {
903
- if (new RegExp(`\\b${sym}\\b`).test(bodyText)) sseUsed.push(sym)
904
- }
905
- if (sseUsed.length > 0) {
906
- imports.push(`from ._sse import ${sseUsed.join(", ")}`)
907
- }
908
815
 
909
- if (/\b_TypedWebSocket\b/.test(bodyText)) {
910
- imports.push("from ._ws import _TypedWebSocket")
911
- }
912
-
913
- const transportUsed: string[] = []
914
- for (const sym of ["LongpollAdapter", "SseAdapter", "TransportOpts", "WsAdapter"]) {
915
- if (new RegExp(`\\b${sym}\\b`).test(bodyText)) transportUsed.push(sym)
916
- }
917
- if (transportUsed.length > 0) {
918
- imports.push(`from ._transport import ${transportUsed.join(", ")}`)
919
- }
816
+ const typingUsed = ["Any", "AsyncIterator", "Literal", "NotRequired", "TypedDict"].filter(uses)
817
+ if (typingUsed.length > 0) imports.push(`from typing import ${typingUsed.join(", ")}`)
920
818
 
921
- if (/\bResumableConnection\b/.test(bodyText)) {
922
- imports.push("from ._realtime import ResumableConnection")
819
+ for (const [mod, syms] of PY_CLIENT_IMPORTS) {
820
+ const used = syms.filter(uses)
821
+ if (used.length === 0) continue
822
+ imports.push(`from ${mod} import (`)
823
+ for (const s of used) imports.push(` ${s},`)
824
+ imports.push(")")
923
825
  }
924
826
 
925
- const usedSchemas = schemaNames.filter((n) => new RegExp(`\\b${n}\\b`).test(bodyText))
926
- if (usedSchemas.length > 0) {
927
- const typesLine = `from .types import ${usedSchemas.join(", ")}`
928
- if (typesLine.length <= 88) {
929
- imports.push(typesLine)
930
- } else {
931
- imports.push("from .types import (")
932
- for (const name of usedSchemas) imports.push(` ${name},`)
933
- imports.push(")")
934
- }
827
+ const typeNames = [...names.schemaNames.values(), ...names.declOrder].filter(uses).sort(cmpCodeUnit)
828
+ if (typeNames.length > 0) {
829
+ imports.push("from .types import (")
830
+ for (const n of typeNames) imports.push(` ${n},`)
831
+ imports.push(")")
935
832
  }
936
833
 
937
834
  const out: string[] = []
@@ -941,89 +838,59 @@ function buildPyClient(
941
838
  out.push(...imports)
942
839
  out.push("")
943
840
  out.push(bodyText)
841
+ out.push("")
944
842
  return out.join("\n")
945
843
  }
946
844
 
947
- function buildPyInit(spec: Record<string, unknown>): string {
948
- const errorClasses = [
949
- "APIError",
950
- "APIStatusError",
951
- "BadGatewayError",
952
- "BadRequestError",
953
- "ConflictError",
954
- "ForbiddenError",
955
- "GatewayTimeoutError",
956
- "InternalServerError",
957
- "NotFoundError",
958
- "RateLimitError",
959
- "ServiceUnavailableError",
960
- "UnauthorizedError",
961
- "UnprocessableEntityError",
962
- ]
963
-
964
- const transportExports = [
965
- "LongpollAdapter",
966
- "SseAdapter",
967
- "TransportAdapter",
968
- "TransportConn",
969
- "TransportKind",
970
- "TransportOpts",
971
- "WsAdapter",
972
- ]
973
-
974
- const realtimeExports = ["ConnectionState", "ResumableConnection"]
845
+ function buildPyInit(names: PyNames): string {
846
+ const typeNames = [...names.schemaNames.values()].sort(cmpCodeUnit)
975
847
 
976
848
  const l: string[] = []
977
849
  l.push(PYTHON_FUTURE)
978
850
  l.push("")
979
851
  l.push("from .client import AsyncSDK, SDK")
980
852
  l.push("from ._errors import (")
981
- for (const name of errorClasses) {
982
- l.push(` ${name},`)
983
- }
853
+ for (const name of ERROR_CLASSES) l.push(` ${name},`)
984
854
  l.push(")")
855
+ l.push("from ._runtime import ClientConfig, InvalidationConfig, PathParamError")
985
856
  l.push("from ._transport import (")
986
- for (const name of transportExports) {
987
- l.push(` ${name},`)
988
- }
857
+ for (const name of TRANSPORT_EXPORTS) l.push(` ${name},`)
989
858
  l.push(")")
990
859
  l.push("from ._realtime import (")
991
- for (const name of realtimeExports) {
992
- l.push(` ${name},`)
993
- }
860
+ for (const name of REALTIME_EXPORTS) l.push(` ${name},`)
994
861
  l.push(")")
995
-
996
- const schemas = (spec.components as Record<string, unknown> | undefined)?.schemas as
997
- | Record<string, Record<string, unknown>>
998
- | undefined
999
- const schemaNames = schemas ? Object.keys(schemas).sort() : []
1000
- if (schemaNames.length > 0) {
862
+ if (typeNames.length > 0) {
1001
863
  l.push("from .types import (")
1002
- for (const name of schemaNames) {
1003
- l.push(` ${name},`)
1004
- }
864
+ for (const name of typeNames) l.push(` ${name},`)
1005
865
  l.push(")")
1006
866
  }
1007
867
 
1008
868
  l.push("")
1009
- const allExports = ["AsyncSDK", "SDK", ...errorClasses, ...transportExports, ...realtimeExports, ...schemaNames]
869
+ const allExports = [
870
+ "AsyncSDK",
871
+ "SDK",
872
+ ...ERROR_CLASSES,
873
+ "ClientConfig",
874
+ "InvalidationConfig",
875
+ "PathParamError",
876
+ ...TRANSPORT_EXPORTS,
877
+ ...REALTIME_EXPORTS,
878
+ ...typeNames,
879
+ ]
1010
880
  l.push("__all__ = [")
1011
- for (const name of allExports) {
1012
- l.push(` "${name}",`)
1013
- }
881
+ for (const name of allExports) l.push(` ${pyString(name)},`)
1014
882
  l.push("]")
1015
-
883
+ l.push("")
1016
884
  return l.join("\n")
1017
885
  }
1018
886
 
1019
- function buildServiceMap(spec: Record<string, unknown>): Record<string, ServiceMapEntry> {
1020
- const ops = collectOperations(spec)
1021
- const result: Record<string, ServiceMapEntry> = {}
1022
- for (const op of ops) {
1023
- result[op.operationId] = {
1024
- invalidate: op.invalidate.length > 0 ? op.invalidate : undefined,
887
+ function buildServiceMap(model: SdkModel): Record<string, ServiceMapEntry> {
888
+ const result: Record<string, ServiceMapEntry> = Object.create(null)
889
+ for (const op of model.ops) {
890
+ result[op.id] = {
891
+ invalidate: op.invalidates.length > 0 ? op.invalidates : undefined,
1025
892
  method: op.method,
1026
- operationId: op.operationId,
893
+ operationId: op.id,
1027
894
  path: op.path,
1028
895
  }
1029
896
  }
@@ -1032,17 +899,18 @@ function buildServiceMap(spec: Record<string, unknown>): Record<string, ServiceM
1032
899
 
1033
900
  export function generatePythonSDK(spec: Record<string, unknown>, options: PySDKOptions = {}): PySDKResult {
1034
901
  const templates = loadPythonRuntimeTemplates()
1035
- const serviceMap = buildServiceMap(spec)
902
+ const model = buildSdkModel(spec as unknown as OpenApiSpecInput)
903
+ const serviceMap = buildServiceMap(model)
904
+ const names = buildPyNames(model)
1036
905
 
1037
906
  const files: Record<string, string> = {}
907
+ for (const [name, content] of templates) files[name] = content
1038
908
 
1039
- for (const [name, content] of templates) {
1040
- files[name] = content
1041
- }
1042
-
1043
- files["types.py"] = buildPyTypes(spec)
1044
- files["client.py"] = buildPyClient(spec, options, serviceMap)
1045
- files["__init__.py"] = buildPyInit(spec)
909
+ /* schema types claim their names first; the client then hoists into the same table */
910
+ const schemaSource = buildPyTypes(model, names)
911
+ files["client.py"] = clientFile(buildPyClient(model, names, options, serviceMap), names)
912
+ files["types.py"] = typesFile(schemaSource, names)
913
+ files["__init__.py"] = buildPyInit(names)
1046
914
 
1047
915
  return { files, serviceMap }
1048
916
  }