@redocly/cli 2.47.0 → 2.49.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 (133) hide show
  1. package/lib/chunks/{QRD3XQNU.js → 2U54VUOE.js} +1 -1
  2. package/lib/chunks/3JJEI7CJ.js +456 -0
  3. package/lib/chunks/4I2OVMBJ.js +11 -0
  4. package/lib/chunks/{REBHK2JR.js → 4JDAOKRJ.js} +1 -1
  5. package/lib/chunks/5JIGUBC3.js +7 -0
  6. package/lib/chunks/{QVV2EKMT.js → 5SAFIITS.js} +1 -1
  7. package/lib/chunks/6KCIFTIG.js +57 -0
  8. package/lib/chunks/{5NR4DJW4.js → 6VSHPQXO.js} +1 -1
  9. package/lib/chunks/{W2NJCZOS.js → 74WJ7S7J.js} +1 -1
  10. package/lib/chunks/{QL4V2A4A.js → 7R5PBTLG.js} +1 -1
  11. package/lib/chunks/BT3ELZVX.js +42 -0
  12. package/lib/chunks/C5TR4MMN.js +19 -0
  13. package/lib/chunks/CCR5RYCX.js +20 -0
  14. package/lib/chunks/DCN3GSUA.js +7 -0
  15. package/lib/chunks/{TEQA5OM7.js → E5I67ZCZ.js} +1 -1
  16. package/lib/chunks/FUMCRNTK.js +78 -0
  17. package/lib/chunks/GNMJA2V2.js +16 -0
  18. package/lib/chunks/GRJG3TOI.js +14 -0
  19. package/lib/chunks/HA22GUKQ.js +7 -0
  20. package/lib/chunks/HJ36YGCI.js +184 -0
  21. package/lib/chunks/HJYFWIVE.js +9 -0
  22. package/lib/chunks/HL5FOKC4.js +16 -0
  23. package/lib/chunks/{SL6AH3N2.js → JBNPGEIJ.js} +1 -1
  24. package/lib/chunks/KHEMMNFU.js +26 -0
  25. package/lib/chunks/{SN2EC2ZR.js → KK2STV73.js} +1 -1
  26. package/lib/chunks/KK77ANZH.js +7 -0
  27. package/lib/chunks/L3TZNNJJ.js +13 -0
  28. package/lib/chunks/M6KLAAY4.js +7 -0
  29. package/lib/chunks/MHITBEGC.js +90 -0
  30. package/lib/chunks/NB54XRRU.js +10 -0
  31. package/lib/chunks/NSIQWNYL.js +7 -0
  32. package/lib/chunks/{YQW463Z7.js → NTPLYWAB.js} +1 -1
  33. package/lib/chunks/{5PXRDPHV.js → NUNOXB7Z.js} +1 -1
  34. package/lib/chunks/NWMTC4AN.js +7 -0
  35. package/lib/chunks/NYLAOZPO.js +22 -0
  36. package/lib/chunks/O4R6BSAS.js +20 -0
  37. package/lib/chunks/OKITK6EF.js +30 -0
  38. package/lib/chunks/OUCZOBEX.js +22 -0
  39. package/lib/chunks/PFYPTG4G.js +7 -0
  40. package/lib/chunks/QZH7EL26.js +7 -0
  41. package/lib/chunks/R4XFBOJU.js +9 -0
  42. package/lib/chunks/REFX2EKS.js +13 -0
  43. package/lib/chunks/{S4MX7JPH.js → RXOEH2LN.js} +1 -1
  44. package/lib/chunks/SGBB4RNM.js +11 -0
  45. package/lib/chunks/SQVG3CEM.js +8 -0
  46. package/lib/chunks/{RIFZSMUA.js → T5EPIBMA.js} +1 -1
  47. package/lib/chunks/TLQZAJQU.js +18 -0
  48. package/lib/chunks/TSB6PODJ.js +13 -0
  49. package/lib/chunks/TSRK76GA.js +19 -0
  50. package/lib/chunks/{34J2ZJDK.js → U2H2LXZE.js} +1 -1
  51. package/lib/chunks/UKSQ7B5H.js +2268 -0
  52. package/lib/chunks/UMQN5HTL.js +42 -0
  53. package/lib/chunks/V2LJ4UGM.js +7 -0
  54. package/lib/chunks/VBPP2WUF.js +7 -0
  55. package/lib/chunks/WPGKH5NI.js +177 -0
  56. package/lib/chunks/WXVC3KOI.js +67 -0
  57. package/lib/chunks/{KEUH3PST.js → YI34NB7D.js} +1 -1
  58. package/lib/chunks/YOJGIGE3.js +7 -0
  59. package/lib/chunks/{TO2KJ36N.js → YP5GHFYE.js} +1 -1
  60. package/lib/chunks/YXXYE4G2.js +7 -0
  61. package/lib/chunks/ZBL4IVEW.js +11 -0
  62. package/lib/chunks/ZMCRMR2V.js +5400 -0
  63. package/lib/chunks/ZOUQBK53.js +14 -0
  64. package/lib/chunks/ZXKOHMC6.js +177 -0
  65. package/lib/eject-assets/AGENTS.md +137 -0
  66. package/lib/eject-assets/generators/cli/docs.ts +224 -0
  67. package/lib/eject-assets/generators/cli/engine-source.ts +21 -0
  68. package/lib/eject-assets/generators/cli/index.ts +90 -0
  69. package/lib/eject-assets/generators/cli/render.ts +348 -0
  70. package/lib/eject-assets/generators/go/client.ts +57 -0
  71. package/lib/eject-assets/generators/go/descriptor.ts +41 -0
  72. package/lib/eject-assets/generators/go/index.ts +336 -0
  73. package/lib/eject-assets/generators/go/models.ts +179 -0
  74. package/lib/eject-assets/generators/go/naming.ts +49 -0
  75. package/lib/eject-assets/generators/go/operations.ts +296 -0
  76. package/lib/eject-assets/generators/go/pagination.ts +194 -0
  77. package/lib/eject-assets/generators/go/types.ts +60 -0
  78. package/lib/eject-assets/generators/mock/faker.ts +214 -0
  79. package/lib/eject-assets/generators/mock/index.ts +35 -0
  80. package/lib/eject-assets/generators/mock/render.ts +282 -0
  81. package/lib/eject-assets/generators/mock/sample.ts +316 -0
  82. package/lib/eject-assets/generators/mock/values.ts +63 -0
  83. package/lib/eject-assets/generators/php/client.ts +63 -0
  84. package/lib/eject-assets/generators/php/descriptor.ts +58 -0
  85. package/lib/eject-assets/generators/php/index.ts +237 -0
  86. package/lib/eject-assets/generators/php/models.ts +275 -0
  87. package/lib/eject-assets/generators/php/naming.ts +51 -0
  88. package/lib/eject-assets/generators/php/operations.ts +232 -0
  89. package/lib/eject-assets/generators/php/pagination.ts +133 -0
  90. package/lib/eject-assets/generators/php/types.ts +148 -0
  91. package/lib/eject-assets/generators/python/client.ts +123 -0
  92. package/lib/eject-assets/generators/python/descriptor.ts +52 -0
  93. package/lib/eject-assets/generators/python/index.ts +251 -0
  94. package/lib/eject-assets/generators/python/models.ts +242 -0
  95. package/lib/eject-assets/generators/python/naming.ts +46 -0
  96. package/lib/eject-assets/generators/python/operations.ts +147 -0
  97. package/lib/eject-assets/generators/python/pagination.ts +128 -0
  98. package/lib/eject-assets/generators/python/types.ts +54 -0
  99. package/lib/eject-assets/generators/swr/index.ts +37 -0
  100. package/lib/eject-assets/generators/swr/render.ts +78 -0
  101. package/lib/eject-assets/generators/tanstack-query/index.ts +48 -0
  102. package/lib/eject-assets/generators/tanstack-query/render.ts +346 -0
  103. package/lib/eject-assets/generators/transformers/index.ts +46 -0
  104. package/lib/eject-assets/generators/transformers/render.ts +506 -0
  105. package/lib/eject-assets/generators/typescript/banner.ts +35 -0
  106. package/lib/eject-assets/generators/typescript/client.ts +254 -0
  107. package/lib/eject-assets/generators/typescript/descriptor.ts +137 -0
  108. package/lib/eject-assets/generators/typescript/index.ts +101 -0
  109. package/lib/eject-assets/generators/typescript/inline-runtime.ts +135 -0
  110. package/lib/eject-assets/generators/typescript/operation-signature.ts +62 -0
  111. package/lib/eject-assets/generators/typescript/operation-types.ts +17 -0
  112. package/lib/eject-assets/generators/typescript/operations.ts +524 -0
  113. package/lib/eject-assets/generators/typescript/response-headers.ts +74 -0
  114. package/lib/eject-assets/generators/typescript/type-guards.ts +159 -0
  115. package/lib/eject-assets/generators/typescript/types.ts +172 -0
  116. package/lib/eject-assets/generators/zod/index.ts +32 -0
  117. package/lib/eject-assets/generators/zod/schemas.ts +456 -0
  118. package/lib/eject-assets/skills/cli-generator/SKILL.md +121 -0
  119. package/lib/eject-assets/skills/client-generators/SKILL.md +142 -0
  120. package/lib/eject-assets/skills/go-generator/SKILL.md +96 -0
  121. package/lib/eject-assets/skills/mock-generator/SKILL.md +50 -0
  122. package/lib/eject-assets/skills/php-generator/SKILL.md +112 -0
  123. package/lib/eject-assets/skills/python-generator/SKILL.md +110 -0
  124. package/lib/eject-assets/skills/swr-generator/SKILL.md +50 -0
  125. package/lib/eject-assets/skills/tanstack-query-generator/SKILL.md +55 -0
  126. package/lib/eject-assets/skills/transformers-generator/SKILL.md +47 -0
  127. package/lib/eject-assets/skills/typescript-generator/SKILL.md +95 -0
  128. package/lib/eject-assets/skills/zod-generator/SKILL.md +54 -0
  129. package/lib/index.js +271 -193
  130. package/package.json +1 -1
  131. package/lib/chunks/B6HS7CVY.js +0 -184
  132. package/lib/chunks/PIJLRHNN.js +0 -8
  133. package/lib/chunks/Y5HLCNL5.js +0 -2542
@@ -0,0 +1,2268 @@
1
+ import { createRequire as __createRequire } from 'node:module';
2
+ import { fileURLToPath as __fileURLToPath } from 'node:url';
3
+ import { dirname as __pathDirname } from 'node:path';
4
+ const require = __createRequire(import.meta.url);
5
+ var __filename = __fileURLToPath(import.meta.url);
6
+ var __dirname = __pathDirname(__filename);
7
+ var e=`// Package client \u2014 the embedded runtime for generated Go SDKs. Hand-authored
8
+ // once and stitched into every generated client (see
9
+ // scripts/generate-runtime-sources.mjs), semantically in lockstep with the
10
+ // TypeScript runtime: auth OR-alternatives, a retry loop with Retry-After and
11
+ // full-jitter backoff, per-attempt timeouts, idempotency keys, and middleware
12
+ // hooks. Standard library only \u2014 a generated Go SDK has zero dependencies.
13
+ package client
14
+
15
+ import (
16
+ "bytes"
17
+ "context"
18
+ "encoding/base64"
19
+ "encoding/json"
20
+ "errors"
21
+ "fmt"
22
+ "io"
23
+ "math/rand"
24
+ "mime/multipart"
25
+ "net/http"
26
+ "net/url"
27
+ "strconv"
28
+ "strings"
29
+ "time"
30
+ )
31
+
32
+ // APIError is returned for a non-2xx response, carrying the decoded error body.
33
+ type APIError struct {
34
+ URL string
35
+ Status int
36
+ StatusText string
37
+ Body any
38
+ }
39
+
40
+ func (e *APIError) Error() string {
41
+ return fmt.Sprintf("request failed with status %d", e.Status)
42
+ }
43
+
44
+ // TimeoutError is returned when a request attempt exceeds the configured
45
+ // timeout \u2014 carrying the context a log line needs.
46
+ type TimeoutError struct {
47
+ OperationID string
48
+ Timeout time.Duration
49
+ Attempt int
50
+ }
51
+
52
+ func (e *TimeoutError) Error() string {
53
+ return fmt.Sprintf("request %q timed out after %s (attempt %d)", e.OperationID, e.Timeout, e.Attempt)
54
+ }
55
+
56
+ // SecuritySpec mirrors the descriptor table's security entries.
57
+ type SecuritySpec struct {
58
+ Scheme string
59
+ Kind string // "bearer" | "basic" | "apiKey"
60
+ Name string // header/query/cookie name for apiKey
61
+ In string // "header" | "query" | "cookie"
62
+ }
63
+
64
+ // Auth holds the client credentials; zero value = anonymous.
65
+ type Auth struct {
66
+ Bearer func() string
67
+ Basic *BasicAuth
68
+ APIKey map[string]func() string
69
+ }
70
+
71
+ type BasicAuth struct {
72
+ Username string
73
+ Password string
74
+ }
75
+
76
+ // RetryConfig mirrors the TypeScript runtime's retry policy knobs.
77
+ type RetryConfig struct {
78
+ Retries int
79
+ RetryDelay time.Duration // base; default 1s
80
+ RetryStrategy string // "" (exponential) | "fixed"
81
+ NoJitter bool
82
+ // RetryOn fully replaces the default predicate when set.
83
+ RetryOn func(attempt int, resp *http.Response, err error) bool
84
+ }
85
+
86
+ // Middleware hooks run around every request (OnRequest before serialization order
87
+ // is N/A in Go \u2014 bodies are values; OnResponse runs in reverse registration order).
88
+ type Middleware struct {
89
+ OnRequest func(req *http.Request)
90
+ OnResponse func(resp *http.Response)
91
+ }
92
+
93
+ // Date is an RFC 3339 full-date \u2014 a calendar date with no time component. Fields
94
+ // typed \`date\` under \`dateType: Date\` use it because encoding/json speaks only
95
+ // RFC 3339 date-time for time.Time, which a bare "2006-01-02" fails to satisfy.
96
+ type Date struct {
97
+ time.Time
98
+ }
99
+
100
+ const dateLayout = "2006-01-02"
101
+
102
+ // UnmarshalJSON parses a "2006-01-02" string; an empty string leaves the zero value.
103
+ func (d *Date) UnmarshalJSON(data []byte) error {
104
+ var raw string
105
+ if err := json.Unmarshal(data, &raw); err != nil {
106
+ return err
107
+ }
108
+ if raw == "" {
109
+ return nil
110
+ }
111
+ parsed, err := time.Parse(dateLayout, raw)
112
+ if err != nil {
113
+ return err
114
+ }
115
+ d.Time = parsed
116
+ return nil
117
+ }
118
+
119
+ // MarshalJSON writes the date back without a time component.
120
+ func (d Date) MarshalJSON() ([]byte, error) {
121
+ return json.Marshal(d.Format(dateLayout))
122
+ }
123
+
124
+ // Config is the per-client configuration shared by every operation method.
125
+ type Config struct {
126
+ ServerURL string
127
+ HTTPClient *http.Client
128
+ Headers map[string]string
129
+ Timeout time.Duration
130
+ Retry RetryConfig
131
+ Middleware []Middleware
132
+ IdempotencyKey func() string
133
+ Auth Auth
134
+ }
135
+
136
+ func resolveToken(provider func() string) string {
137
+ if provider == nil {
138
+ return ""
139
+ }
140
+ return provider()
141
+ }
142
+
143
+ func schemeConfigured(spec SecuritySpec, auth Auth) bool {
144
+ switch spec.Kind {
145
+ case "apiKey":
146
+ _, ok := auth.APIKey[spec.Scheme]
147
+ return ok
148
+ case "bearer":
149
+ return auth.Bearer != nil
150
+ default:
151
+ return auth.Basic != nil
152
+ }
153
+ }
154
+
155
+ // resolveAuth applies the first fully-configured OR-alternative; when none is,
156
+ // the first alternative's configured schemes are still sent (the server rejects
157
+ // the request \u2014 same behavior as the TypeScript runtime).
158
+ func resolveAuth(security [][]SecuritySpec, auth Auth) (map[string]string, url.Values) {
159
+ headers := map[string]string{}
160
+ query := url.Values{}
161
+ if len(security) == 0 {
162
+ return headers, query
163
+ }
164
+ alternative := security[0]
165
+ for _, candidate := range security {
166
+ all := true
167
+ for _, spec := range candidate {
168
+ if !schemeConfigured(spec, auth) {
169
+ all = false
170
+ break
171
+ }
172
+ }
173
+ if all {
174
+ alternative = candidate
175
+ break
176
+ }
177
+ }
178
+ var cookies []string
179
+ for _, spec := range alternative {
180
+ switch spec.Kind {
181
+ case "apiKey":
182
+ provider, ok := auth.APIKey[spec.Scheme]
183
+ if !ok {
184
+ continue
185
+ }
186
+ value := resolveToken(provider)
187
+ switch spec.In {
188
+ case "query":
189
+ query.Set(spec.Name, value)
190
+ case "cookie":
191
+ cookies = append(cookies, spec.Name+"="+url.QueryEscape(value))
192
+ default:
193
+ headers[spec.Name] = value
194
+ }
195
+ case "bearer":
196
+ if auth.Bearer != nil {
197
+ headers["Authorization"] = "Bearer " + resolveToken(auth.Bearer)
198
+ }
199
+ default:
200
+ if auth.Basic != nil {
201
+ token := base64.StdEncoding.EncodeToString([]byte(auth.Basic.Username + ":" + auth.Basic.Password))
202
+ headers["Authorization"] = "Basic " + token
203
+ }
204
+ }
205
+ }
206
+ if len(cookies) > 0 {
207
+ headers["Cookie"] = strings.Join(cookies, "; ")
208
+ }
209
+ return headers, query
210
+ }
211
+
212
+ // buildURL substitutes {param} path placeholders with percent-encoded values.
213
+ func buildURL(serverURL, path string, pathParams map[string]string) string {
214
+ filled := path
215
+ for name, value := range pathParams {
216
+ filled = strings.ReplaceAll(filled, "{"+name+"}", url.PathEscape(value))
217
+ }
218
+ return strings.TrimRight(serverURL, "/") + filled
219
+ }
220
+
221
+ var transientStatus = map[int]bool{408: true, 429: true, 500: true, 502: true, 503: true, 504: true}
222
+
223
+ func defaultRetryOn(method string, headers map[string]string, resp *http.Response, err error) bool {
224
+ safe := false
225
+ switch strings.ToUpper(method) {
226
+ case "GET", "HEAD", "PUT", "DELETE", "OPTIONS":
227
+ safe = true
228
+ }
229
+ if _, ok := headers["Idempotency-Key"]; ok {
230
+ safe = true
231
+ }
232
+ if !safe {
233
+ return false
234
+ }
235
+ if err != nil {
236
+ return true
237
+ }
238
+ return resp != nil && transientStatus[resp.StatusCode]
239
+ }
240
+
241
+ func retryDelay(retry RetryConfig, attempt int, retryAfter string) time.Duration {
242
+ if retryAfter != "" {
243
+ if seconds, err := strconv.ParseFloat(retryAfter, 64); err == nil {
244
+ return time.Duration(seconds * float64(time.Second))
245
+ }
246
+ }
247
+ base := retry.RetryDelay
248
+ if base == 0 {
249
+ base = time.Second
250
+ }
251
+ raw := base
252
+ if retry.RetryStrategy != "fixed" {
253
+ raw = base * time.Duration(1<<(attempt-1))
254
+ }
255
+ if retry.NoJitter {
256
+ return raw
257
+ }
258
+ return time.Duration(rand.Int63n(int64(raw) + 1))
259
+ }
260
+
261
+ type requestSpec struct {
262
+ OperationID string
263
+ Method string
264
+ URL string
265
+ Headers map[string]string
266
+ Query url.Values
267
+ Body io.Reader
268
+ ContentType string
269
+ Timeout time.Duration
270
+ Retry *RetryConfig
271
+ IdempotencyKey string
272
+ // bodyBytes is retained so retries can replay the body.
273
+ bodyBytes []byte
274
+ }
275
+
276
+ // send is the request core: header merge, idempotency keys, the retry loop
277
+ // (fresh timeout budget per attempt), and the middleware onion.
278
+ func send(ctx context.Context, config *Config, spec requestSpec) (*http.Response, error) {
279
+ retry := config.Retry
280
+ if spec.Retry != nil {
281
+ retry = *spec.Retry
282
+ }
283
+ timeout := config.Timeout
284
+ if spec.Timeout != 0 {
285
+ timeout = spec.Timeout
286
+ }
287
+ headers := map[string]string{}
288
+ for key, value := range config.Headers {
289
+ headers[key] = value
290
+ }
291
+ for key, value := range spec.Headers {
292
+ headers[key] = value
293
+ }
294
+ method := strings.ToUpper(spec.Method)
295
+ if (method == "POST" || method == "PATCH") && headers["Idempotency-Key"] == "" {
296
+ if spec.IdempotencyKey != "" {
297
+ headers["Idempotency-Key"] = spec.IdempotencyKey
298
+ } else if config.IdempotencyKey != nil {
299
+ headers["Idempotency-Key"] = config.IdempotencyKey()
300
+ }
301
+ }
302
+ httpClient := config.HTTPClient
303
+ if httpClient == nil {
304
+ httpClient = http.DefaultClient
305
+ }
306
+ if spec.Body != nil {
307
+ payload, err := io.ReadAll(spec.Body)
308
+ if err != nil {
309
+ return nil, err
310
+ }
311
+ spec.bodyBytes = payload
312
+ }
313
+ fullURL := spec.URL
314
+ if len(spec.Query) > 0 {
315
+ separator := "?"
316
+ if strings.Contains(fullURL, "?") {
317
+ separator = "&"
318
+ }
319
+ fullURL += separator + spec.Query.Encode()
320
+ }
321
+ maxAttempts := 1 + retry.Retries
322
+ for attempt := 1; ; attempt++ {
323
+ attemptCtx := ctx
324
+ var cancel context.CancelFunc
325
+ if timeout > 0 {
326
+ attemptCtx, cancel = context.WithTimeout(ctx, timeout)
327
+ }
328
+ var bodyReader io.Reader
329
+ if spec.bodyBytes != nil {
330
+ bodyReader = bytes.NewReader(spec.bodyBytes)
331
+ }
332
+ req, err := http.NewRequestWithContext(attemptCtx, method, fullURL, bodyReader)
333
+ if err != nil {
334
+ if cancel != nil {
335
+ cancel()
336
+ }
337
+ return nil, err
338
+ }
339
+ for key, value := range headers {
340
+ req.Header.Set(key, value)
341
+ }
342
+ if spec.ContentType != "" && spec.bodyBytes != nil {
343
+ req.Header.Set("Content-Type", spec.ContentType)
344
+ }
345
+ for _, mw := range config.Middleware {
346
+ if mw.OnRequest != nil {
347
+ mw.OnRequest(req)
348
+ }
349
+ }
350
+ resp, err := httpClient.Do(req)
351
+ shouldRetry := retry.RetryOn
352
+ retryable := false
353
+ if shouldRetry != nil {
354
+ retryable = shouldRetry(attempt, resp, err)
355
+ } else {
356
+ retryable = defaultRetryOn(method, headers, resp, err)
357
+ }
358
+ if err != nil {
359
+ if cancel != nil {
360
+ cancel()
361
+ }
362
+ timedOut := errors.Is(err, context.DeadlineExceeded) && ctx.Err() == nil
363
+ if attempt < maxAttempts && retryable {
364
+ time.Sleep(retryDelay(retry, attempt, ""))
365
+ continue
366
+ }
367
+ if timedOut {
368
+ return nil, &TimeoutError{OperationID: spec.OperationID, Timeout: timeout, Attempt: attempt}
369
+ }
370
+ return nil, err
371
+ }
372
+ for i := len(config.Middleware) - 1; i >= 0; i-- {
373
+ if config.Middleware[i].OnResponse != nil {
374
+ config.Middleware[i].OnResponse(resp)
375
+ }
376
+ }
377
+ if resp.StatusCode >= 400 && attempt < maxAttempts && retryable {
378
+ after := resp.Header.Get("Retry-After")
379
+ io.Copy(io.Discard, resp.Body)
380
+ resp.Body.Close()
381
+ if cancel != nil {
382
+ cancel()
383
+ }
384
+ time.Sleep(retryDelay(retry, attempt, after))
385
+ continue
386
+ }
387
+ // The response body outlives this call; tie the attempt context's lifetime to it.
388
+ if cancel != nil {
389
+ resp.Body = &cancelOnClose{ReadCloser: resp.Body, cancel: cancel}
390
+ }
391
+ return resp, nil
392
+ }
393
+ }
394
+
395
+ type cancelOnClose struct {
396
+ io.ReadCloser
397
+ cancel context.CancelFunc
398
+ }
399
+
400
+ func (c *cancelOnClose) Close() error {
401
+ c.cancel()
402
+ return c.ReadCloser.Close()
403
+ }
404
+
405
+ // decodeJSON decodes a response body into target; a nil target drains and closes.
406
+ func decodeJSON(resp *http.Response, target any) error {
407
+ defer resp.Body.Close()
408
+ if target == nil {
409
+ _, err := io.Copy(io.Discard, resp.Body)
410
+ return err
411
+ }
412
+ return json.NewDecoder(resp.Body).Decode(target)
413
+ }
414
+
415
+ // headerString returns the named response header, or nil when absent.
416
+ func headerString(header http.Header, name string) *string {
417
+ value := header.Get(name)
418
+ if value == "" {
419
+ return nil
420
+ }
421
+ return &value
422
+ }
423
+
424
+ // headerInt64 parses the named header as an integer; nil when absent or unparsable.
425
+ func headerInt64(header http.Header, name string) *int64 {
426
+ raw := strings.TrimSpace(header.Get(name))
427
+ if raw == "" {
428
+ return nil
429
+ }
430
+ value, err := strconv.ParseInt(raw, 10, 64)
431
+ if err != nil {
432
+ return nil
433
+ }
434
+ return &value
435
+ }
436
+
437
+ // headerFloat64 parses the named header as a number; nil when absent or unparsable.
438
+ func headerFloat64(header http.Header, name string) *float64 {
439
+ raw := strings.TrimSpace(header.Get(name))
440
+ if raw == "" {
441
+ return nil
442
+ }
443
+ value, err := strconv.ParseFloat(raw, 64)
444
+ if err != nil {
445
+ return nil
446
+ }
447
+ return &value
448
+ }
449
+
450
+ // headerBool parses a \`true\`/\`false\` header; nil when absent or anything else.
451
+ func headerBool(header http.Header, name string) *bool {
452
+ raw := strings.ToLower(strings.TrimSpace(header.Get(name)))
453
+ if raw != "true" && raw != "false" {
454
+ return nil
455
+ }
456
+ value := raw == "true"
457
+ return &value
458
+ }
459
+
460
+ // apiErrorFrom builds the structured error for a non-2xx response.
461
+ func apiErrorFrom(resp *http.Response, requestURL string) error {
462
+ defer resp.Body.Close()
463
+ var body any
464
+ data, _ := io.ReadAll(resp.Body)
465
+ if len(data) > 0 {
466
+ if err := json.Unmarshal(data, &body); err != nil {
467
+ body = string(data)
468
+ }
469
+ }
470
+ return &APIError{URL: requestURL, Status: resp.StatusCode, StatusText: resp.Status, Body: body}
471
+ }
472
+
473
+ // \u2500\u2500\u2500 Pagination \u2500\u2500\u2500
474
+
475
+ // PaginationSpec mirrors the descriptor table's pagination entries.
476
+ type PaginationSpec struct {
477
+ Style string
478
+ Param string
479
+ NextCursor string
480
+ HasMore string
481
+ LimitParam string
482
+ Items string
483
+ }
484
+
485
+ // resolvePointer walks an RFC 6901 JSON pointer over decoded JSON; nil on any miss.
486
+ func resolvePointer(data any, pointer string) any {
487
+ if pointer == "" {
488
+ return data
489
+ }
490
+ if !strings.HasPrefix(pointer, "/") {
491
+ return nil
492
+ }
493
+ current := data
494
+ for _, token := range strings.Split(pointer[1:], "/") {
495
+ key := strings.ReplaceAll(strings.ReplaceAll(token, "~1", "/"), "~0", "~")
496
+ switch typed := current.(type) {
497
+ case map[string]any:
498
+ current = typed[key]
499
+ case []any:
500
+ index, err := strconv.Atoi(key)
501
+ if err != nil || index < 0 || index >= len(typed) {
502
+ return nil
503
+ }
504
+ current = typed[index]
505
+ default:
506
+ return nil
507
+ }
508
+ if current == nil {
509
+ return nil
510
+ }
511
+ }
512
+ return current
513
+ }
514
+
515
+ // reencode converts decoded JSON (maps/slices) into a typed value via a JSON round-trip.
516
+ func reencode(raw any, target any) error {
517
+ data, err := json.Marshal(raw)
518
+ if err != nil {
519
+ return err
520
+ }
521
+ return json.Unmarshal(data, target)
522
+ }
523
+
524
+ type pageCall func(params url.Values) (any, *http.Response, error)
525
+
526
+ // iterPages yields raw page JSON per the pagination spec \u2014 the same stop
527
+ // conditions and infinite-loop guards as the TypeScript runtime. The returned
528
+ // function is a range-over-func iterator (Go 1.23+) and plainly callable before that.
529
+ func iterPages(call pageCall, spec PaginationSpec, base url.Values) func(yield func(any, error) bool) {
530
+ return func(yield func(any, error) bool) {
531
+ switch spec.Style {
532
+ case "cursor":
533
+ var cursor any
534
+ if values, ok := base[spec.Param]; ok && len(values) > 0 {
535
+ cursor = values[0]
536
+ }
537
+ for {
538
+ params := cloneValues(base)
539
+ if cursor != nil {
540
+ params.Set(spec.Param, fmt.Sprint(cursor))
541
+ }
542
+ page, _, err := call(params)
543
+ if err != nil {
544
+ yield(nil, err)
545
+ return
546
+ }
547
+ if !yield(page, nil) {
548
+ return
549
+ }
550
+ if spec.HasMore != "" {
551
+ if more, ok := resolvePointer(page, spec.HasMore).(bool); ok && !more {
552
+ return
553
+ }
554
+ }
555
+ next := resolvePointer(page, spec.NextCursor)
556
+ if next == nil || next == "" {
557
+ return
558
+ }
559
+ switch next.(type) {
560
+ case string, float64:
561
+ default:
562
+ yield(nil, fmt.Errorf("pagination cursor at %s is not a string or number", spec.NextCursor))
563
+ return
564
+ }
565
+ if cursor != nil && fmt.Sprint(next) == fmt.Sprint(cursor) {
566
+ yield(nil, errors.New("pagination did not advance: the operation returned the same cursor twice"))
567
+ return
568
+ }
569
+ cursor = next
570
+ }
571
+ case "link":
572
+ params := cloneValues(base)
573
+ previous := ""
574
+ for {
575
+ page, resp, err := call(params)
576
+ if err != nil {
577
+ yield(nil, err)
578
+ return
579
+ }
580
+ if !yield(page, nil) {
581
+ return
582
+ }
583
+ target := linkNext(resp.Header.Get("Link"))
584
+ if target == "" {
585
+ return
586
+ }
587
+ pageURL := ""
588
+ if resp.Request != nil && resp.Request.URL != nil {
589
+ pageURL = resp.Request.URL.String()
590
+ }
591
+ baseURL, err := url.Parse(pageURL)
592
+ if err != nil || pageURL == "" {
593
+ baseURL, _ = url.Parse("http://relative.invalid")
594
+ }
595
+ targetURL, err := baseURL.Parse(target)
596
+ if err != nil {
597
+ yield(nil, err)
598
+ return
599
+ }
600
+ next := targetURL.String()
601
+ if next == previous || next == pageURL {
602
+ yield(nil, errors.New(\`pagination did not advance: the Link rel="next" target repeats\`))
603
+ return
604
+ }
605
+ previous = next
606
+ params = cloneValues(base)
607
+ for key, values := range targetURL.Query() {
608
+ for _, value := range values {
609
+ params.Add(key, value)
610
+ }
611
+ }
612
+ }
613
+ default: // offset / page
614
+ position := 0
615
+ if spec.Style == "page" {
616
+ position = 1
617
+ }
618
+ if values, ok := base[spec.Param]; ok && len(values) > 0 && values[0] != "" {
619
+ if parsed, err := strconv.Atoi(values[0]); err == nil {
620
+ position = parsed
621
+ }
622
+ }
623
+ previousItems := ""
624
+ for {
625
+ params := cloneValues(base)
626
+ params.Set(spec.Param, strconv.Itoa(position))
627
+ page, _, err := call(params)
628
+ if err != nil {
629
+ yield(nil, err)
630
+ return
631
+ }
632
+ items, _ := resolvePointer(page, spec.Items).([]any)
633
+ serialized := ""
634
+ if items != nil {
635
+ serialized = fmt.Sprint(items)
636
+ if serialized == previousItems {
637
+ yield(nil, errors.New("pagination did not advance: the operation returned the same page twice"))
638
+ return
639
+ }
640
+ }
641
+ if !yield(page, nil) {
642
+ return
643
+ }
644
+ if len(items) == 0 {
645
+ return
646
+ }
647
+ previousItems = serialized
648
+ if spec.Style == "page" {
649
+ position++
650
+ } else {
651
+ position += len(items)
652
+ }
653
+ }
654
+ }
655
+ }
656
+ }
657
+
658
+ func cloneValues(values url.Values) url.Values {
659
+ out := url.Values{}
660
+ for key, entries := range values {
661
+ for _, entry := range entries {
662
+ out.Add(key, entry)
663
+ }
664
+ }
665
+ return out
666
+ }
667
+
668
+ func linkNext(header string) string {
669
+ if header == "" {
670
+ return ""
671
+ }
672
+ for _, entry := range strings.Split(header, ",") {
673
+ parts := strings.Split(entry, ";")
674
+ if len(parts) < 2 {
675
+ continue
676
+ }
677
+ target := strings.TrimSpace(parts[0])
678
+ if !strings.HasPrefix(target, "<") || !strings.HasSuffix(target, ">") {
679
+ continue
680
+ }
681
+ for _, param := range parts[1:] {
682
+ trimmed := strings.TrimSpace(param)
683
+ if strings.HasPrefix(trimmed, "rel=") {
684
+ rel := strings.Trim(strings.TrimPrefix(trimmed, "rel="), \`"\`)
685
+ for _, kind := range strings.Fields(rel) {
686
+ if kind == "next" {
687
+ return strings.Trim(target, "<>")
688
+ }
689
+ }
690
+ }
691
+ }
692
+ }
693
+ return ""
694
+ }
695
+
696
+ // \u2500\u2500\u2500 Server-Sent Events \u2500\u2500\u2500
697
+
698
+ // ServerSentEvent is one decoded event; Data is the raw text (or parsed JSON
699
+ // for operations that declare a JSON event stream).
700
+ type ServerSentEvent struct {
701
+ Event string
702
+ Data any
703
+ ID string
704
+ Retry int
705
+ }
706
+
707
+ func parseSSEFrame(raw string, jsonData bool) (ServerSentEvent, bool, error) {
708
+ event := ServerSentEvent{Retry: -1}
709
+ sawField := false
710
+ var dataLines []string
711
+ normalized := strings.ReplaceAll(strings.ReplaceAll(raw, "\\r\\n", "\\n"), "\\r", "\\n")
712
+ for _, line := range strings.Split(normalized, "\\n") {
713
+ if line == "" || strings.HasPrefix(line, ":") {
714
+ continue
715
+ }
716
+ field, value, _ := strings.Cut(line, ":")
717
+ value = strings.TrimPrefix(value, " ")
718
+ sawField = true
719
+ switch field {
720
+ case "event":
721
+ event.Event = value
722
+ case "data":
723
+ dataLines = append(dataLines, value)
724
+ case "id":
725
+ event.ID = value
726
+ case "retry":
727
+ if parsed, err := strconv.Atoi(value); err == nil && parsed >= 0 && value != "" {
728
+ event.Retry = parsed
729
+ }
730
+ }
731
+ }
732
+ if !sawField {
733
+ return event, false, nil
734
+ }
735
+ text := strings.Join(dataLines, "\\n")
736
+ event.Data = text
737
+ if jsonData && text != "" {
738
+ var parsed any
739
+ if err := json.Unmarshal([]byte(text), &parsed); err != nil {
740
+ return event, false, err
741
+ }
742
+ event.Data = parsed
743
+ }
744
+ return event, true, nil
745
+ }
746
+
747
+ // iterSSE streams events, reconnecting on dropped connections with Last-Event-ID
748
+ // (a fresh open call = fresh auth); a 4xx/5xx or a bad JSON payload is definitive.
749
+ func iterSSE(open func(extraHeaders map[string]string) (*http.Response, error), jsonData bool) func(yield func(ServerSentEvent, error) bool) {
750
+ return func(yield func(ServerSentEvent, error) bool) {
751
+ lastEventID := ""
752
+ serverRetry := -1
753
+ failures := 0
754
+ for {
755
+ headers := map[string]string{"Accept": "text/event-stream"}
756
+ if lastEventID != "" {
757
+ headers["Last-Event-ID"] = lastEventID
758
+ }
759
+ resp, err := open(headers)
760
+ if err == nil && resp.StatusCode >= 400 {
761
+ yield(ServerSentEvent{}, apiErrorFrom(resp, ""))
762
+ return
763
+ }
764
+ if err == nil {
765
+ failures = 0
766
+ buffer := ""
767
+ chunk := make([]byte, 4096)
768
+ clean := false
769
+ for {
770
+ n, readErr := resp.Body.Read(chunk)
771
+ buffer += string(chunk[:n])
772
+ for {
773
+ frame, rest, found := strings.Cut(buffer, "\\n\\n")
774
+ if !found {
775
+ break
776
+ }
777
+ buffer = rest
778
+ event, ok, parseErr := parseSSEFrame(frame, jsonData)
779
+ if parseErr != nil {
780
+ resp.Body.Close()
781
+ yield(ServerSentEvent{}, parseErr)
782
+ return
783
+ }
784
+ if ok {
785
+ if event.ID != "" {
786
+ lastEventID = event.ID
787
+ }
788
+ if event.Retry >= 0 {
789
+ serverRetry = event.Retry
790
+ }
791
+ if !yield(event, nil) {
792
+ resp.Body.Close()
793
+ return
794
+ }
795
+ }
796
+ }
797
+ if readErr == io.EOF {
798
+ clean = true
799
+ break
800
+ }
801
+ if readErr != nil {
802
+ break
803
+ }
804
+ }
805
+ resp.Body.Close()
806
+ if clean {
807
+ if strings.TrimSpace(buffer) != "" {
808
+ if event, ok, parseErr := parseSSEFrame(buffer, jsonData); parseErr == nil && ok {
809
+ yield(event, nil)
810
+ }
811
+ }
812
+ return
813
+ }
814
+ }
815
+ failures++
816
+ base := time.Second
817
+ if serverRetry >= 0 {
818
+ base = time.Duration(serverRetry) * time.Millisecond
819
+ }
820
+ delay := base * time.Duration(1<<(failures-1))
821
+ if delay > 30*time.Second {
822
+ delay = 30 * time.Second
823
+ }
824
+ time.Sleep(time.Duration(rand.Int63n(int64(delay) + 1)))
825
+ }
826
+ }
827
+ }
828
+
829
+ // \u2500\u2500\u2500 Multipart \u2500\u2500\u2500
830
+
831
+ // toMultipart splits a typed body into a multipart/form-data payload: []byte
832
+ // values upload as file parts, everything else as form fields (nested values
833
+ // JSON-encoded) \u2014 mirroring the TypeScript runtime's FormData serialization.
834
+ func toMultipart(body any) (string, io.Reader, error) {
835
+ var wire map[string]any
836
+ if err := reencode(body, &wire); err != nil {
837
+ return "", nil, err
838
+ }
839
+ buffer := &bytes.Buffer{}
840
+ writer := multipart.NewWriter(buffer)
841
+ for key, value := range wire {
842
+ switch typed := value.(type) {
843
+ case string:
844
+ if err := writer.WriteField(key, typed); err != nil {
845
+ return "", nil, err
846
+ }
847
+ case float64, bool:
848
+ if err := writer.WriteField(key, fmt.Sprint(typed)); err != nil {
849
+ return "", nil, err
850
+ }
851
+ default:
852
+ encoded, err := json.Marshal(typed)
853
+ if err != nil {
854
+ return "", nil, err
855
+ }
856
+ if err := writer.WriteField(key, string(encoded)); err != nil {
857
+ return "", nil, err
858
+ }
859
+ }
860
+ }
861
+ if err := writer.Close(); err != nil {
862
+ return "", nil, err
863
+ }
864
+ return writer.FormDataContentType(), buffer, nil
865
+ }
866
+ `;var t=`<?php
867
+
868
+ // @redocly/client-generator PHP runtime \u2014 embedded into generated clients.
869
+ // PHP >= 8.1, zero Composer dependencies; HTTP over the curl extension.
870
+ // The generated file re-declares the namespace; the embed strips this header.
871
+
872
+ declare(strict_types=1);
873
+
874
+ namespace RedoclyClientRuntime;
875
+
876
+ /** A response with status >= 400, decoded body attached. */
877
+ final class ApiError extends \\RuntimeException
878
+ {
879
+ public function __construct(
880
+ public readonly string $url,
881
+ public readonly int $status,
882
+ public readonly string $reason,
883
+ public readonly mixed $body,
884
+ ) {
885
+ parent::__construct("HTTP {$status} {$reason} for {$url}");
886
+ }
887
+ }
888
+
889
+ /** Every attempt timed out or failed to connect. */
890
+ final class TimeoutError extends \\RuntimeException
891
+ {
892
+ public function __construct(
893
+ public readonly string $url,
894
+ public readonly ?float $timeout,
895
+ public readonly int $attempts,
896
+ ) {
897
+ $seconds = $timeout === null ? 'the configured timeout' : "{$timeout}s";
898
+ parent::__construct("Request to {$url} timed out after {$seconds} ({$attempts} attempt(s))");
899
+ }
900
+ }
901
+
902
+ /** One parsed \`text/event-stream\` frame. */
903
+ /** A \`<op>WithHeaders()\` result: the decoded body plus coerced declared headers. */
904
+ final class Envelope
905
+ {
906
+ public function __construct(
907
+ public readonly mixed $data,
908
+ public readonly array $headers,
909
+ public readonly int $status,
910
+ ) {
911
+ }
912
+ }
913
+
914
+ /** Coerce declared response headers per \`[name, key, type]\` specs; absent/unparsable omitted. */
915
+ function readEnvelopeHeaders(array $response, array $specs): array
916
+ {
917
+ $headers = [];
918
+ foreach ($specs as [$name, $key, $type]) {
919
+ $raw = $response['headers'][$name] ?? null;
920
+ if ($raw === null) {
921
+ continue;
922
+ }
923
+ if ($type === 'integer' || $type === 'number') {
924
+ if (is_numeric($raw)) {
925
+ $headers[$key] = $type === 'integer' ? (int) $raw : (float) $raw;
926
+ }
927
+ } elseif ($type === 'boolean') {
928
+ $lower = strtolower(trim($raw));
929
+ if ($lower === 'true' || $lower === 'false') {
930
+ $headers[$key] = $lower === 'true';
931
+ }
932
+ } else {
933
+ $headers[$key] = $raw;
934
+ }
935
+ }
936
+ return $headers;
937
+ }
938
+
939
+ final class ServerSentEvent
940
+ {
941
+ public function __construct(
942
+ public readonly string $event,
943
+ public readonly mixed $data,
944
+ public readonly ?string $id = null,
945
+ public readonly ?int $retry = null,
946
+ ) {
947
+ }
948
+ }
949
+
950
+ /**
951
+ * Per-instance configuration.
952
+ * \`auth\`: \`['bearer' => string|callable, 'basic' => ['username' => ..., 'password' => ...], 'apiKey' => [scheme => string|callable]]\`.
953
+ * \`retry\`: \`['attempts' => int, 'delay' => float, 'strategy' => 'exponential'|'fixed', 'retryOn' => callable]\`.
954
+ * \`middleware\`: callables \`fn(array $request, callable $next): array\` around each attempt.
955
+ */
956
+ final class Config
957
+ {
958
+ public function __construct(
959
+ public string $serverUrl = '',
960
+ public array $auth = [],
961
+ public ?float $timeout = null,
962
+ public array $retry = [],
963
+ public array $middleware = [],
964
+ public string $clientHeader = 'redocly-client-generator',
965
+ ) {
966
+ }
967
+ }
968
+
969
+ /** Resolve a literal-or-callable credential to its string value. */
970
+ function resolveToken(mixed $provider): string
971
+ {
972
+ return is_callable($provider) ? (string) $provider() : (string) $provider;
973
+ }
974
+
975
+ /**
976
+ * Apply the first fully-configured security alternative. \`$security\` is an OR-list
977
+ * of AND-sets of specs: \`['kind' => 'bearer'|'basic'|'apiKey', 'scheme' => ..., 'name' => ?, 'in' => ?]\`.
978
+ * Returns \`[headers, query, cookies]\`.
979
+ */
980
+ function resolveAuth(array $security, array $auth): array
981
+ {
982
+ foreach ($security as $andSet) {
983
+ $headers = [];
984
+ $query = [];
985
+ $cookies = [];
986
+ $satisfied = true;
987
+ foreach ($andSet as $spec) {
988
+ if ($spec['kind'] === 'bearer' && isset($auth['bearer'])) {
989
+ $headers['Authorization'] = 'Bearer ' . resolveToken($auth['bearer']);
990
+ } elseif ($spec['kind'] === 'basic' && isset($auth['basic'])) {
991
+ $headers['Authorization'] =
992
+ 'Basic ' . base64_encode($auth['basic']['username'] . ':' . $auth['basic']['password']);
993
+ } elseif ($spec['kind'] === 'apiKey' && isset($auth['apiKey'][$spec['scheme']])) {
994
+ $value = resolveToken($auth['apiKey'][$spec['scheme']]);
995
+ if ($spec['in'] === 'query') {
996
+ $query[$spec['name']] = $value;
997
+ } elseif ($spec['in'] === 'cookie') {
998
+ $cookies[] = $spec['name'] . '=' . rawurlencode($value);
999
+ } else {
1000
+ $headers[$spec['name']] = $value;
1001
+ }
1002
+ } else {
1003
+ $satisfied = false;
1004
+ break;
1005
+ }
1006
+ }
1007
+ if ($satisfied) {
1008
+ return [$headers, $query, $cookies];
1009
+ }
1010
+ }
1011
+ return [[], [], []];
1012
+ }
1013
+
1014
+ /** Substitute \`{param}\` templates with encoded values and prefix the server URL. */
1015
+ function buildUrl(string $serverUrl, string $path, array $pathParams): string
1016
+ {
1017
+ foreach ($pathParams as $name => $value) {
1018
+ $path = str_replace('{' . $name . '}', rawurlencode((string) $value), $path);
1019
+ }
1020
+ return rtrim($serverUrl, '/') . $path;
1021
+ }
1022
+
1023
+ /** The default retry predicate: 5xx, 429, and transport timeouts/connect failures. */
1024
+ function defaultRetryOn(array $context): bool
1025
+ {
1026
+ if (($context['timedOut'] ?? false) === true) {
1027
+ return true;
1028
+ }
1029
+ $status = $context['status'] ?? 0;
1030
+ return $status >= 500 || $status === 429;
1031
+ }
1032
+
1033
+ /** Delay before the next attempt: \`Retry-After\` wins; otherwise jittered (fixed|exponential) backoff. */
1034
+ function retryDelay(int $attempt, array $retry, ?string $retryAfter): float
1035
+ {
1036
+ if ($retryAfter !== null && ctype_digit($retryAfter)) {
1037
+ return (float) $retryAfter;
1038
+ }
1039
+ $base = (float) ($retry['delay'] ?? 1.0);
1040
+ $strategy = $retry['strategy'] ?? 'exponential';
1041
+ $delay = $strategy === 'fixed' ? $base : $base * (2 ** ($attempt - 1));
1042
+ return $delay * (0.5 + mt_rand() / mt_getrandmax() / 2);
1043
+ }
1044
+
1045
+ /** Append query params in form style: list values repeat the key (\`tag=a&tag=b\`). */
1046
+ function appendQuery(string $url, array $query): string
1047
+ {
1048
+ $pairs = [];
1049
+ foreach ($query as $name => $value) {
1050
+ foreach (is_array($value) ? $value : [$value] as $single) {
1051
+ $encoded = is_bool($single) ? ($single ? 'true' : 'false') : (string) $single;
1052
+ $pairs[] = rawurlencode($name) . '=' . rawurlencode($encoded);
1053
+ }
1054
+ }
1055
+ if ($pairs === []) {
1056
+ return $url;
1057
+ }
1058
+ return $url . (str_contains($url, '?') ? '&' : '?') . implode('&', $pairs);
1059
+ }
1060
+
1061
+ /** One raw curl exchange. Returns \`['status', 'reason', 'headers', 'body', 'url', 'timedOut']\`. */
1062
+ function rawSend(Config $config, array $request): array
1063
+ {
1064
+ $url = appendQuery($request['url'], $request['query'] ?? []);
1065
+ $handle = curl_init($url);
1066
+ $headerLines = [];
1067
+ foreach ($request['headers'] ?? [] as $name => $value) {
1068
+ $headerLines[] = $name . ': ' . $value;
1069
+ }
1070
+ $responseHeaders = [];
1071
+ curl_setopt_array($handle, [
1072
+ CURLOPT_CUSTOMREQUEST => $request['method'],
1073
+ CURLOPT_RETURNTRANSFER => true,
1074
+ CURLOPT_HTTPHEADER => $headerLines,
1075
+ CURLOPT_HEADERFUNCTION => function ($ch, string $line) use (&$responseHeaders): int {
1076
+ $parts = explode(':', $line, 2);
1077
+ if (count($parts) === 2) {
1078
+ $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
1079
+ }
1080
+ return strlen($line);
1081
+ },
1082
+ ]);
1083
+ if (($request['body'] ?? null) !== null) {
1084
+ curl_setopt($handle, CURLOPT_POSTFIELDS, $request['body']);
1085
+ }
1086
+ if ($config->timeout !== null) {
1087
+ curl_setopt($handle, CURLOPT_TIMEOUT_MS, (int) round($config->timeout * 1000));
1088
+ }
1089
+ $body = curl_exec($handle);
1090
+ $errno = curl_errno($handle);
1091
+ $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
1092
+ $effectiveUrl = (string) curl_getinfo($handle, CURLINFO_EFFECTIVE_URL);
1093
+ if ($errno !== 0) {
1094
+ $timedOut = $errno === CURLE_OPERATION_TIMEDOUT || $errno === CURLE_COULDNT_CONNECT;
1095
+ return [
1096
+ 'status' => 0,
1097
+ 'reason' => curl_strerror($errno) ?? 'transport error',
1098
+ 'headers' => [],
1099
+ 'body' => '',
1100
+ 'url' => $effectiveUrl,
1101
+ 'timedOut' => $timedOut,
1102
+ ];
1103
+ }
1104
+ return [
1105
+ 'status' => $status,
1106
+ 'reason' => '',
1107
+ 'headers' => $responseHeaders,
1108
+ 'body' => is_string($body) ? $body : '',
1109
+ 'url' => $effectiveUrl,
1110
+ 'timedOut' => false,
1111
+ ];
1112
+ }
1113
+
1114
+ /**
1115
+ * Send with retries and middleware. \`$request\` carries \`operationId\`, \`method\`, \`url\`,
1116
+ * \`headers\`, \`query\`, and optional \`body\`/\`contentType\`/\`idempotencyKey\`.
1117
+ * Returns the raw response array; callers map status >= 400 to \`ApiError\`.
1118
+ */
1119
+ function send(Config $config, array $request): array
1120
+ {
1121
+ $headers = $request['headers'] ?? [];
1122
+ $headers['X-Redocly-Client'] = $config->clientHeader;
1123
+ if (($request['contentType'] ?? null) !== null) {
1124
+ $headers['Content-Type'] = $request['contentType'];
1125
+ }
1126
+ if (($request['idempotencyKey'] ?? null) !== null) {
1127
+ $headers['Idempotency-Key'] = $request['idempotencyKey'];
1128
+ }
1129
+ $request['headers'] = $headers;
1130
+
1131
+ $handler = fn (array $req): array => rawSend($config, $req);
1132
+ foreach (array_reverse($config->middleware) as $middleware) {
1133
+ $next = $handler;
1134
+ $handler = fn (array $req): array => $middleware($req, $next);
1135
+ }
1136
+
1137
+ $attempts = max(1, (int) ($config->retry['attempts'] ?? 3));
1138
+ $retryOn = $config->retry['retryOn'] ?? __NAMESPACE__ . '\\\\defaultRetryOn';
1139
+ $response = null;
1140
+ for ($attempt = 1; $attempt <= $attempts; $attempt++) {
1141
+ $response = $handler($request);
1142
+ $context = [
1143
+ 'status' => $response['status'],
1144
+ 'timedOut' => $response['timedOut'],
1145
+ 'attempt' => $attempt,
1146
+ 'operationId' => $request['operationId'] ?? '',
1147
+ ];
1148
+ if ($attempt === $attempts || !$retryOn($context)) {
1149
+ break;
1150
+ }
1151
+ $seconds = retryDelay($attempt, $config->retry, $response['headers']['retry-after'] ?? null);
1152
+ usleep((int) round($seconds * 1_000_000));
1153
+ }
1154
+ if ($response['timedOut']) {
1155
+ throw new TimeoutError($response['url'], $config->timeout, $attempts);
1156
+ }
1157
+ if ($response['status'] === 0) {
1158
+ throw new \\RuntimeException("Request to {$response['url']} failed: {$response['reason']}");
1159
+ }
1160
+ return $response;
1161
+ }
1162
+
1163
+ /** Decoded JSON body (assoc arrays), or null for empty bodies. */
1164
+ function decodeJson(array $response): mixed
1165
+ {
1166
+ if ($response['body'] === '') {
1167
+ return null;
1168
+ }
1169
+ return json_decode($response['body'], true);
1170
+ }
1171
+
1172
+ /** \`ApiError\` from a non-2xx response. */
1173
+ function apiErrorFrom(array $response): ApiError
1174
+ {
1175
+ return new ApiError($response['url'], $response['status'], $response['reason'], decodeJson($response));
1176
+ }
1177
+
1178
+ /** Walk an RFC 6901 JSON pointer over decoded JSON; null on any miss. */
1179
+ function resolvePointer(mixed $data, string $pointer): mixed
1180
+ {
1181
+ if ($pointer === '') {
1182
+ return $data;
1183
+ }
1184
+ foreach (explode('/', substr($pointer, 1)) as $token) {
1185
+ $key = str_replace(['~1', '~0'], ['/', '~'], $token);
1186
+ if (!is_array($data) || !array_key_exists($key, $data)) {
1187
+ return null;
1188
+ }
1189
+ $data = $data[$key];
1190
+ }
1191
+ return $data;
1192
+ }
1193
+
1194
+ /** The \`rel="next"\` target of a \`Link\` header, or null. */
1195
+ function linkNext(?string $header): ?string
1196
+ {
1197
+ if ($header === null) {
1198
+ return null;
1199
+ }
1200
+ foreach (explode(',', $header) as $part) {
1201
+ if (preg_match('/<([^>]+)>\\s*;[^,]*rel="?next"?/', trim($part), $match) === 1) {
1202
+ return $match[1];
1203
+ }
1204
+ }
1205
+ return null;
1206
+ }
1207
+
1208
+ /**
1209
+ * Auto-pagination: \`$call(array $params): [mixed rawPage, array $response]\`, \`$spec\` is the
1210
+ * normalized rule (\`style\`, \`param\`, \`nextCursor\`, \`hasMore\`, \`items\`), \`$base\` the caller's
1211
+ * query params. Yields raw decoded pages; generated wrappers hydrate them into models.
1212
+ */
1213
+ function iterPages(callable $call, array $spec, array $base): \\Generator
1214
+ {
1215
+ $params = $base;
1216
+ $style = $spec['style'];
1217
+ $seenCursors = [];
1218
+ $seenLinks = [];
1219
+ $offset = null;
1220
+ $page = null;
1221
+ while (true) {
1222
+ [$raw, $response] = $call($params);
1223
+ yield $raw;
1224
+ if ($style === 'cursor') {
1225
+ $next = resolvePointer($raw, $spec['nextCursor'] ?? '');
1226
+ if (isset($spec['hasMore']) && resolvePointer($raw, $spec['hasMore']) !== true) {
1227
+ return;
1228
+ }
1229
+ if (!is_string($next) || $next === '' || isset($seenCursors[$next])) {
1230
+ return;
1231
+ }
1232
+ $seenCursors[$next] = true;
1233
+ $params[$spec['param']] = $next;
1234
+ } elseif ($style === 'link') {
1235
+ $target = linkNext($response['headers']['link'] ?? null);
1236
+ if ($target === null || isset($seenLinks[$target])) {
1237
+ return;
1238
+ }
1239
+ $seenLinks[$target] = true;
1240
+ $parsed = parse_url($target);
1241
+ $linkParams = [];
1242
+ parse_str($parsed['query'] ?? '', $linkParams);
1243
+ $params = array_merge($params, $linkParams);
1244
+ } else {
1245
+ $items = resolvePointer($raw, $spec['items'] ?? '');
1246
+ $count = is_array($items) ? count($items) : 0;
1247
+ if ($count === 0) {
1248
+ return;
1249
+ }
1250
+ if ($style === 'offset') {
1251
+ $offset = ($offset ?? (int) ($base[$spec['param']] ?? 0)) + $count;
1252
+ $params[$spec['param']] = $offset;
1253
+ } else {
1254
+ $page = ($page ?? (int) ($base[$spec['param']] ?? 1)) + 1;
1255
+ $params[$spec['param']] = $page;
1256
+ }
1257
+ }
1258
+ }
1259
+ }
1260
+
1261
+ /** Parse one SSE frame; returns \`[?ServerSentEvent, ?string lastEventId, ?int retryMs]\`. */
1262
+ function parseSseFrame(string $frame, bool $jsonData): array
1263
+ {
1264
+ $event = 'message';
1265
+ $dataLines = [];
1266
+ $id = null;
1267
+ $retry = null;
1268
+ foreach (explode("\\n", str_replace("\\r\\n", "\\n", $frame)) as $line) {
1269
+ if ($line === '' || str_starts_with($line, ':')) {
1270
+ continue;
1271
+ }
1272
+ $colon = strpos($line, ':');
1273
+ $field = $colon === false ? $line : substr($line, 0, $colon);
1274
+ $value = $colon === false ? '' : ltrim(substr($line, $colon + 1), ' ');
1275
+ if ($field === 'event') {
1276
+ $event = $value;
1277
+ } elseif ($field === 'data') {
1278
+ $dataLines[] = $value;
1279
+ } elseif ($field === 'id') {
1280
+ $id = $value;
1281
+ } elseif ($field === 'retry' && ctype_digit($value)) {
1282
+ $retry = (int) $value;
1283
+ }
1284
+ }
1285
+ if ($dataLines === [] && $id === null && $retry === null) {
1286
+ return [null, null, $retry];
1287
+ }
1288
+ $data = implode("\\n", $dataLines);
1289
+ $decoded = $jsonData && $data !== '' ? json_decode($data, true) : $data;
1290
+ return [new ServerSentEvent($event, $decoded, $id, $retry), $id, $retry];
1291
+ }
1292
+
1293
+ /**
1294
+ * Stream server-sent events. \`$open(array $extraHeaders): \\CurlHandle\` returns a configured
1295
+ * (not yet executed) handle; this pump drives it with curl_multi, yields parsed frames, and
1296
+ * reconnects with \`Last-Event-ID\` on transient failures (4xx is definitive; backoff <= 30s).
1297
+ */
1298
+ function iterSse(callable $open, bool $jsonData): \\Generator
1299
+ {
1300
+ $lastEventId = null;
1301
+ $retryMs = 3000;
1302
+ while (true) {
1303
+ $extra = ['Accept' => 'text/event-stream'];
1304
+ if ($lastEventId !== null) {
1305
+ $extra['Last-Event-ID'] = $lastEventId;
1306
+ }
1307
+ $handle = $open($extra);
1308
+ $buffer = '';
1309
+ curl_setopt($handle, CURLOPT_WRITEFUNCTION, function ($ch, string $chunk) use (&$buffer): int {
1310
+ $buffer .= $chunk;
1311
+ return strlen($chunk);
1312
+ });
1313
+ $multi = curl_multi_init();
1314
+ curl_multi_add_handle($multi, $handle);
1315
+ do {
1316
+ curl_multi_exec($multi, $running);
1317
+ if ($running > 0) {
1318
+ curl_multi_select($multi, 0.1);
1319
+ }
1320
+ while (($split = strpos($buffer, "\\n\\n")) !== false || ($split = strpos($buffer, "\\r\\n\\r\\n")) !== false) {
1321
+ $frameLength = $buffer[$split] === "\\r" ? 4 : 2;
1322
+ $frame = substr($buffer, 0, $split);
1323
+ $buffer = substr($buffer, $split + $frameLength);
1324
+ [$event, $id, $retry] = parseSseFrame($frame, $jsonData);
1325
+ if ($id !== null) {
1326
+ $lastEventId = $id;
1327
+ }
1328
+ if ($retry !== null) {
1329
+ $retryMs = min($retry, 30000);
1330
+ }
1331
+ if ($event !== null) {
1332
+ yield $event;
1333
+ }
1334
+ }
1335
+ } while ($running > 0);
1336
+ $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
1337
+ $url = (string) curl_getinfo($handle, CURLINFO_EFFECTIVE_URL);
1338
+ curl_multi_remove_handle($multi, $handle);
1339
+ curl_multi_close($multi);
1340
+ if ($status >= 400 && $status < 500) {
1341
+ throw new ApiError($url, $status, '', $buffer);
1342
+ }
1343
+ // A clean 200 end-of-stream is done; anything else reconnects with Last-Event-ID.
1344
+ if ($status === 200) {
1345
+ return;
1346
+ }
1347
+ usleep($retryMs * 1000);
1348
+ }
1349
+ }
1350
+
1351
+ /** Encode an assoc body as \`multipart/form-data\`; nested values are JSON parts. Returns \`[contentType, body]\`. */
1352
+ function toMultipart(array $body): array
1353
+ {
1354
+ $boundary = 'redocly-' . bin2hex(random_bytes(12));
1355
+ $parts = '';
1356
+ foreach ($body as $name => $value) {
1357
+ $parts .= "--{$boundary}\\r\\n";
1358
+ if (is_array($value)) {
1359
+ $parts .= "Content-Disposition: form-data; name=\\"{$name}\\"\\r\\n";
1360
+ $parts .= "Content-Type: application/json\\r\\n\\r\\n";
1361
+ $parts .= json_encode($value) . "\\r\\n";
1362
+ } else {
1363
+ $parts .= "Content-Disposition: form-data; name=\\"{$name}\\"\\r\\n\\r\\n";
1364
+ $parts .= (is_bool($value) ? ($value ? 'true' : 'false') : (string) $value) . "\\r\\n";
1365
+ }
1366
+ }
1367
+ $parts .= "--{$boundary}--\\r\\n";
1368
+ return ['multipart/form-data; boundary=' . $boundary, $parts];
1369
+ }
1370
+ `;var n={"_errors.py":`# Runtime errors and the result-mode envelope for generated Python clients.
1371
+ # Hand-authored once, embedded into every generated client (see
1372
+ # scripts/generate-runtime-sources.mjs) \u2014 mirror of the TypeScript runtime's
1373
+ # errors.ts, kept semantically in lockstep.
1374
+ from __future__ import annotations
1375
+
1376
+ from dataclasses import dataclass
1377
+ from typing import Any, Generic, Optional, TypeVar
1378
+
1379
+ T = TypeVar("T")
1380
+ E = TypeVar("E")
1381
+
1382
+
1383
+ class ApiError(Exception):
1384
+ """Raised (throw mode) for a non-2xx response, carrying the decoded error body."""
1385
+
1386
+ def __init__(self, url: str, status: int, status_text: str, body: Any) -> None:
1387
+ super().__init__(f"Request failed with status {status}")
1388
+ self.url = url
1389
+ self.status = status
1390
+ self.status_text = status_text
1391
+ self.body = body
1392
+
1393
+
1394
+ class ApiTimeoutError(Exception):
1395
+ """Raised when a request attempt exceeds the configured timeout \u2014 carries the
1396
+ context a log line needs (which operation, what budget, which attempt)."""
1397
+
1398
+ def __init__(self, operation_id: str, timeout: float, attempt: int) -> None:
1399
+ super().__init__(
1400
+ f'Request "{operation_id}" timed out after {timeout} s (attempt {attempt})'
1401
+ )
1402
+ self.operation_id = operation_id
1403
+ self.timeout = timeout
1404
+ self.attempt = attempt
1405
+
1406
+
1407
+ @dataclass
1408
+ class Result(Generic[T, E]):
1409
+ """Result-mode return shape: exactly one of \`data\`/\`error\` is set."""
1410
+
1411
+ data: Optional[T]
1412
+ error: Optional[E]
1413
+ response: Any # httpx.Response
1414
+
1415
+ @property
1416
+ def ok(self) -> bool:
1417
+ return self.error is None
1418
+ `,"_auth.py":`# Auth resolution for generated Python clients \u2014 mirror of the TypeScript
1419
+ # runtime's auth.ts: the first OR-alternative whose schemes are all configured
1420
+ # is applied, so "bearer OR apiKey" works with either credential and never
1421
+ # sends both. Cookie-borne api keys fold into a single Cookie header.
1422
+ from __future__ import annotations
1423
+
1424
+ import base64
1425
+ from typing import Any, Callable, Dict, List, Tuple, Union
1426
+ from urllib.parse import quote
1427
+
1428
+ TokenProvider = Union[str, Callable[[], str]]
1429
+
1430
+
1431
+ def _api_keys(auth: Dict[str, Any]) -> Dict[str, Any]:
1432
+ """The apiKey credentials. \`apiKey\` is the documented key (it matches the scheme
1433
+ kind and the other language SDKs); \`api_key\` is accepted too, so a snake_case
1434
+ config keeps working."""
1435
+ return {**(auth.get("api_key") or {}), **(auth.get("apiKey") or {})}
1436
+
1437
+ def _resolve_token(provider: TokenProvider) -> str:
1438
+ return provider() if callable(provider) else provider
1439
+
1440
+
1441
+ def _is_configured(scheme: Dict[str, Any], auth: Dict[str, Any]) -> bool:
1442
+ kind = scheme["kind"]
1443
+ if kind == "apiKey":
1444
+ return scheme["scheme"] in _api_keys(auth)
1445
+ if kind == "bearer":
1446
+ return auth.get("bearer") is not None
1447
+ return auth.get("basic") is not None
1448
+
1449
+
1450
+ def resolve_auth(
1451
+ security: List[List[Dict[str, Any]]], auth: Dict[str, Any]
1452
+ ) -> Tuple[Dict[str, str], Dict[str, str]]:
1453
+ """Build (headers, query) for one operation's security OR-alternatives from
1454
+ the client credentials. When no alternative is fully configured, the first
1455
+ alternative's configured schemes are still sent (the server rejects the
1456
+ request \u2014 same behavior as the TypeScript runtime)."""
1457
+ alternative = next(
1458
+ (schemes for schemes in security if all(_is_configured(s, auth) for s in schemes)),
1459
+ security[0] if security else [],
1460
+ )
1461
+ headers: Dict[str, str] = {}
1462
+ query: Dict[str, str] = {}
1463
+ cookies: List[str] = []
1464
+ for scheme in alternative:
1465
+ kind = scheme["kind"]
1466
+ if kind == "apiKey":
1467
+ provider = _api_keys(auth).get(scheme["scheme"])
1468
+ if provider is None:
1469
+ continue
1470
+ value = _resolve_token(provider)
1471
+ location = scheme.get("in", "header")
1472
+ if location == "header":
1473
+ headers[scheme["name"]] = value
1474
+ elif location == "query":
1475
+ query[scheme["name"]] = value
1476
+ else:
1477
+ # Reserved characters (\`;\`, \`=\`, space) must not break Cookie syntax.
1478
+ cookies.append(f"{scheme['name']}={quote(value, safe='')}")
1479
+ elif kind == "bearer":
1480
+ provider = auth.get("bearer")
1481
+ if provider is not None:
1482
+ headers["Authorization"] = f"Bearer {_resolve_token(provider)}"
1483
+ else:
1484
+ basic = auth.get("basic")
1485
+ if basic is not None:
1486
+ username, password = basic["username"], basic["password"]
1487
+ token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii")
1488
+ headers["Authorization"] = f"Basic {token}"
1489
+ if cookies:
1490
+ headers["Cookie"] = "; ".join(cookies)
1491
+ return headers, query
1492
+ `,"_url.py":`# URL assembly for generated Python clients \u2014 path-parameter substitution with
1493
+ # percent-encoding, mirroring the TypeScript runtime's url.ts semantics.
1494
+ from __future__ import annotations
1495
+
1496
+ from typing import Any, Dict
1497
+ from urllib.parse import quote
1498
+
1499
+
1500
+ def build_url(server_url: str, path: str, path_params: Dict[str, Any]) -> str:
1501
+ filled = path
1502
+ for name, value in path_params.items():
1503
+ filled = filled.replace("{" + name + "}", quote(str(value), safe=""))
1504
+ return server_url.rstrip("/") + filled
1505
+ `,"_decode.py":`# Reflective JSON <-> model conversion for generated Python clients. Models are
1506
+ # plain dataclasses by default, or pydantic BaseModels under \`models: pydantic\`;
1507
+ # one decoder serves both. For a dataclass it hydrates parsed JSON reflectively,
1508
+ # honoring each class's \`_field_map\` (python name -> wire name) and the typing
1509
+ # constructs the generator emits: Optional/Union, List, Dict, Enum, Literal, Any.
1510
+ # For a pydantic model it defers to pydantic, which already knows the aliases.
1511
+ # encode() mirrors whichever it was given back to wire shape.
1512
+ from __future__ import annotations
1513
+
1514
+ import dataclasses
1515
+ import typing
1516
+ from datetime import date, datetime
1517
+ from enum import Enum
1518
+ from typing import Any, Dict, Tuple, get_args, get_origin, get_type_hints
1519
+
1520
+ # Discriminated unions: resolved Union annotation -> (wire property, {value: class}).
1521
+ # The generated module registers its unions here; decode() dispatches through it
1522
+ # before falling back to trying members in order.
1523
+ DISCRIMINATORS: Dict[Any, Tuple[str, Dict[str, Any]]] = {}
1524
+
1525
+
1526
+ def decode(type_: Any, data: Any):
1527
+ """Best-effort hydration: wire data -> the annotated Python shape. Unknown or
1528
+ mismatched shapes pass through unchanged (the server is the source of truth)."""
1529
+ if data is None or type_ is Any or type_ is None:
1530
+ return data
1531
+ # \`Annotated[Union[...], Field(discriminator=...)]\`: pydantic reads that annotation on a
1532
+ # model's own field, so here only the union underneath matters.
1533
+ if hasattr(type_, "__metadata__"):
1534
+ type_ = get_args(type_)[0]
1535
+ origin = get_origin(type_)
1536
+ if origin is typing.Union:
1537
+ discriminator = DISCRIMINATORS.get(type_)
1538
+ if discriminator is not None and isinstance(data, dict):
1539
+ wire_property, mapping = discriminator
1540
+ target = mapping.get(data.get(wire_property))
1541
+ if target is not None:
1542
+ try:
1543
+ return decode(target, data)
1544
+ except (TypeError, ValueError, KeyError):
1545
+ pass
1546
+ for member in get_args(type_):
1547
+ if member is type(None):
1548
+ continue
1549
+ try:
1550
+ return decode(member, data)
1551
+ except (TypeError, ValueError, KeyError):
1552
+ continue
1553
+ return data
1554
+ if origin is list:
1555
+ (item_type,) = get_args(type_) or (Any,)
1556
+ return [decode(item_type, item) for item in data]
1557
+ if origin is dict:
1558
+ args = get_args(type_)
1559
+ value_type = args[1] if len(args) == 2 else Any
1560
+ return {key: decode(value_type, value) for key, value in data.items()}
1561
+ if origin is typing.Literal:
1562
+ return data
1563
+ if isinstance(type_, type) and issubclass(type_, Enum):
1564
+ return type_(data)
1565
+ # \`dateType: Date\` annotates date/date-time fields as datetime objects; a value that
1566
+ # doesn't parse passes through unchanged (the server is the source of truth).
1567
+ if type_ is datetime or type_ is date:
1568
+ if not isinstance(data, str):
1569
+ return data
1570
+ try:
1571
+ # \`datetime\` accepts a bare date too; \`date\` rejects a timestamp, so trim it.
1572
+ return (
1573
+ datetime.fromisoformat(data)
1574
+ if type_ is datetime
1575
+ else date.fromisoformat(data[:10])
1576
+ )
1577
+ except ValueError:
1578
+ return data
1579
+ # A pydantic model validates itself, aliases included. \`ValidationError\`
1580
+ # subclasses \`ValueError\`, so union member probing above still works.
1581
+ if isinstance(type_, type) and hasattr(type_, "model_validate"):
1582
+ return type_.model_validate(data)
1583
+ if dataclasses.is_dataclass(type_):
1584
+ hints = get_type_hints(type_)
1585
+ field_map = getattr(type_, "_field_map", {})
1586
+ kwargs = {}
1587
+ for field in dataclasses.fields(type_):
1588
+ wire = field_map.get(field.name, field.name)
1589
+ if isinstance(data, dict) and wire in data:
1590
+ kwargs[field.name] = decode(hints.get(field.name, Any), data[wire])
1591
+ return type_(**kwargs)
1592
+ return data
1593
+
1594
+
1595
+ def encode(value: Any):
1596
+ """Python shape -> wire (JSON) shape; inverse of decode for request bodies."""
1597
+ # \`mode="json"\` resolves datetimes and enums the same way the branches below do,
1598
+ # and \`exclude_none\` matches the dataclass path: an unset optional is not sent.
1599
+ if hasattr(value, "model_dump") and not isinstance(value, type):
1600
+ return value.model_dump(by_alias=True, exclude_none=True, mode="json")
1601
+ if dataclasses.is_dataclass(value) and not isinstance(value, type):
1602
+ field_map = getattr(type(value), "_field_map", {})
1603
+ out = {}
1604
+ for field in dataclasses.fields(value):
1605
+ item = getattr(value, field.name)
1606
+ if item is None:
1607
+ continue
1608
+ out[field_map.get(field.name, field.name)] = encode(item)
1609
+ return out
1610
+ if isinstance(value, Enum):
1611
+ return value.value
1612
+ # A date-only value must not gain a time component on the way out.
1613
+ if isinstance(value, datetime):
1614
+ return value.isoformat()
1615
+ if isinstance(value, date):
1616
+ return value.isoformat()
1617
+ if isinstance(value, list):
1618
+ return [encode(item) for item in value]
1619
+ if isinstance(value, dict):
1620
+ return {key: encode(item) for key, item in value.items()}
1621
+ return value
1622
+ `,"_send.py":`# The request core for generated Python clients \u2014 mirror of the TypeScript
1623
+ # runtime's send.ts: default + config + per-call headers, on_request middleware
1624
+ # BEFORE serialization (mutations are sent), the retry loop (idempotent-methods
1625
+ # default, Idempotency-Key opt-in makes POST/PATCH safe, Retry-After honored,
1626
+ # exponential backoff with full jitter, a fresh timeout budget per attempt), and
1627
+ # the reverse on_response onion.
1628
+ from __future__ import annotations
1629
+
1630
+ import asyncio
1631
+ import random
1632
+ import time
1633
+ import uuid
1634
+ from dataclasses import dataclass
1635
+ from typing import Any, Dict, Generic, List, Optional, Tuple, TypeVar
1636
+
1637
+ import httpx
1638
+
1639
+ from ._errors import ApiTimeoutError
1640
+
1641
+ T = TypeVar("T")
1642
+
1643
+
1644
+ @dataclass
1645
+ class Envelope(Generic[T]):
1646
+ """A *_with_headers() result: decoded body + coerced declared headers + raw response."""
1647
+
1648
+ data: T
1649
+ headers: Dict[str, Any]
1650
+ response: httpx.Response
1651
+
1652
+
1653
+ def read_envelope_headers(
1654
+ response: httpx.Response, specs: List[Tuple[str, str, str]]
1655
+ ) -> Dict[str, Any]:
1656
+ """Coerce declared response headers per (name, key, type) specs; absent/unparsable omitted."""
1657
+ headers: Dict[str, Any] = {}
1658
+ for name, key, type_ in specs:
1659
+ raw = response.headers.get(name)
1660
+ if raw is None:
1661
+ continue
1662
+ if type_ in ("integer", "number"):
1663
+ try:
1664
+ headers[key] = int(raw) if type_ == "integer" else float(raw)
1665
+ except ValueError:
1666
+ pass
1667
+ elif type_ == "boolean":
1668
+ lower = raw.strip().lower()
1669
+ if lower in ("true", "false"):
1670
+ headers[key] = lower == "true"
1671
+ else:
1672
+ headers[key] = raw
1673
+ return headers
1674
+
1675
+
1676
+ _IDEMPOTENT_METHODS = {"GET", "HEAD", "PUT", "DELETE", "OPTIONS"}
1677
+ _TRANSIENT_STATUS = {408, 429, 500, 502, 503, 504}
1678
+
1679
+
1680
+ def _default_retry_on(method: str, headers: Dict[str, str], response: Optional[httpx.Response]) -> bool:
1681
+ safe = method.upper() in _IDEMPOTENT_METHODS or "Idempotency-Key" in headers
1682
+ if not safe:
1683
+ return False
1684
+ return response is None or response.status_code in _TRANSIENT_STATUS
1685
+
1686
+
1687
+ def _retry_delay(retry: Dict[str, Any], attempt: int, retry_after: Optional[str]) -> float:
1688
+ if retry_after:
1689
+ try:
1690
+ return float(retry_after)
1691
+ except ValueError:
1692
+ pass # HTTP-date form: fall through to backoff
1693
+ base = float(retry.get("retry_delay", 1.0))
1694
+ raw = base if retry.get("retry_strategy") == "fixed" else base * (2 ** (attempt - 1))
1695
+ return random.uniform(0, raw) if retry.get("jitter", True) is not False else raw
1696
+
1697
+
1698
+ def send(
1699
+ client: httpx.Client,
1700
+ config: Dict[str, Any],
1701
+ op: Dict[str, Any],
1702
+ url: str,
1703
+ *,
1704
+ method: str,
1705
+ headers: Optional[Dict[str, str]] = None,
1706
+ params: Optional[Dict[str, Any]] = None,
1707
+ json_body: Any = None,
1708
+ content: Any = None,
1709
+ data: Any = None,
1710
+ files: Any = None,
1711
+ timeout: Optional[float] = None,
1712
+ idempotency_key: Any = None,
1713
+ retry: Optional[Dict[str, Any]] = None,
1714
+ ) -> httpx.Response:
1715
+ merged_retry: Dict[str, Any] = {**(config.get("retry") or {}), **(retry or {})}
1716
+ effective_timeout = timeout if timeout is not None else config.get("timeout")
1717
+ merged_headers: Dict[str, str] = {**(config.get("headers") or {}), **(headers or {})}
1718
+
1719
+ # One stable key per LOGICAL call \u2014 set before the retry loop so every
1720
+ # attempt re-sends the same key; a caller-provided header always wins.
1721
+ key = idempotency_key if idempotency_key is not None else config.get("idempotency_key")
1722
+ if (
1723
+ key not in (None, False)
1724
+ and method.upper() in ("POST", "PATCH")
1725
+ and "Idempotency-Key" not in merged_headers
1726
+ ):
1727
+ merged_headers["Idempotency-Key"] = (
1728
+ key if isinstance(key, str) else key() if callable(key) else str(uuid.uuid4())
1729
+ )
1730
+
1731
+ context = {
1732
+ "url": url,
1733
+ "method": method.upper(),
1734
+ "headers": merged_headers,
1735
+ "body": json_body,
1736
+ "operation": op,
1737
+ }
1738
+ middleware: List[Any] = config.get("middleware") or []
1739
+ for mw in middleware:
1740
+ on_request = getattr(mw, "on_request", None) or (mw.get("on_request") if isinstance(mw, dict) else None)
1741
+ if on_request:
1742
+ on_request(context)
1743
+
1744
+ max_attempts = 1 + int(merged_retry.get("retries", 0))
1745
+ retry_on = merged_retry.get("retry_on") or (
1746
+ lambda ctx: _default_retry_on(context["method"], context["headers"], ctx.get("response"))
1747
+ )
1748
+
1749
+ attempt = 0
1750
+ while True:
1751
+ attempt += 1
1752
+ try:
1753
+ response = client.request(
1754
+ context["method"],
1755
+ context["url"],
1756
+ headers=context["headers"],
1757
+ params=params,
1758
+ json=context["body"] if content is None and files is None and data is None else None,
1759
+ content=content,
1760
+ data=data,
1761
+ files=files,
1762
+ timeout=effective_timeout if effective_timeout is not None else httpx.USE_CLIENT_DEFAULT,
1763
+ )
1764
+ except httpx.TimeoutException:
1765
+ if attempt < max_attempts and retry_on({"attempt": attempt, "response": None}):
1766
+ time.sleep(_retry_delay(merged_retry, attempt, None))
1767
+ continue
1768
+ raise ApiTimeoutError(op.get("id", "?"), float(effective_timeout or 0), attempt) from None
1769
+ except httpx.TransportError:
1770
+ if attempt < max_attempts and retry_on({"attempt": attempt, "response": None}):
1771
+ time.sleep(_retry_delay(merged_retry, attempt, None))
1772
+ continue
1773
+ raise
1774
+
1775
+ for mw in reversed(middleware):
1776
+ on_response = getattr(mw, "on_response", None) or (mw.get("on_response") if isinstance(mw, dict) else None)
1777
+ if on_response:
1778
+ replaced = on_response(response, context)
1779
+ if replaced is not None:
1780
+ response = replaced
1781
+
1782
+ if (
1783
+ not response.is_success
1784
+ and attempt < max_attempts
1785
+ and retry_on({"attempt": attempt, "response": response})
1786
+ ):
1787
+ time.sleep(_retry_delay(merged_retry, attempt, response.headers.get("retry-after")))
1788
+ continue
1789
+ return response
1790
+
1791
+
1792
+ async def send_async(
1793
+ client: httpx.AsyncClient,
1794
+ config: Dict[str, Any],
1795
+ op: Dict[str, Any],
1796
+ url: str,
1797
+ *,
1798
+ method: str,
1799
+ headers: Optional[Dict[str, str]] = None,
1800
+ params: Optional[Dict[str, Any]] = None,
1801
+ json_body: Any = None,
1802
+ content: Any = None,
1803
+ data: Any = None,
1804
+ files: Any = None,
1805
+ timeout: Optional[float] = None,
1806
+ idempotency_key: Any = None,
1807
+ retry: Optional[Dict[str, Any]] = None,
1808
+ ) -> httpx.Response:
1809
+ """The async mirror of send() \u2014 same retry/timeout/idempotency semantics."""
1810
+ merged_retry: Dict[str, Any] = {**(config.get("retry") or {}), **(retry or {})}
1811
+ effective_timeout = timeout if timeout is not None else config.get("timeout")
1812
+ merged_headers: Dict[str, str] = {**(config.get("headers") or {}), **(headers or {})}
1813
+ key = idempotency_key if idempotency_key is not None else config.get("idempotency_key")
1814
+ if (
1815
+ key not in (None, False)
1816
+ and method.upper() in ("POST", "PATCH")
1817
+ and "Idempotency-Key" not in merged_headers
1818
+ ):
1819
+ merged_headers["Idempotency-Key"] = (
1820
+ key if isinstance(key, str) else key() if callable(key) else str(uuid.uuid4())
1821
+ )
1822
+ context = {
1823
+ "url": url,
1824
+ "method": method.upper(),
1825
+ "headers": merged_headers,
1826
+ "body": json_body,
1827
+ "operation": op,
1828
+ }
1829
+ middleware: List[Any] = config.get("middleware") or []
1830
+ for mw in middleware:
1831
+ on_request = getattr(mw, "on_request", None) or (mw.get("on_request") if isinstance(mw, dict) else None)
1832
+ if on_request:
1833
+ on_request(context)
1834
+ max_attempts = 1 + int(merged_retry.get("retries", 0))
1835
+ retry_on = merged_retry.get("retry_on") or (
1836
+ lambda ctx: _default_retry_on(context["method"], context["headers"], ctx.get("response"))
1837
+ )
1838
+ attempt = 0
1839
+ while True:
1840
+ attempt += 1
1841
+ try:
1842
+ response = await client.request(
1843
+ context["method"],
1844
+ context["url"],
1845
+ headers=context["headers"],
1846
+ params=params,
1847
+ json=context["body"] if content is None and files is None and data is None else None,
1848
+ content=content,
1849
+ data=data,
1850
+ files=files,
1851
+ timeout=effective_timeout if effective_timeout is not None else httpx.USE_CLIENT_DEFAULT,
1852
+ )
1853
+ except httpx.TimeoutException:
1854
+ if attempt < max_attempts and retry_on({"attempt": attempt, "response": None}):
1855
+ await asyncio.sleep(_retry_delay(merged_retry, attempt, None))
1856
+ continue
1857
+ raise ApiTimeoutError(op.get("id", "?"), float(effective_timeout or 0), attempt) from None
1858
+ except httpx.TransportError:
1859
+ if attempt < max_attempts and retry_on({"attempt": attempt, "response": None}):
1860
+ await asyncio.sleep(_retry_delay(merged_retry, attempt, None))
1861
+ continue
1862
+ raise
1863
+ for mw in reversed(middleware):
1864
+ on_response = getattr(mw, "on_response", None) or (mw.get("on_response") if isinstance(mw, dict) else None)
1865
+ if on_response:
1866
+ replaced = on_response(response, context)
1867
+ if replaced is not None:
1868
+ response = replaced
1869
+ if (
1870
+ not response.is_success
1871
+ and attempt < max_attempts
1872
+ and retry_on({"attempt": attempt, "response": response})
1873
+ ):
1874
+ await asyncio.sleep(_retry_delay(merged_retry, attempt, response.headers.get("retry-after")))
1875
+ continue
1876
+ return response
1877
+ `,"_paginate.py":`# Auto-pagination iterators for generated Python clients \u2014 the TypeScript
1878
+ # runtime's paginate.ts semantics ported: cursor (next-cursor pointer, optional
1879
+ # has-more flag, repeated-cursor guard), offset/page (advance by count/one,
1880
+ # repeated-page guard, null start treated as absent), and link (RFC 8288
1881
+ # \`Link: rel="next"\` following with relative resolution and a loop guard).
1882
+ from __future__ import annotations
1883
+
1884
+ import re
1885
+ from typing import Any, AsyncIterator, Awaitable, Callable, Dict, Iterator, Optional, Tuple
1886
+ from urllib.parse import parse_qsl, urljoin, urlparse
1887
+
1888
+ # call(params) -> (parsed_json, httpx.Response)
1889
+ PageCall = Callable[[Dict[str, Any]], Tuple[Any, Any]]
1890
+
1891
+
1892
+ def resolve_pointer(data: Any, pointer: str) -> Any:
1893
+ """RFC 6901 JSON pointer over parsed JSON; None on any miss."""
1894
+ if pointer == "":
1895
+ return data
1896
+ if not pointer.startswith("/"):
1897
+ return None
1898
+ current = data
1899
+ for token in pointer[1:].split("/"):
1900
+ key = token.replace("~1", "/").replace("~0", "~")
1901
+ if isinstance(current, dict):
1902
+ current = current.get(key)
1903
+ elif isinstance(current, list) and key.isdigit():
1904
+ index = int(key)
1905
+ current = current[index] if index < len(current) else None
1906
+ else:
1907
+ return None
1908
+ if current is None:
1909
+ return None
1910
+ return current
1911
+
1912
+
1913
+ def iter_pages(call: PageCall, spec: Dict[str, Any], params: Optional[Dict[str, Any]] = None) -> Iterator[Any]:
1914
+ """Yield raw page JSON per the pagination spec; every page is yielded before
1915
+ the stop condition is evaluated, so the last page always arrives."""
1916
+ style = spec["style"]
1917
+ base = dict(params or {})
1918
+ if style == "cursor":
1919
+ cursor = base.get(spec["param"])
1920
+ while True:
1921
+ page_params = dict(base)
1922
+ if cursor is not None:
1923
+ page_params[spec["param"]] = cursor
1924
+ page, _response = call(page_params)
1925
+ yield page
1926
+ if spec.get("has_more") is not None and resolve_pointer(page, spec["has_more"]) is False:
1927
+ return
1928
+ nxt = resolve_pointer(page, spec.get("next_cursor", ""))
1929
+ if nxt is None or nxt == "":
1930
+ return
1931
+ if not isinstance(nxt, (str, int, float)):
1932
+ raise ValueError(f"Pagination cursor at {spec['next_cursor']} is not a string or number")
1933
+ if nxt == cursor:
1934
+ raise ValueError("Pagination did not advance: the operation returned the same cursor twice")
1935
+ cursor = nxt
1936
+ elif style == "link":
1937
+ yield from _iter_pages_by_link(call, base)
1938
+ else: # offset / page
1939
+ start = base.get(spec["param"])
1940
+ fallback = 1 if style == "page" else 0
1941
+ try:
1942
+ position = fallback if start in (None, "") else int(start)
1943
+ except (TypeError, ValueError):
1944
+ position = fallback
1945
+ previous_items = None
1946
+ while True:
1947
+ page, _response = call({**base, spec["param"]: position})
1948
+ items = resolve_pointer(page, spec.get("items", ""))
1949
+ serialized = repr(items) if isinstance(items, list) else None
1950
+ if serialized is not None and serialized == previous_items:
1951
+ raise ValueError("Pagination did not advance: the operation returned the same page twice")
1952
+ yield page
1953
+ if not isinstance(items, list) or len(items) == 0:
1954
+ return
1955
+ previous_items = serialized
1956
+ position += 1 if style == "page" else len(items)
1957
+
1958
+
1959
+ def _link_next(header: Optional[str]) -> Optional[str]:
1960
+ if not header:
1961
+ return None
1962
+ for entry in re.split(r",\\s*(?=<)", header):
1963
+ match = re.match(r"^\\s*<([^>]*)>(.*)$", entry)
1964
+ if not match:
1965
+ continue
1966
+ rel = re.search(r';\\s*rel\\s*=\\s*"?([^";]+)"?', match.group(2), re.IGNORECASE)
1967
+ if rel and "next" in rel.group(1).split():
1968
+ return match.group(1)
1969
+ return None
1970
+
1971
+
1972
+ def _iter_pages_by_link(call: PageCall, base: Dict[str, Any]) -> Iterator[Any]:
1973
+ params = dict(base)
1974
+ previous = None
1975
+ while True:
1976
+ page, response = call(params)
1977
+ yield page
1978
+ target = _link_next(response.headers.get("link"))
1979
+ if target is None:
1980
+ return
1981
+ page_url = str(response.request.url) if response.request is not None else ""
1982
+ nxt = urljoin(page_url or "http://relative.invalid", target)
1983
+ if nxt in (previous, page_url):
1984
+ raise ValueError('Pagination did not advance: the Link rel="next" target repeats')
1985
+ previous = nxt
1986
+ link_params: Dict[str, Any] = {}
1987
+ for key, value in parse_qsl(urlparse(nxt).query):
1988
+ if key in link_params:
1989
+ existing = link_params[key]
1990
+ link_params[key] = [*existing, value] if isinstance(existing, list) else [existing, value]
1991
+ else:
1992
+ link_params[key] = value
1993
+ params = {**base, **link_params}
1994
+
1995
+
1996
+ def iter_items(call: PageCall, spec: Dict[str, Any], params: Optional[Dict[str, Any]] = None) -> Iterator[Any]:
1997
+ """Each page's \`items\` pointer, flattened."""
1998
+ for page in iter_pages(call, spec, params):
1999
+ items = resolve_pointer(page, spec.get("items", ""))
2000
+ if isinstance(items, list):
2001
+ yield from items
2002
+
2003
+
2004
+ # call(params) -> awaitable of (parsed_json, httpx.Response)
2005
+ AsyncPageCall = Callable[[Dict[str, Any]], Awaitable[Tuple[Any, Any]]]
2006
+
2007
+
2008
+ async def aiter_pages(
2009
+ call: AsyncPageCall, spec: Dict[str, Any], params: Optional[Dict[str, Any]] = None
2010
+ ) -> AsyncIterator[Any]:
2011
+ """Async mirror of iter_pages \u2014 same stop conditions and guards."""
2012
+ style = spec["style"]
2013
+ base = dict(params or {})
2014
+ if style == "cursor":
2015
+ cursor = base.get(spec["param"])
2016
+ while True:
2017
+ page_params = dict(base)
2018
+ if cursor is not None:
2019
+ page_params[spec["param"]] = cursor
2020
+ page, _response = await call(page_params)
2021
+ yield page
2022
+ if spec.get("has_more") is not None and resolve_pointer(page, spec["has_more"]) is False:
2023
+ return
2024
+ nxt = resolve_pointer(page, spec.get("next_cursor", ""))
2025
+ if nxt is None or nxt == "":
2026
+ return
2027
+ if not isinstance(nxt, (str, int, float)):
2028
+ raise ValueError(f"Pagination cursor at {spec['next_cursor']} is not a string or number")
2029
+ if nxt == cursor:
2030
+ raise ValueError("Pagination did not advance: the operation returned the same cursor twice")
2031
+ cursor = nxt
2032
+ elif style == "link":
2033
+ previous = None
2034
+ link_params: Dict[str, Any] = dict(base)
2035
+ while True:
2036
+ page, response = await call(link_params)
2037
+ yield page
2038
+ target = _link_next(response.headers.get("link"))
2039
+ if target is None:
2040
+ return
2041
+ page_url = str(response.request.url) if response.request is not None else ""
2042
+ nxt = urljoin(page_url or "http://relative.invalid", target)
2043
+ if nxt in (previous, page_url):
2044
+ raise ValueError('Pagination did not advance: the Link rel="next" target repeats')
2045
+ previous = nxt
2046
+ merged: Dict[str, Any] = {}
2047
+ for key, value in parse_qsl(urlparse(nxt).query):
2048
+ if key in merged:
2049
+ existing = merged[key]
2050
+ merged[key] = [*existing, value] if isinstance(existing, list) else [existing, value]
2051
+ else:
2052
+ merged[key] = value
2053
+ link_params = {**base, **merged}
2054
+ else:
2055
+ start = base.get(spec["param"])
2056
+ fallback = 1 if style == "page" else 0
2057
+ try:
2058
+ position = fallback if start in (None, "") else int(start)
2059
+ except (TypeError, ValueError):
2060
+ position = fallback
2061
+ previous_items = None
2062
+ while True:
2063
+ page, _response = await call({**base, spec["param"]: position})
2064
+ items = resolve_pointer(page, spec.get("items", ""))
2065
+ serialized = repr(items) if isinstance(items, list) else None
2066
+ if serialized is not None and serialized == previous_items:
2067
+ raise ValueError("Pagination did not advance: the operation returned the same page twice")
2068
+ yield page
2069
+ if not isinstance(items, list) or len(items) == 0:
2070
+ return
2071
+ previous_items = serialized
2072
+ position += 1 if style == "page" else len(items)
2073
+
2074
+
2075
+ async def aiter_items(
2076
+ call: AsyncPageCall, spec: Dict[str, Any], params: Optional[Dict[str, Any]] = None
2077
+ ) -> AsyncIterator[Any]:
2078
+ async for page in aiter_pages(call, spec, params):
2079
+ items = resolve_pointer(page, spec.get("items", ""))
2080
+ if isinstance(items, list):
2081
+ for item in items:
2082
+ yield item
2083
+ `,"_sse.py":`# Server-Sent Events for generated Python clients \u2014 the TypeScript runtime's
2084
+ # sse.ts semantics ported: frame parsing per the EventSource spec (retry must be
2085
+ # ASCII digits; comment-only frames skipped; multi-line data joined with \\n) and
2086
+ # auto-reconnect resuming from the last event id via Last-Event-ID, with
2087
+ # exponential backoff capped at 30s. JSON payloads are parsed when the operation
2088
+ # declares a JSON event stream.
2089
+ from __future__ import annotations
2090
+
2091
+ import asyncio
2092
+ import json
2093
+ import random
2094
+ import time
2095
+ from dataclasses import dataclass
2096
+ from typing import Any, AsyncIterator, Callable, Dict, Iterator, Optional
2097
+
2098
+ import httpx
2099
+
2100
+ _FRAME_DELIMITER = "\\n\\n"
2101
+
2102
+
2103
+ @dataclass
2104
+ class ServerSentEvent:
2105
+ data: Any
2106
+ event: Optional[str] = None
2107
+ id: Optional[str] = None
2108
+ retry: Optional[int] = None
2109
+
2110
+
2111
+ def parse_sse_frame(raw: str, data_kind: str = "text") -> Optional[ServerSentEvent]:
2112
+ event = None
2113
+ data_lines = []
2114
+ event_id = None
2115
+ retry = None
2116
+ saw_field = False
2117
+ for line in raw.replace("\\r\\n", "\\n").replace("\\r", "\\n").split("\\n"):
2118
+ if line == "" or line.startswith(":"):
2119
+ continue
2120
+ field, _, value = line.partition(":")
2121
+ if value.startswith(" "):
2122
+ value = value[1:]
2123
+ saw_field = True
2124
+ if field == "event":
2125
+ event = value
2126
+ elif field == "data":
2127
+ data_lines.append(value)
2128
+ elif field == "id":
2129
+ event_id = value
2130
+ elif field == "retry" and value.isdigit():
2131
+ retry = int(value)
2132
+ if not saw_field:
2133
+ return None
2134
+ text = "\\n".join(data_lines)
2135
+ data: Any = text
2136
+ if data_kind == "json" and text != "":
2137
+ data = json.loads(text)
2138
+ return ServerSentEvent(data=data, event=event, id=event_id, retry=retry)
2139
+
2140
+
2141
+ def iter_sse(
2142
+ open_stream: Callable[[Dict[str, str]], Any],
2143
+ data_kind: str = "text",
2144
+ reconnect: bool = True,
2145
+ reconnect_delay: float = 1.0,
2146
+ ) -> Iterator[ServerSentEvent]:
2147
+ """Iterate an event stream. \`open_stream(extra_headers)\` must return an
2148
+ httpx streaming-response context manager; it is reopened on dropped
2149
+ connections with Last-Event-ID set (fresh call = fresh auth)."""
2150
+ last_event_id: Optional[str] = None
2151
+ server_retry: Optional[float] = None
2152
+ failures = 0
2153
+ while True:
2154
+ headers = {"Accept": "text/event-stream"}
2155
+ if last_event_id is not None:
2156
+ headers["Last-Event-ID"] = last_event_id
2157
+ try:
2158
+ with open_stream(headers) as response:
2159
+ if response.status_code >= 400:
2160
+ response.read()
2161
+ raise httpx.HTTPStatusError(
2162
+ f"SSE request failed with status {response.status_code}",
2163
+ request=response.request,
2164
+ response=response,
2165
+ )
2166
+ failures = 0
2167
+ buffer = ""
2168
+ for chunk in response.iter_text():
2169
+ buffer += chunk
2170
+ while _FRAME_DELIMITER in buffer:
2171
+ raw, buffer = buffer.split(_FRAME_DELIMITER, 1)
2172
+ parsed = parse_sse_frame(raw, data_kind)
2173
+ if parsed is not None:
2174
+ if parsed.id is not None:
2175
+ last_event_id = parsed.id
2176
+ if parsed.retry is not None:
2177
+ server_retry = parsed.retry / 1000
2178
+ yield parsed
2179
+ # Clean end: flush a trailing frame, then finish (no reconnect).
2180
+ if buffer.strip():
2181
+ parsed = parse_sse_frame(buffer, data_kind)
2182
+ if parsed is not None:
2183
+ yield parsed
2184
+ return
2185
+ except httpx.HTTPStatusError:
2186
+ raise # a 4xx/5xx is definitive, not a dropped connection
2187
+ except (httpx.TransportError, httpx.TimeoutException):
2188
+ if not reconnect:
2189
+ raise
2190
+ failures += 1
2191
+ base = server_retry if server_retry is not None else reconnect_delay
2192
+ time.sleep(random.uniform(0, min(base * (2 ** (failures - 1)), 30.0)))
2193
+
2194
+
2195
+ async def aiter_sse(
2196
+ open_stream: Callable[[Dict[str, str]], Any],
2197
+ data_kind: str = "text",
2198
+ reconnect: bool = True,
2199
+ reconnect_delay: float = 1.0,
2200
+ ) -> AsyncIterator[ServerSentEvent]:
2201
+ """Async mirror of iter_sse; \`open_stream\` returns an async context manager."""
2202
+ last_event_id: Optional[str] = None
2203
+ server_retry: Optional[float] = None
2204
+ failures = 0
2205
+ while True:
2206
+ headers = {"Accept": "text/event-stream"}
2207
+ if last_event_id is not None:
2208
+ headers["Last-Event-ID"] = last_event_id
2209
+ try:
2210
+ async with open_stream(headers) as response:
2211
+ if response.status_code >= 400:
2212
+ await response.aread()
2213
+ raise httpx.HTTPStatusError(
2214
+ f"SSE request failed with status {response.status_code}",
2215
+ request=response.request,
2216
+ response=response,
2217
+ )
2218
+ failures = 0
2219
+ buffer = ""
2220
+ async for chunk in response.aiter_text():
2221
+ buffer += chunk
2222
+ while _FRAME_DELIMITER in buffer:
2223
+ raw, buffer = buffer.split(_FRAME_DELIMITER, 1)
2224
+ parsed = parse_sse_frame(raw, data_kind)
2225
+ if parsed is not None:
2226
+ if parsed.id is not None:
2227
+ last_event_id = parsed.id
2228
+ if parsed.retry is not None:
2229
+ server_retry = parsed.retry / 1000
2230
+ yield parsed
2231
+ if buffer.strip():
2232
+ parsed = parse_sse_frame(buffer, data_kind)
2233
+ if parsed is not None:
2234
+ yield parsed
2235
+ return
2236
+ except httpx.HTTPStatusError:
2237
+ raise
2238
+ except (httpx.TransportError, httpx.TimeoutException):
2239
+ if not reconnect:
2240
+ raise
2241
+ failures += 1
2242
+ base = server_retry if server_retry is not None else reconnect_delay
2243
+ await asyncio.sleep(random.uniform(0, min(base * (2 ** (failures - 1)), 30.0)))
2244
+ `,"_multipart.py":`# Multipart bodies for generated Python clients \u2014 a typed dict/dataclass body is
2245
+ # split into httpx's (data, files): bytes and file-like values upload as parts,
2246
+ # everything else is form data (nested values JSON-encoded, mirroring the
2247
+ # TypeScript runtime's FormData serialization).
2248
+ from __future__ import annotations
2249
+
2250
+ import json
2251
+ from typing import Any, Dict, Tuple
2252
+
2253
+ from ._decode import encode
2254
+
2255
+
2256
+ def to_multipart(body: Any) -> Tuple[Dict[str, Any], Dict[str, Any]]:
2257
+ wire = encode(body)
2258
+ data: Dict[str, Any] = {}
2259
+ files: Dict[str, Any] = {}
2260
+ for key, value in (wire or {}).items():
2261
+ if isinstance(value, (bytes, bytearray)) or hasattr(value, "read"):
2262
+ files[key] = value
2263
+ elif isinstance(value, (dict, list)):
2264
+ data[key] = json.dumps(value)
2265
+ else:
2266
+ data[key] = value
2267
+ return data, files
2268
+ `};export{e as a,t as b,n as c};